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