1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package org.drrickorang.loopback; 18 19 import android.content.Context; 20 import android.graphics.Canvas; 21 import android.graphics.Color; 22 import android.graphics.Paint; 23 import android.graphics.Rect; 24 import android.util.AttributeSet; 25 import android.util.Log; 26 import android.view.View; 27 28 29 /** 30 * This is the histogram used to show recorder/player buffer period. 31 */ 32 33 public class HistogramView extends View { 34 private static final String TAG = "HistogramView"; 35 36 37 private Paint mHistPaint; 38 private Paint mTextPaint; 39 private Paint mLinePaint; 40 private Paint mXLabelPaint; 41 42 private int[] mData; // data for buffer period 43 private int[] mDisplayData; // modified data that is used to draw histogram 44 private int mMaxBufferPeriod = 0; 45 // number of x-axis labels excluding the last x-axis label 46 private int mNumberOfXLabel = 5; // mNumberOfXLabel must > 0 47 48 private final int mYAxisBase = 10; // base of y-axis log scale 49 private final int mYLabelSize = 40; 50 private final int mXLabelSize = 40; 51 private final int mLineWidth = 3; 52 private final int mMaxNumberOfBeams = 202; // the max amount of beams to display on the screen 53 54 // Note: if want to change this to base other than 10, must change the way x labels are 55 // displayed. It's currently half-hardcoded. 56 private final int mBucketBase = 10; // a bucket's range 57 58 HistogramView(Context context, AttributeSet attrs)59 public HistogramView(Context context, AttributeSet attrs) { 60 super(context, attrs); 61 initPaints(); 62 } 63 64 65 /** Initiate all the Paint objects. */ initPaints()66 private void initPaints() { 67 mHistPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 68 mHistPaint.setStyle(Paint.Style.FILL); 69 mHistPaint.setColor(Color.BLUE); 70 71 mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 72 mTextPaint.setColor(Color.BLACK); 73 mTextPaint.setTextSize(mYLabelSize); 74 75 mXLabelPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 76 mXLabelPaint.setColor(Color.BLACK); 77 mXLabelPaint.setTextSize(mXLabelSize); 78 79 mLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG); 80 mLinePaint.setColor(Color.BLACK); 81 mLinePaint.setStrokeWidth(mLineWidth); 82 } 83 84 @Override onDraw(Canvas canvas)85 protected void onDraw(Canvas canvas) { 86 super.onDraw(canvas); 87 fillCanvas(canvas, this.getRight(), this.getBottom()); 88 } 89 90 // also called in LoopbackActivity.java fillCanvas(Canvas canvas, int right, int bottom)91 void fillCanvas(Canvas canvas, int right, int bottom) { 92 canvas.drawColor(Color.GRAY); 93 94 if (mData == null || mData.length == 0) { 95 return; 96 } 97 98 int arrayLength = mData.length; 99 boolean exceedBufferPeriodRange; 100 if (mMaxBufferPeriod != 0) { 101 final int extraYMargin = 5; // the extra margin between y labels and y-axis 102 final int beamInterval = 2; // separate each beam in the histogram by such amount 103 int range; // the number of beams that's going to be displayed on histogram 104 if (mMaxBufferPeriod > arrayLength - 1) { 105 range = arrayLength; 106 exceedBufferPeriodRange = true; 107 } else { 108 range = mMaxBufferPeriod + 1; 109 exceedBufferPeriodRange = false; 110 } 111 112 if (range == 0) { 113 return; 114 } 115 116 boolean isUsingDisplayData = false; 117 int oldRange = range; 118 int interval = 1; 119 120 // if there are more beams than allowed to be displayed on screen, 121 // put beams into buckets 122 if (range > mMaxNumberOfBeams) { 123 isUsingDisplayData = true; 124 int bucketOrder = 0; 125 if (exceedBufferPeriodRange) { // there should be one extra beam for 101+ 126 range -= 2; 127 while (range > mMaxNumberOfBeams - 2) { 128 range /= mBucketBase; 129 bucketOrder++; 130 } 131 range += 2; // assuming always XXX1+, not something like 0~473, 474+. 132 133 } else { 134 range--; 135 int temp = range; 136 while (range > mMaxNumberOfBeams - 2) { 137 range /= mBucketBase; 138 bucketOrder++; 139 } 140 141 if ((temp % mBucketBase) != 0) { 142 range += 2; 143 } else { 144 range++; 145 } 146 } 147 148 interval = (int) Math.pow(mBucketBase, bucketOrder); 149 mDisplayData = new int[mMaxNumberOfBeams]; 150 mDisplayData[0] = mData[0]; 151 152 // putting data into buckets. 153 for (int i = 1; i < (range - 1); i++) { 154 for (int j = (((i - 1) * interval) + 1); (j <= (i * interval)); j++) { 155 mDisplayData[i] += mData[j]; 156 } 157 } 158 159 if (exceedBufferPeriodRange) { 160 mDisplayData[range - 1] = mData[oldRange - 1]; 161 } else { 162 for (int i = (((range - 2) * interval) + 1); i < oldRange; i++) { 163 mDisplayData[range - 1] += mData[i]; 164 } 165 } 166 } else { 167 mDisplayData = mData; 168 } 169 170 // calculate the max frequency among all latencies 171 int maxBufferPeriodFreq = 0; 172 for (int i = 1; i < range; i++) { 173 if (mDisplayData[i] > maxBufferPeriodFreq) { 174 maxBufferPeriodFreq = mDisplayData[i]; 175 } 176 } 177 178 if (maxBufferPeriodFreq == 0) { 179 return; 180 } 181 182 // find the closest order of "mYAxisBase" according to maxBufferPeriodFreq 183 int order = (int) Math.ceil((Math.log10(maxBufferPeriodFreq)) / 184 (Math.log10(mYAxisBase))); 185 float height = ((float) (bottom - mXLabelSize - mLineWidth) / (order + 1)); 186 187 // y labels 188 String[] yLabels = new String[order + 2]; // store {"0", "1", "10", ...} for base = 10 189 yLabels[0] = "0"; 190 int yStartPoint = bottom - mXLabelSize - mLineWidth; 191 canvas.drawText(yLabels[0], 0, yStartPoint, mTextPaint); 192 int currentValue = 1; 193 for (int i = 1; i < yLabels.length; i++) { 194 yLabels[i] = Integer.toString(currentValue); 195 // Label is displayed at lower than it should be by the amount of "mYLabelSize" 196 canvas.drawText(yLabels[i], 0, yStartPoint - (i * height) + mYLabelSize, 197 mTextPaint); 198 currentValue *= mYAxisBase; 199 } 200 201 // draw x axis 202 canvas.drawLine(0, bottom - mXLabelSize, right, bottom - mXLabelSize, mLinePaint); 203 204 // draw y axis 205 int yMargin = getTextWidth(yLabels[order + 1], mTextPaint); 206 canvas.drawLine(yMargin + extraYMargin, bottom, yMargin + extraYMargin, 207 0, mLinePaint); 208 209 // width of each beam in the histogram 210 float width = ((float) (right - yMargin - extraYMargin - mLineWidth - 211 (range * beamInterval)) / range); 212 213 // draw x labels 214 String lastXLabel; 215 int xLabelInterval; 216 int xStartPoint = yMargin + extraYMargin + mLineWidth; // position of first beam 217 String[] xLabels; 218 219 // mNumberOfXLabel includes "0" but excludes the last label, which will be at last beam 220 // if mNumberOfXLabel exceeds the total beams that's going to have, reduce its value 221 if (mNumberOfXLabel - 1 > range - 2) { 222 mNumberOfXLabel = range - 1; 223 } 224 225 // 226 if (!isUsingDisplayData) { // in this case each beam represent one buffer period 227 if ((range - 2) < mNumberOfXLabel) { 228 xLabelInterval = 1; 229 } else { 230 xLabelInterval = (range - 2) / mNumberOfXLabel; 231 } 232 233 xLabels = new String[mNumberOfXLabel]; 234 xLabels[0] = "0"; // first label is for 0 235 canvas.drawText(xLabels[0], yMargin + extraYMargin + mLineWidth, bottom, 236 mXLabelPaint); 237 238 float xLabelLineStartX; 239 float xLabelLineStartY; 240 int xLabelLineLength = 10; 241 for (int i = 1; i < mNumberOfXLabel; i++) { 242 xLabelLineStartX = xStartPoint + 243 (xLabelInterval * i * (width + beamInterval)); 244 xLabels[i] = Integer.toString(i * xLabelInterval); 245 canvas.drawText(xLabels[i], xLabelLineStartX, bottom, mXLabelPaint); 246 247 //add a vertical line to indicate label's corresponding beams 248 xLabelLineStartY = bottom - mXLabelSize; 249 canvas.drawLine(xLabelLineStartX, xLabelLineStartY, xLabelLineStartX, 250 xLabelLineStartY - xLabelLineLength, mLinePaint); 251 } 252 253 // last label is for the last beam 254 if (exceedBufferPeriodRange) { 255 lastXLabel = Integer.toString(range - 1) + "+"; 256 } else { 257 lastXLabel = Integer.toString(range - 1); 258 } 259 260 canvas.drawText(lastXLabel, right - getTextWidth(lastXLabel, mXLabelPaint) - 1, 261 bottom, mXLabelPaint); 262 263 } else { // in this case each beam represent a range of buffer period 264 // if mNumberOfXLabel exceeds amount of beams, decrease mNumberOfXLabel 265 if ((range - 2) < mNumberOfXLabel) { 266 xLabelInterval = 1; 267 } else { 268 xLabelInterval = (range - 2) / mNumberOfXLabel; 269 } 270 271 xLabels = new String[mNumberOfXLabel]; 272 xLabels[0] = "0"; // first label is for 0ms 273 canvas.drawText(xLabels[0], yMargin + extraYMargin + mLineWidth, bottom, 274 mXLabelPaint); 275 276 // draw all the middle labels 277 for (int i = 1; i < mNumberOfXLabel; i++) { 278 xLabels[i] = Integer.toString((i * xLabelInterval) - 1) + "1-" + 279 Integer.toString(i * xLabelInterval) + "0"; 280 canvas.drawText(xLabels[i], xStartPoint + (xLabelInterval * i * 281 (width + beamInterval)), bottom, mXLabelPaint); 282 } 283 284 // draw the last label for the last beam 285 if (exceedBufferPeriodRange) { 286 lastXLabel = Integer.toString(oldRange - 1) + "+"; 287 } else { 288 if ((((range - 2) * interval) + 1) == oldRange - 1) { 289 lastXLabel = Integer.toString(oldRange - 1); 290 } else { 291 lastXLabel = Integer.toString(range - 2) + "1-" + 292 Integer.toString(oldRange - 1); 293 } 294 } 295 296 canvas.drawText(lastXLabel, right - getTextWidth(lastXLabel, mXLabelPaint) - 1, 297 bottom, mXLabelPaint); 298 } 299 300 // draw the histogram 301 float currentLeft = yMargin + extraYMargin + mLineWidth; 302 float currentTop; 303 float currentRight; 304 int currentBottom = bottom - mXLabelSize - mLineWidth; 305 for (int i = 0; i < range; i++) { 306 currentRight = currentLeft + width; 307 // calculate the height of the beam. Skip drawing if mDisplayData[i] = 0 308 if (mDisplayData[i] != 0) { 309 float units = (float) (((Math.log10((double) mDisplayData[i])) / 310 Math.log10(mYAxisBase)) + 1.0); 311 currentTop = currentBottom - (height * units); 312 canvas.drawRect(currentLeft, currentTop, currentRight, 313 currentBottom, mHistPaint); 314 } 315 316 currentLeft = currentRight + beamInterval; 317 } 318 319 } 320 } 321 322 323 /** get the width of "text" when using "paint". */ getTextWidth(String text, Paint paint)324 public int getTextWidth(String text, Paint paint) { 325 int width; 326 Rect bounds = new Rect(); 327 paint.getTextBounds(text, 0, text.length(), bounds); 328 width = bounds.left + bounds.width(); 329 return width; 330 } 331 332 /** Copy buffer period data into "mData" */ setBufferPeriodArray(int[] data)333 public void setBufferPeriodArray(int[] data) { 334 if (data == null) { 335 return; 336 } 337 338 if (mData == null || data.length != mData.length) { 339 mData = new int[data.length]; 340 } 341 342 System.arraycopy(data, 0, mData, 0, data.length); 343 } 344 345 setMaxBufferPeriod(int ReadBufferPeriod)346 public void setMaxBufferPeriod(int ReadBufferPeriod) { 347 mMaxBufferPeriod = ReadBufferPeriod; 348 } 349 350 log(String msg)351 private static void log(String msg) { 352 Log.v(TAG, msg); 353 } 354 355 } 356