ChartNetworkSeriesView.java revision e2afc0f283f58ce60c107643978bfff25ec5d5c1
1/* 2 * Copyright (C) 2011 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 17package com.android.settings.widget; 18 19import static android.text.format.DateUtils.DAY_IN_MILLIS; 20import static android.text.format.DateUtils.WEEK_IN_MILLIS; 21 22import android.content.Context; 23import android.content.res.TypedArray; 24import android.graphics.Canvas; 25import android.graphics.Color; 26import android.graphics.DashPathEffect; 27import android.graphics.Paint; 28import android.graphics.Paint.Style; 29import android.graphics.Path; 30import android.graphics.RectF; 31import android.net.NetworkStatsHistory; 32import android.util.AttributeSet; 33import android.util.Log; 34import android.view.View; 35 36import com.android.settings.R; 37import com.google.common.base.Preconditions; 38 39/** 40 * {@link NetworkStatsHistory} series to render inside a {@link ChartView}, 41 * using {@link ChartAxis} to map into screen coordinates. 42 */ 43public class ChartNetworkSeriesView extends View { 44 private static final String TAG = "ChartNetworkSeriesView"; 45 private static final boolean LOGD = false; 46 47 private ChartAxis mHoriz; 48 private ChartAxis mVert; 49 50 private Paint mPaintStroke; 51 private Paint mPaintFill; 52 private Paint mPaintFillSecondary; 53 private Paint mPaintEstimate; 54 55 private NetworkStatsHistory mStats; 56 57 private Path mPathStroke; 58 private Path mPathFill; 59 private Path mPathEstimate; 60 61 private long mPrimaryLeft; 62 private long mPrimaryRight; 63 64 /** Series will be extended to reach this end time. */ 65 private long mEndTime = Long.MIN_VALUE; 66 67 private boolean mEstimateVisible = false; 68 69 private long mMax; 70 private long mMaxEstimate; 71 72 public ChartNetworkSeriesView(Context context) { 73 this(context, null, 0); 74 } 75 76 public ChartNetworkSeriesView(Context context, AttributeSet attrs) { 77 this(context, attrs, 0); 78 } 79 80 public ChartNetworkSeriesView(Context context, AttributeSet attrs, int defStyle) { 81 super(context, attrs, defStyle); 82 83 final TypedArray a = context.obtainStyledAttributes( 84 attrs, R.styleable.ChartNetworkSeriesView, defStyle, 0); 85 86 final int stroke = a.getColor(R.styleable.ChartNetworkSeriesView_strokeColor, Color.RED); 87 final int fill = a.getColor(R.styleable.ChartNetworkSeriesView_fillColor, Color.RED); 88 final int fillSecondary = a.getColor( 89 R.styleable.ChartNetworkSeriesView_fillColorSecondary, Color.RED); 90 91 setChartColor(stroke, fill, fillSecondary); 92 setWillNotDraw(false); 93 94 a.recycle(); 95 96 mPathStroke = new Path(); 97 mPathFill = new Path(); 98 mPathEstimate = new Path(); 99 } 100 101 void init(ChartAxis horiz, ChartAxis vert) { 102 mHoriz = Preconditions.checkNotNull(horiz, "missing horiz"); 103 mVert = Preconditions.checkNotNull(vert, "missing vert"); 104 } 105 106 public void setChartColor(int stroke, int fill, int fillSecondary) { 107 mPaintStroke = new Paint(); 108 mPaintStroke.setStrokeWidth(6.0f); 109 mPaintStroke.setColor(stroke); 110 mPaintStroke.setStyle(Style.STROKE); 111 mPaintStroke.setAntiAlias(true); 112 113 mPaintFill = new Paint(); 114 mPaintFill.setColor(fill); 115 mPaintFill.setStyle(Style.FILL); 116 mPaintFill.setAntiAlias(true); 117 118 mPaintFillSecondary = new Paint(); 119 mPaintFillSecondary.setColor(fillSecondary); 120 mPaintFillSecondary.setStyle(Style.FILL); 121 mPaintFillSecondary.setAntiAlias(true); 122 123 mPaintEstimate = new Paint(); 124 mPaintEstimate.setStrokeWidth(3.0f); 125 mPaintEstimate.setColor(fillSecondary); 126 mPaintEstimate.setStyle(Style.STROKE); 127 mPaintEstimate.setAntiAlias(true); 128 mPaintEstimate.setPathEffect(new DashPathEffect(new float[] { 10, 10 }, 1)); 129 } 130 131 public void bindNetworkStats(NetworkStatsHistory stats) { 132 mStats = stats; 133 134 mPathStroke.reset(); 135 mPathFill.reset(); 136 mPathEstimate.reset(); 137 invalidate(); 138 } 139 140 /** 141 * Set the range to paint with {@link #mPaintFill}, leaving the remaining 142 * area to be painted with {@link #mPaintFillSecondary}. 143 */ 144 public void setPrimaryRange(long left, long right) { 145 mPrimaryLeft = left; 146 mPrimaryRight = right; 147 invalidate(); 148 } 149 150 @Override 151 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 152 generatePath(); 153 } 154 155 /** 156 * Erase any existing {@link Path} and generate series outline based on 157 * currently bound {@link NetworkStatsHistory} data. 158 */ 159 public void generatePath() { 160 if (LOGD) Log.d(TAG, "generatePath()"); 161 162 mMax = 0; 163 mPathStroke.reset(); 164 mPathFill.reset(); 165 mPathEstimate.reset(); 166 167 // bail when not enough stats to render 168 if (mStats == null || mStats.size() < 2) return; 169 170 final int width = getWidth(); 171 final int height = getHeight(); 172 173 boolean started = false; 174 float firstX = 0; 175 float lastX = 0; 176 float lastY = 0; 177 long lastTime = Long.MIN_VALUE; 178 179 // TODO: count fractional data from first bucket crossing start; 180 // currently it only accepts first full bucket. 181 182 long totalData = 0; 183 184 NetworkStatsHistory.Entry entry = null; 185 for (int i = 0; i < mStats.size(); i++) { 186 entry = mStats.getValues(i, entry); 187 188 lastTime = entry.bucketStart + entry.bucketDuration; 189 final float x = mHoriz.convertToPoint(lastTime); 190 final float y = mVert.convertToPoint(totalData); 191 192 // skip until we find first stats on screen 193 if (i > 0 && !started && x > 0) { 194 mPathStroke.moveTo(lastX, lastY); 195 mPathFill.moveTo(lastX, lastY); 196 started = true; 197 firstX = x; 198 } 199 200 if (started) { 201 mPathStroke.lineTo(x, y); 202 mPathFill.lineTo(x, y); 203 totalData += entry.rxBytes + entry.txBytes; 204 } 205 206 // skip if beyond view 207 if (x > width) break; 208 209 lastX = x; 210 lastY = y; 211 } 212 213 // when data falls short, extend to requested end time 214 if (lastTime < mEndTime) { 215 lastX = mHoriz.convertToPoint(mEndTime); 216 217 if (started) { 218 mPathStroke.lineTo(lastX, lastY); 219 mPathFill.lineTo(lastX, lastY); 220 } 221 } 222 223 if (LOGD) { 224 final RectF bounds = new RectF(); 225 mPathFill.computeBounds(bounds, true); 226 Log.d(TAG, "onLayout() rendered with bounds=" + bounds.toString() + " and totalData=" 227 + totalData); 228 } 229 230 // drop to bottom of graph from current location 231 mPathFill.lineTo(lastX, height); 232 mPathFill.lineTo(firstX, height); 233 234 mMax = totalData; 235 236 // build estimated data 237 mPathEstimate.moveTo(lastX, lastY); 238 239 final long now = System.currentTimeMillis(); 240 final long bucketDuration = mStats.getBucketDuration(); 241 242 // long window is average over two weeks 243 entry = mStats.getValues(lastTime - WEEK_IN_MILLIS * 2, lastTime, now, entry); 244 final long longWindow = (entry.rxBytes + entry.txBytes) * bucketDuration 245 / entry.bucketDuration; 246 247 long futureTime = 0; 248 while (lastX < width) { 249 futureTime += bucketDuration; 250 251 // short window is day average last week 252 final long lastWeekTime = lastTime - WEEK_IN_MILLIS + (futureTime % WEEK_IN_MILLIS); 253 entry = mStats.getValues(lastWeekTime - DAY_IN_MILLIS, lastWeekTime, now, entry); 254 final long shortWindow = (entry.rxBytes + entry.txBytes) * bucketDuration 255 / entry.bucketDuration; 256 257 totalData += (longWindow * 7 + shortWindow * 3) / 10; 258 259 lastX = mHoriz.convertToPoint(lastTime + futureTime); 260 lastY = mVert.convertToPoint(totalData); 261 262 mPathEstimate.lineTo(lastX, lastY); 263 } 264 265 mMaxEstimate = totalData; 266 } 267 268 public void setEndTime(long endTime) { 269 mEndTime = endTime; 270 } 271 272 public void setEstimateVisible(boolean estimateVisible) { 273 mEstimateVisible = estimateVisible; 274 invalidate(); 275 } 276 277 public long getMaxEstimate() { 278 return mMaxEstimate; 279 } 280 281 public long getMaxVisible() { 282 return mEstimateVisible ? mMaxEstimate : mMax; 283 } 284 285 @Override 286 protected void onDraw(Canvas canvas) { 287 int save; 288 289 final float primaryLeftPoint = mHoriz.convertToPoint(mPrimaryLeft); 290 final float primaryRightPoint = mHoriz.convertToPoint(mPrimaryRight); 291 292 if (mEstimateVisible) { 293 save = canvas.save(); 294 canvas.clipRect(0, 0, getWidth(), getHeight()); 295 canvas.drawPath(mPathEstimate, mPaintEstimate); 296 canvas.restoreToCount(save); 297 } 298 299 save = canvas.save(); 300 canvas.clipRect(0, 0, primaryLeftPoint, getHeight()); 301 canvas.drawPath(mPathFill, mPaintFillSecondary); 302 canvas.restoreToCount(save); 303 304 save = canvas.save(); 305 canvas.clipRect(primaryRightPoint, 0, getWidth(), getHeight()); 306 canvas.drawPath(mPathFill, mPaintFillSecondary); 307 canvas.restoreToCount(save); 308 309 save = canvas.save(); 310 canvas.clipRect(primaryLeftPoint, 0, primaryRightPoint, getHeight()); 311 canvas.drawPath(mPathFill, mPaintFill); 312 canvas.drawPath(mPathStroke, mPaintStroke); 313 canvas.restoreToCount(save); 314 315 } 316} 317