ChartSweepView.java revision 54d0af57fd2dca14f0c7c34a48942aa6ecdc3f06
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 android.content.Context; 20import android.content.res.TypedArray; 21import android.graphics.Canvas; 22import android.graphics.Color; 23import android.graphics.Paint; 24import android.graphics.Point; 25import android.graphics.Rect; 26import android.graphics.drawable.Drawable; 27import android.text.DynamicLayout; 28import android.text.Layout.Alignment; 29import android.text.SpannableStringBuilder; 30import android.text.TextPaint; 31import android.util.AttributeSet; 32import android.util.MathUtils; 33import android.view.MotionEvent; 34import android.view.View; 35 36import com.android.settings.R; 37import com.google.common.base.Preconditions; 38 39/** 40 * Sweep across a {@link ChartView} at a specific {@link ChartAxis} value, which 41 * a user can drag. 42 */ 43public class ChartSweepView extends View { 44 45 private Drawable mSweep; 46 private Rect mSweepPadding = new Rect(); 47 private Point mSweepOffset = new Point(); 48 49 private Rect mMargins = new Rect(); 50 private float mNeighborMargin; 51 52 private int mFollowAxis; 53 54 private int mLabelSize; 55 private int mLabelTemplateRes; 56 private int mLabelColor; 57 58 private SpannableStringBuilder mLabelTemplate; 59 private DynamicLayout mLabelLayout; 60 61 private ChartAxis mAxis; 62 private long mValue; 63 64 private long mValidAfter; 65 private long mValidBefore; 66 private ChartSweepView mValidAfterDynamic; 67 private ChartSweepView mValidBeforeDynamic; 68 69 public static final int HORIZONTAL = 0; 70 public static final int VERTICAL = 1; 71 72 public interface OnSweepListener { 73 public void onSweep(ChartSweepView sweep, boolean sweepDone); 74 } 75 76 private OnSweepListener mListener; 77 private MotionEvent mTracking; 78 79 public ChartSweepView(Context context) { 80 this(context, null); 81 } 82 83 public ChartSweepView(Context context, AttributeSet attrs) { 84 this(context, attrs, 0); 85 } 86 87 public ChartSweepView(Context context, AttributeSet attrs, int defStyle) { 88 super(context, attrs, defStyle); 89 90 final TypedArray a = context.obtainStyledAttributes( 91 attrs, R.styleable.ChartSweepView, defStyle, 0); 92 93 setSweepDrawable(a.getDrawable(R.styleable.ChartSweepView_sweepDrawable)); 94 setFollowAxis(a.getInt(R.styleable.ChartSweepView_followAxis, -1)); 95 setNeighborMargin(a.getDimensionPixelSize(R.styleable.ChartSweepView_neighborMargin, 0)); 96 97 setLabelSize(a.getDimensionPixelSize(R.styleable.ChartSweepView_labelSize, 0)); 98 setLabelTemplate(a.getResourceId(R.styleable.ChartSweepView_labelTemplate, 0)); 99 setLabelColor(a.getColor(R.styleable.ChartSweepView_labelColor, Color.BLUE)); 100 101 a.recycle(); 102 103 setWillNotDraw(false); 104 } 105 106 void init(ChartAxis axis) { 107 mAxis = Preconditions.checkNotNull(axis, "missing axis"); 108 } 109 110 public int getFollowAxis() { 111 return mFollowAxis; 112 } 113 114 public Rect getMargins() { 115 return mMargins; 116 } 117 118 /** 119 * Return the number of pixels that the "target" area is inset from the 120 * {@link View} edge, along the current {@link #setFollowAxis(int)}. 121 */ 122 private float getTargetInset() { 123 if (mFollowAxis == VERTICAL) { 124 final float targetHeight = mSweep.getIntrinsicHeight() - mSweepPadding.top 125 - mSweepPadding.bottom; 126 return mSweepPadding.top + (targetHeight / 2); 127 } else { 128 final float targetWidth = mSweep.getIntrinsicWidth() - mSweepPadding.left 129 - mSweepPadding.right; 130 return mSweepPadding.left + (targetWidth / 2); 131 } 132 } 133 134 public void addOnSweepListener(OnSweepListener listener) { 135 mListener = listener; 136 } 137 138 private void dispatchOnSweep(boolean sweepDone) { 139 if (mListener != null) { 140 mListener.onSweep(this, sweepDone); 141 } 142 } 143 144 @Override 145 public void setEnabled(boolean enabled) { 146 super.setEnabled(enabled); 147 requestLayout(); 148 } 149 150 public void setSweepDrawable(Drawable sweep) { 151 if (mSweep != null) { 152 mSweep.setCallback(null); 153 unscheduleDrawable(mSweep); 154 } 155 156 if (sweep != null) { 157 sweep.setCallback(this); 158 if (sweep.isStateful()) { 159 sweep.setState(getDrawableState()); 160 } 161 sweep.setVisible(getVisibility() == VISIBLE, false); 162 mSweep = sweep; 163 sweep.getPadding(mSweepPadding); 164 } else { 165 mSweep = null; 166 } 167 168 invalidate(); 169 } 170 171 public void setFollowAxis(int followAxis) { 172 mFollowAxis = followAxis; 173 } 174 175 public void setLabelSize(int size) { 176 mLabelSize = size; 177 invalidateLabelTemplate(); 178 } 179 180 public void setLabelTemplate(int resId) { 181 mLabelTemplateRes = resId; 182 invalidateLabelTemplate(); 183 } 184 185 public void setLabelColor(int color) { 186 mLabelColor = color; 187 invalidateLabelTemplate(); 188 } 189 190 private void invalidateLabelTemplate() { 191 if (mLabelTemplateRes != 0) { 192 final CharSequence template = getResources().getText(mLabelTemplateRes); 193 194 final TextPaint paint = new TextPaint(Paint.ANTI_ALIAS_FLAG); 195 paint.density = getResources().getDisplayMetrics().density; 196 paint.setCompatibilityScaling(getResources().getCompatibilityInfo().applicationScale); 197 paint.setColor(mLabelColor); 198 199 mLabelTemplate = new SpannableStringBuilder(template); 200 mLabelLayout = new DynamicLayout( 201 mLabelTemplate, paint, mLabelSize, Alignment.ALIGN_RIGHT, 1f, 0f, false); 202 invalidateLabel(); 203 204 } else { 205 mLabelTemplate = null; 206 mLabelLayout = null; 207 } 208 209 invalidate(); 210 requestLayout(); 211 } 212 213 private void invalidateLabel() { 214 if (mLabelTemplate != null && mAxis != null) { 215 mAxis.buildLabel(getResources(), mLabelTemplate, mValue); 216 invalidate(); 217 } 218 } 219 220 @Override 221 public void jumpDrawablesToCurrentState() { 222 super.jumpDrawablesToCurrentState(); 223 if (mSweep != null) { 224 mSweep.jumpToCurrentState(); 225 } 226 } 227 228 @Override 229 public void setVisibility(int visibility) { 230 super.setVisibility(visibility); 231 if (mSweep != null) { 232 mSweep.setVisible(visibility == VISIBLE, false); 233 } 234 } 235 236 @Override 237 protected boolean verifyDrawable(Drawable who) { 238 return who == mSweep || super.verifyDrawable(who); 239 } 240 241 public ChartAxis getAxis() { 242 return mAxis; 243 } 244 245 public void setValue(long value) { 246 mValue = value; 247 invalidateLabel(); 248 } 249 250 public long getValue() { 251 return mValue; 252 } 253 254 public float getPoint() { 255 if (isEnabled()) { 256 return mAxis.convertToPoint(mValue); 257 } else { 258 // when disabled, show along top edge 259 return 0; 260 } 261 } 262 263 /** 264 * Set valid range this sweep can move within, in {@link #mAxis} values. The 265 * most restrictive combination of all valid ranges is used. 266 */ 267 public void setValidRange(long validAfter, long validBefore) { 268 mValidAfter = validAfter; 269 mValidBefore = validBefore; 270 } 271 272 public void setNeighborMargin(float neighborMargin) { 273 mNeighborMargin = neighborMargin; 274 } 275 276 /** 277 * Set valid range this sweep can move within, defined by the given 278 * {@link ChartSweepView}. The most restrictive combination of all valid 279 * ranges is used. 280 */ 281 public void setValidRangeDynamic(ChartSweepView validAfter, ChartSweepView validBefore) { 282 mValidAfterDynamic = validAfter; 283 mValidBeforeDynamic = validBefore; 284 } 285 286 @Override 287 public boolean onTouchEvent(MotionEvent event) { 288 if (!isEnabled()) return false; 289 290 final View parent = (View) getParent(); 291 switch (event.getAction()) { 292 case MotionEvent.ACTION_DOWN: { 293 294 // only start tracking when in sweet spot 295 final boolean accept; 296 if (mFollowAxis == VERTICAL) { 297 accept = event.getX() > getWidth() - (mSweepPadding.right * 2); 298 } else { 299 accept = event.getY() > getHeight() - (mSweepPadding.bottom * 2); 300 } 301 302 if (accept) { 303 mTracking = event.copy(); 304 305 // starting drag should activate entire chart 306 if (!parent.isActivated()) { 307 parent.setActivated(true); 308 } 309 310 return true; 311 } else { 312 return false; 313 } 314 } 315 case MotionEvent.ACTION_MOVE: { 316 getParent().requestDisallowInterceptTouchEvent(true); 317 318 // content area of parent 319 final Rect parentContent = getParentContentRect(); 320 final Rect clampRect = computeClampRect(parentContent); 321 322 if (mFollowAxis == VERTICAL) { 323 final float currentTargetY = getTop() - mMargins.top; 324 final float requestedTargetY = currentTargetY 325 + (event.getRawY() - mTracking.getRawY()); 326 final float clampedTargetY = MathUtils.constrain( 327 requestedTargetY, clampRect.top, clampRect.bottom); 328 setTranslationY(clampedTargetY - currentTargetY); 329 330 setValue(mAxis.convertToValue(clampedTargetY - parentContent.top)); 331 } else { 332 final float currentTargetX = getLeft() - mMargins.left; 333 final float requestedTargetX = currentTargetX 334 + (event.getRawX() - mTracking.getRawX()); 335 final float clampedTargetX = MathUtils.constrain( 336 requestedTargetX, clampRect.left, clampRect.right); 337 setTranslationX(clampedTargetX - currentTargetX); 338 339 setValue(mAxis.convertToValue(clampedTargetX - parentContent.left)); 340 } 341 342 dispatchOnSweep(false); 343 return true; 344 } 345 case MotionEvent.ACTION_UP: { 346 mTracking = null; 347 dispatchOnSweep(true); 348 setTranslationX(0); 349 setTranslationY(0); 350 requestLayout(); 351 return true; 352 } 353 default: { 354 return false; 355 } 356 } 357 } 358 359 /** 360 * Update {@link #mValue} based on current position, including any 361 * {@link #onTouchEvent(MotionEvent)} in progress. Typically used when 362 * {@link ChartAxis} changes during sweep adjustment. 363 */ 364 public void updateValueFromPosition() { 365 final Rect parentContent = getParentContentRect(); 366 if (mFollowAxis == VERTICAL) { 367 final float effectiveY = getY() - mMargins.top - parentContent.top; 368 setValue(mAxis.convertToValue(effectiveY)); 369 } else { 370 final float effectiveX = getX() - mMargins.left - parentContent.left; 371 setValue(mAxis.convertToValue(effectiveX)); 372 } 373 } 374 375 public int shouldAdjustAxis() { 376 return mAxis.shouldAdjustAxis(getValue()); 377 } 378 379 private Rect getParentContentRect() { 380 final View parent = (View) getParent(); 381 return new Rect(parent.getPaddingLeft(), parent.getPaddingTop(), 382 parent.getWidth() - parent.getPaddingRight(), 383 parent.getHeight() - parent.getPaddingBottom()); 384 } 385 386 @Override 387 public void addOnLayoutChangeListener(OnLayoutChangeListener listener) { 388 // ignored to keep LayoutTransition from animating us 389 } 390 391 @Override 392 public void removeOnLayoutChangeListener(OnLayoutChangeListener listener) { 393 // ignored to keep LayoutTransition from animating us 394 } 395 396 private long getValidAfterDynamic() { 397 final ChartSweepView dynamic = mValidAfterDynamic; 398 return dynamic != null && dynamic.isEnabled() ? dynamic.getValue() : Long.MIN_VALUE; 399 } 400 401 private long getValidBeforeDynamic() { 402 final ChartSweepView dynamic = mValidBeforeDynamic; 403 return dynamic != null && dynamic.isEnabled() ? dynamic.getValue() : Long.MAX_VALUE; 404 } 405 406 /** 407 * Compute {@link Rect} in {@link #getParent()} coordinates that we should 408 * be clamped inside of, usually from {@link #setValidRange(long, long)} 409 * style rules. 410 */ 411 private Rect computeClampRect(Rect parentContent) { 412 // create two rectangles, and pick most restrictive combination 413 final Rect rect = buildClampRect(parentContent, mValidAfter, mValidBefore, 0f); 414 final Rect dynamicRect = buildClampRect( 415 parentContent, getValidAfterDynamic(), getValidBeforeDynamic(), mNeighborMargin); 416 417 rect.intersect(dynamicRect); 418 return rect; 419 } 420 421 private Rect buildClampRect( 422 Rect parentContent, long afterValue, long beforeValue, float margin) { 423 if (mAxis instanceof InvertedChartAxis) { 424 long temp = beforeValue; 425 beforeValue = afterValue; 426 afterValue = temp; 427 } 428 429 final boolean afterValid = afterValue != Long.MIN_VALUE && afterValue != Long.MAX_VALUE; 430 final boolean beforeValid = beforeValue != Long.MIN_VALUE && beforeValue != Long.MAX_VALUE; 431 432 final float afterPoint = mAxis.convertToPoint(afterValue) + margin; 433 final float beforePoint = mAxis.convertToPoint(beforeValue) - margin; 434 435 final Rect clampRect = new Rect(parentContent); 436 if (mFollowAxis == VERTICAL) { 437 if (beforeValid) clampRect.bottom = clampRect.top + (int) beforePoint; 438 if (afterValid) clampRect.top += afterPoint; 439 } else { 440 if (beforeValid) clampRect.right = clampRect.left + (int) beforePoint; 441 if (afterValid) clampRect.left += afterPoint; 442 } 443 return clampRect; 444 } 445 446 @Override 447 protected void drawableStateChanged() { 448 super.drawableStateChanged(); 449 if (mSweep.isStateful()) { 450 mSweep.setState(getDrawableState()); 451 } 452 } 453 454 @Override 455 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 456 457 // TODO: handle vertical labels 458 if (isEnabled() && mLabelLayout != null) { 459 final int sweepHeight = mSweep.getIntrinsicHeight(); 460 final int templateHeight = mLabelLayout.getHeight(); 461 462 mSweepOffset.x = 0; 463 mSweepOffset.y = (int) ((templateHeight / 2) - getTargetInset()); 464 setMeasuredDimension(mSweep.getIntrinsicWidth(), Math.max(sweepHeight, templateHeight)); 465 466 } else { 467 mSweepOffset.x = 0; 468 mSweepOffset.y = 0; 469 setMeasuredDimension(mSweep.getIntrinsicWidth(), mSweep.getIntrinsicHeight()); 470 } 471 472 if (mFollowAxis == VERTICAL) { 473 final int targetHeight = mSweep.getIntrinsicHeight() - mSweepPadding.top 474 - mSweepPadding.bottom; 475 mMargins.top = -(mSweepPadding.top + (targetHeight / 2)); 476 mMargins.bottom = 0; 477 mMargins.left = -mSweepPadding.left; 478 mMargins.right = mSweepPadding.right; 479 } else { 480 final int targetWidth = mSweep.getIntrinsicWidth() - mSweepPadding.left 481 - mSweepPadding.right; 482 mMargins.left = -(mSweepPadding.left + (targetWidth / 2)); 483 mMargins.right = 0; 484 mMargins.top = -mSweepPadding.top; 485 mMargins.bottom = mSweepPadding.bottom; 486 } 487 488 mMargins.offset(-mSweepOffset.x, -mSweepOffset.y); 489 } 490 491 @Override 492 protected void onDraw(Canvas canvas) { 493 final int width = getWidth(); 494 final int height = getHeight(); 495 496 final int labelSize; 497 if (isEnabled() && mLabelLayout != null) { 498 mLabelLayout.draw(canvas); 499 labelSize = mLabelSize; 500 } else { 501 labelSize = 0; 502 } 503 504 if (mFollowAxis == VERTICAL) { 505 mSweep.setBounds(labelSize, mSweepOffset.y, width, 506 mSweepOffset.y + mSweep.getIntrinsicHeight()); 507 } else { 508 mSweep.setBounds(mSweepOffset.x, labelSize, 509 mSweepOffset.x + mSweep.getIntrinsicWidth(), height); 510 } 511 512 mSweep.draw(canvas); 513 } 514 515} 516