PieRenderer.java revision d028bb06a5ec6c23c82195f099ac7b24983ac4a4
1/* 2 * Copyright (C) 2012 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.camera.ui; 18 19import android.content.Context; 20import android.content.res.Resources; 21import android.graphics.Canvas; 22import android.graphics.Color; 23import android.graphics.Paint; 24import android.graphics.Path; 25import android.graphics.Point; 26import android.graphics.PointF; 27import android.graphics.RectF; 28import android.os.Handler; 29import android.os.Message; 30import android.view.MotionEvent; 31import android.view.View; 32import android.view.animation.Animation; 33import android.view.animation.Transformation; 34 35import com.android.camera.R; 36 37import java.util.ArrayList; 38import java.util.List; 39 40public class PieRenderer extends OverlayRenderer 41 implements FocusIndicator { 42 43 private static final String TAG = "CAM Pie"; 44 45 // Sometimes continuous autofocus starts and stops several times quickly. 46 // These states are used to make sure the animation is run for at least some 47 // time. 48 private int mState; 49 private ScaleAnimation mAnimation = new ScaleAnimation(); 50 private static final int STATE_IDLE = 0; 51 private static final int STATE_FOCUSING = 1; 52 private static final int STATE_FINISHING = 2; 53 private static final int STATE_PIE = 3; 54 55 private Runnable mDisappear = new Disappear(); 56 private Animation.AnimationListener mEndAction = new EndAction(); 57 private static final int SCALING_UP_TIME = 600; 58 private static final int SCALING_DOWN_TIME = 100; 59 private static final int DISAPPEAR_TIMEOUT = 200; 60 private static final int DIAL_HORIZONTAL = 157; 61 62 private static final long PIE_OPEN_DELAY = 200; 63 private static final long FOCUS_TAP_TIMEOUT = 500; 64 65 private static final int MSG_OPEN = 2; 66 private static final int MSG_CLOSE = 3; 67 private static final int MSG_SUBMENU = 4; 68 private static final int MSG_FOCUS_TAP = 5; 69 private static final float PIE_SWEEP = (float)(Math.PI * 2 / 3); 70 // geometry 71 private Point mCenter; 72 private int mRadius; 73 private int mRadiusInc; 74 private int mSlop; 75 76 // the detection if touch is inside a slice is offset 77 // inbounds by this amount to allow the selection to show before the 78 // finger covers it 79 private int mTouchOffset; 80 81 private List<PieItem> mItems; 82 83 private PieItem mOpenItem; 84 85 private Paint mNormalPaint; 86 private Paint mSelectedPaint; 87 private Paint mSubPaint; 88 89 // touch handling 90 private PieItem mCurrentItem; 91 92 private Paint mFocusPaint; 93 private int mSuccessColor; 94 private int mFailColor; 95 private int mCircleSize; 96 private int mFocusX; 97 private int mFocusY; 98 private int mCenterX; 99 private int mCenterY; 100 101 private int mDialAngle; 102 private RectF mCircle; 103 private RectF mDial; 104 private Point mPoint1; 105 private Point mPoint2; 106 private int mStartAnimationAngle; 107 private boolean mFocused; 108 private int mInnerOffset; 109 private int mOuterStroke; 110 private int mInnerStroke; 111 private boolean mFocusFromTap; 112 private boolean mTapMode; 113 private boolean mBlockFocus; 114 115 116 private Handler mHandler = new Handler() { 117 public void handleMessage(Message msg) { 118 switch(msg.what) { 119 case MSG_OPEN: 120 if (mListener != null) { 121 mListener.onPieOpened(mCenter.x, mCenter.y); 122 } 123 break; 124 case MSG_CLOSE: 125 if (mListener != null) { 126 mListener.onPieClosed(); 127 } 128 break; 129 case MSG_SUBMENU: 130 openCurrentItem(); 131 break; 132 case MSG_FOCUS_TAP: 133 // reset flag 134 mTapMode = false; 135 if (mState == STATE_PIE) { 136 show(false); 137 } 138 break; 139 } 140 } 141 }; 142 143 private PieListener mListener; 144 145 static public interface PieListener { 146 public void onPieOpened(int centerX, int centerY); 147 public void onPieClosed(); 148 } 149 150 public void setPieListener(PieListener pl) { 151 mListener = pl; 152 } 153 154 public PieRenderer(Context context) { 155 init(context); 156 } 157 private void init(Context ctx) { 158 setVisible(false); 159 mItems = new ArrayList<PieItem>(); 160 Resources res = ctx.getResources(); 161 mRadius = (int) res.getDimensionPixelSize(R.dimen.pie_radius_start); 162 mCircleSize = mRadius - res.getDimensionPixelSize(R.dimen.focus_radius_offset); 163 mRadiusInc = (int) res.getDimensionPixelSize(R.dimen.pie_radius_increment); 164 mSlop = (int) res.getDimensionPixelSize(R.dimen.pie_touch_slop); 165 mTouchOffset = (int) res.getDimensionPixelSize(R.dimen.pie_touch_offset); 166 mCenter = new Point(0,0); 167 mNormalPaint = new Paint(); 168 mNormalPaint.setColor(Color.argb(0, 0, 0, 0)); 169 mNormalPaint.setAntiAlias(true); 170 mSelectedPaint = new Paint(); 171 mSelectedPaint.setColor(Color.argb(255, 51, 181, 229)); 172 mSelectedPaint.setAntiAlias(true); 173 mSubPaint = new Paint(); 174 mSubPaint.setAntiAlias(true); 175 mSubPaint.setColor(Color.argb(200, 250, 230, 128)); 176 mFocusPaint = new Paint(); 177 mFocusPaint.setAntiAlias(true); 178 mFocusPaint.setColor(Color.WHITE); 179 mFocusPaint.setStyle(Paint.Style.STROKE); 180 mSuccessColor = Color.GREEN; 181 mFailColor = Color.RED; 182 mCircle = new RectF(); 183 mDial = new RectF(); 184 mPoint1 = new Point(); 185 mPoint2 = new Point(); 186 mInnerOffset = res.getDimensionPixelSize(R.dimen.focus_inner_offset); 187 mOuterStroke = res.getDimensionPixelSize(R.dimen.focus_outer_stroke); 188 mInnerStroke = res.getDimensionPixelSize(R.dimen.focus_inner_stroke); 189 mState = STATE_IDLE; 190 mBlockFocus = false; 191 } 192 193 public boolean showsItems() { 194 return mTapMode; 195 } 196 197 public void addItem(PieItem item) { 198 // add the item to the pie itself 199 mItems.add(item); 200 } 201 202 public void removeItem(PieItem item) { 203 mItems.remove(item); 204 } 205 206 public void clearItems() { 207 mItems.clear(); 208 } 209 210 public void showInCenter() { 211 if ((mState == STATE_PIE) && isVisible()) { 212 mTapMode = false; 213 show(false); 214 } else { 215 if (mState != STATE_IDLE) { 216 mHandler.removeMessages(MSG_FOCUS_TAP); 217 cancelFocus(); 218 } 219 mState = STATE_PIE; 220 setCenter(mCenterX, mCenterY); 221 mTapMode = true; 222 show(true); 223 } 224 } 225 226 public void hide() { 227 show(false); 228 } 229 230 /** 231 * guaranteed has center set 232 * @param show 233 */ 234 private void show(boolean show) { 235 if (show) { 236 mState = STATE_PIE; 237 // ensure clean state 238 mCurrentItem = null; 239 mOpenItem = null; 240 for (PieItem item : mItems) { 241 item.setSelected(false); 242 } 243 layoutPie(); 244 } else { 245 mState = STATE_IDLE; 246 } 247 setVisible(show); 248 mHandler.sendEmptyMessage(show ? MSG_OPEN : MSG_CLOSE); 249 } 250 251 public void setCenter(int x, int y) { 252 mCenter.x = x; 253 mCenter.y = y; 254 // when using the pie menu, align the focus ring 255 alignFocus(x, y); 256 } 257 258 private void setupPie(int x, int y) { 259 // when using the focus ring, align pie items 260 mCenter.x = x; 261 mCenter.y = y; 262 mCurrentItem = null; 263 mOpenItem = null; 264 for (PieItem item : mItems) { 265 item.setSelected(false); 266 } 267 layoutPie(); 268 } 269 270 private void layoutPie() { 271 int rgap = 2; 272 int inner = mRadius + rgap; 273 int outer = mRadius + mRadiusInc - rgap; 274 int gap = 1; 275 layoutItems(mItems, (float) (Math.PI / 2), inner, outer, gap); 276 } 277 278 private void layoutItems(List<PieItem> items, float centerAngle, int inner, 279 int outer, int gap) { 280 float emptyangle = PIE_SWEEP / 16; 281 float sweep = (float) (PIE_SWEEP - 2 * emptyangle) / items.size(); 282 float angle = centerAngle - PIE_SWEEP / 2 + emptyangle + sweep / 2; 283 // check if we have custom geometry 284 // first item we find triggers custom sweep for all 285 // this allows us to re-use the path 286 for (PieItem item : items) { 287 if (item.getCenter() >= 0) { 288 sweep = item.getSweep(); 289 break; 290 } 291 } 292 Path path = makeSlice(getDegrees(0) - gap, getDegrees(sweep) + gap, 293 outer, inner, mCenter); 294 for (PieItem item : items) { 295 // shared between items 296 item.setPath(path); 297 View view = item.getView(); 298 if (item.getCenter() >= 0) { 299 angle = item.getCenter(); 300 } 301 if (view != null) { 302 view.measure(view.getLayoutParams().width, 303 view.getLayoutParams().height); 304 int w = view.getMeasuredWidth(); 305 int h = view.getMeasuredHeight(); 306 // move views to outer border 307 int r = inner + (outer - inner) * 2 / 3; 308 int x = (int) (r * Math.cos(angle)); 309 int y = mCenter.y - (int) (r * Math.sin(angle)) - h / 2; 310 x = mCenter.x + x - w / 2; 311 view.layout(x, y, x + w, y + h); 312 } 313 float itemstart = angle - sweep / 2; 314 item.setGeometry(itemstart, sweep, inner, outer); 315 if (item.hasItems()) { 316 layoutItems(item.getItems(), angle, inner, 317 outer + mRadiusInc / 2, gap); 318 } 319 angle += sweep; 320 } 321 } 322 323 private Path makeSlice(float start, float end, int outer, int inner, Point center) { 324 RectF bb = 325 new RectF(center.x - outer, center.y - outer, center.x + outer, 326 center.y + outer); 327 RectF bbi = 328 new RectF(center.x - inner, center.y - inner, center.x + inner, 329 center.y + inner); 330 Path path = new Path(); 331 path.arcTo(bb, start, end - start, true); 332 path.arcTo(bbi, end, start - end); 333 path.close(); 334 return path; 335 } 336 337 /** 338 * converts a 339 * @param angle from 0..PI to Android degrees (clockwise starting at 3 o'clock) 340 * @return skia angle 341 */ 342 private float getDegrees(double angle) { 343 return (float) (360 - 180 * angle / Math.PI); 344 } 345 346 @Override 347 public void onDraw(Canvas canvas) { 348 drawFocus(canvas); 349 if (mState == STATE_FINISHING) return; 350 if (mOpenItem == null) { 351 // draw base menu 352 for (PieItem item : mItems) { 353 drawItem(canvas, item); 354 } 355 } else { 356 for (PieItem inner : mOpenItem.getItems()) { 357 drawItem(canvas, inner); 358 } 359 } 360 } 361 362 private void drawItem(Canvas canvas, PieItem item) { 363 if ((mState == STATE_PIE) && (item.getView() != null)) { 364 if (item.getPath() != null) { 365 Paint p = item.isSelected() ? mSelectedPaint : mNormalPaint; 366 int state = canvas.save(); 367 float r = getDegrees(item.getStartAngle()); 368 canvas.rotate(r, mCenter.x, mCenter.y); 369 canvas.drawPath(item.getPath(), p); 370 canvas.restoreToCount(state); 371 // draw the item view 372 View view = item.getView(); 373 state = canvas.save(); 374 canvas.translate(view.getX(), view.getY()); 375 view.draw(canvas); 376 canvas.restoreToCount(state); 377 } 378 } 379 } 380 381 // touch handling for pie 382 383 @Override 384 public boolean onTouchEvent(MotionEvent evt) { 385 float x = evt.getX(); 386 float y = evt.getY(); 387 int action = evt.getActionMasked(); 388 PointF polar = getPolar(x, y, !(mTapMode)); 389 if (MotionEvent.ACTION_DOWN == action) { 390 if (mTapMode) { 391 PieItem item = findItem(polar); 392 if ((item != null) && (mCurrentItem != item)) { 393 mHandler.removeMessages(MSG_FOCUS_TAP); 394 mState = STATE_PIE; 395 onEnter(item); 396 } 397 } else { 398 setCenter((int) x, (int) y); 399 show(true); 400 } 401 return true; 402 } else if (MotionEvent.ACTION_UP == action) { 403 if (mTapMode) { 404 PieItem item = findItem(polar); 405 if (item == null) { 406 mState = STATE_IDLE; 407 show(false); 408 mTapMode = false; 409 } else { 410 if (!item.hasItems()) { 411 show(false); 412 mTapMode = false; 413 mState = STATE_IDLE; 414 item.getView().performClick(); 415 item.setSelected(false); 416 } 417 } 418 return true; 419 } else if (isVisible()) { 420 PieItem item = mCurrentItem; 421 deselect(); 422 show(false); 423 if ((item != null) && (item.getView() != null)) { 424 item.getView().performClick(); 425 } 426 return true; 427 } 428 } else if (MotionEvent.ACTION_CANCEL == action) { 429 if (isVisible() || mTapMode) { 430 show(false); 431 } 432 deselect(); 433 return false; 434 } else if (MotionEvent.ACTION_MOVE == action) { 435 if (polar.y < mRadius) { 436 if (mOpenItem != null) { 437 mOpenItem = null; 438 } else { 439 deselect(); 440 } 441 return false; 442 } 443 PieItem item = findItem(polar); 444 if ((item != null) && (mCurrentItem != item)) { 445 onEnter(item); 446 } 447 } 448 return false; 449 } 450 451 /** 452 * enter a slice for a view 453 * updates model only 454 * @param item 455 */ 456 private void onEnter(PieItem item) { 457 if (mCurrentItem != null) { 458 mCurrentItem.setSelected(false); 459 } 460 if (item != null && item.isEnabled()) { 461 item.setSelected(true); 462 mCurrentItem = item; 463 if ((mCurrentItem != mOpenItem) && mCurrentItem.hasItems()) { 464 mHandler.sendEmptyMessageDelayed(MSG_SUBMENU, PIE_OPEN_DELAY); 465 } 466 } else { 467 mCurrentItem = null; 468 } 469 } 470 471 private void deselect() { 472 if (mCurrentItem != null) { 473 mCurrentItem.setSelected(false); 474 mHandler.removeMessages(MSG_SUBMENU); 475 } 476 if (mOpenItem != null) { 477 mOpenItem = null; 478 } 479 mCurrentItem = null; 480 } 481 482 private void openCurrentItem() { 483 if ((mCurrentItem != null) && mCurrentItem.hasItems()) { 484 mCurrentItem.setSelected(false); 485 mOpenItem = mCurrentItem; 486 } 487 } 488 489 private PointF getPolar(float x, float y, boolean useOffset) { 490 PointF res = new PointF(); 491 // get angle and radius from x/y 492 res.x = (float) Math.PI / 2; 493 x = x - mCenter.x; 494 y = mCenter.y - y; 495 res.y = (float) Math.sqrt(x * x + y * y); 496 if (x != 0) { 497 res.x = (float) Math.atan2(y, x); 498 if (res.x < 0) { 499 res.x = (float) (2 * Math.PI + res.x); 500 } 501 } 502 res.y = res.y + (useOffset ? mTouchOffset : 0); 503 return res; 504 } 505 506 /** 507 * @param polar x: angle, y: dist 508 * @return the item at angle/dist or null 509 */ 510 private PieItem findItem(PointF polar) { 511 // find the matching item: 512 List<PieItem> items = (mOpenItem != null) ? mOpenItem.getItems() : mItems; 513 for (PieItem item : items) { 514 if (inside(polar, item)) { 515 return item; 516 } 517 } 518 return null; 519 } 520 521 private boolean inside(PointF polar, PieItem item) { 522 return (item.getInnerRadius() < polar.y) 523 && (item.getStartAngle() < polar.x) 524 && (item.getStartAngle() + item.getSweep() > polar.x) 525 && (!mTapMode || (item.getOuterRadius() > polar.y)); 526 } 527 528 @Override 529 public boolean handlesTouch() { 530 return true; 531 } 532 533 // focus specific code 534 535 public void setBlockFocus(boolean blocked) { 536 mBlockFocus = blocked; 537 if (blocked) { 538 clear(); 539 } 540 } 541 542 public void setFocus(int x, int y, boolean startImmediately) { 543 mFocusFromTap = true; 544 mTapMode = true; 545 switch(mOverlay.getOrientation()) { 546 case 0: 547 mFocusX = x; 548 mFocusY = y; 549 break; 550 case 180: 551 mFocusX = getWidth() - x; 552 mFocusY = getHeight() - y; 553 break; 554 case 90: 555 mFocusX = getWidth() - y; 556 mFocusY = x; 557 break; 558 case 270: 559 mFocusX = y ; 560 mFocusY = getHeight() - x; 561 break; 562 } 563 setCircle(mFocusX, mFocusY); 564 setupPie(mFocusX, mFocusY); 565 if (startImmediately) { 566 // cameras that don't support focus still need to show menu 567 setVisible(true); 568 mState = STATE_PIE; 569 mHandler.removeMessages(MSG_FOCUS_TAP); 570 mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_FOCUS_TAP), 571 FOCUS_TAP_TIMEOUT); 572 update(); 573 } 574 } 575 576 public void alignFocus(int x, int y) { 577 mOverlay.removeCallbacks(mDisappear); 578 mAnimation.cancel(); 579 mAnimation.reset(); 580 mFocusX = x; 581 mFocusY = y; 582 mDialAngle = DIAL_HORIZONTAL; 583 setCircle(x, y); 584 mFocused = false; 585 } 586 587 public int getSize() { 588 return 2 * mCircleSize; 589 } 590 591 private int getRandomRange() { 592 return (int)(-60 + 120 * Math.random()); 593 } 594 595 @Override 596 public void layout(int l, int t, int r, int b) { 597 super.layout(l, t, r, b); 598 mCenterX = (r - l) / 2; 599 mCenterY = (b - t) / 2; 600 mFocusX = mCenterX; 601 mFocusY = mCenterY; 602 setCircle(mFocusX, mFocusY); 603 if (isVisible() && mState == STATE_PIE) { 604 setCenter(mCenterX, mCenterY); 605 layoutPie(); 606 } 607 } 608 609 private void setCircle(int cx, int cy) { 610 mCircle.set(cx - mCircleSize, cy - mCircleSize, 611 cx + mCircleSize, cy + mCircleSize); 612 mDial.set(cx - mCircleSize + mInnerOffset, cy - mCircleSize + mInnerOffset, 613 cx + mCircleSize - mInnerOffset, cy + mCircleSize - mInnerOffset); 614 } 615 616 public void drawFocus(Canvas canvas) { 617 if (mBlockFocus) return; 618 mFocusPaint.setStrokeWidth(mOuterStroke); 619 canvas.drawCircle((float) mFocusX, (float) mFocusY, (float) mCircleSize, mFocusPaint); 620 if (mState == STATE_PIE) return; 621 int color = mFocusPaint.getColor(); 622 if (mState == STATE_FINISHING) { 623 mFocusPaint.setColor(mFocused ? mSuccessColor : mFailColor); 624 } 625 mFocusPaint.setStrokeWidth(mInnerStroke); 626 canvas.drawArc(mDial, mDialAngle, 45, false, mFocusPaint); 627 canvas.drawArc(mDial, mDialAngle + 180, 45, false, mFocusPaint); 628 drawLine(canvas, mDialAngle, mFocusPaint); 629 drawLine(canvas, mDialAngle + 45, mFocusPaint); 630 drawLine(canvas, mDialAngle + 180, mFocusPaint); 631 drawLine(canvas, mDialAngle + 225, mFocusPaint); 632 mFocusPaint.setColor(color); 633 } 634 635 private void drawLine(Canvas canvas, int angle, Paint p) { 636 convertCart(angle, mCircleSize - mInnerOffset, mPoint1); 637 convertCart(angle, mCircleSize - mInnerOffset + mInnerOffset / 3, mPoint2); 638 canvas.drawLine(mPoint1.x + mFocusX, mPoint1.y + mFocusY, 639 mPoint2.x + mFocusX, mPoint2.y + mFocusY, p); 640 } 641 642 private static void convertCart(int angle, int radius, Point out) { 643 double a = 2 * Math.PI * (angle % 360) / 360; 644 out.x = (int) (radius * Math.cos(a) + 0.5); 645 out.y = (int) (radius * Math.sin(a) + 0.5); 646 } 647 648 @Override 649 public void showStart() { 650 if (mState == STATE_IDLE) { 651 if (mFocusFromTap) { 652 mHandler.removeMessages(MSG_FOCUS_TAP); 653 mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_FOCUS_TAP), 654 FOCUS_TAP_TIMEOUT); 655 } 656 mStartAnimationAngle = 67; 657 int range = getRandomRange(); 658 startAnimation(SCALING_UP_TIME, 659 false, mStartAnimationAngle, mStartAnimationAngle + range); 660 mState = STATE_FOCUSING; 661 } 662 } 663 664 @Override 665 public void showSuccess(boolean timeout) { 666 if (mState == STATE_FOCUSING) { 667 startAnimation(SCALING_DOWN_TIME, 668 timeout, mStartAnimationAngle); 669 mState = STATE_FINISHING; 670 mFocused = true; 671 } 672 } 673 674 @Override 675 public void showFail(boolean timeout) { 676 if (mState == STATE_FOCUSING) { 677 startAnimation(SCALING_DOWN_TIME, 678 timeout, mStartAnimationAngle); 679 mState = STATE_FINISHING; 680 mFocused = false; 681 } 682 } 683 684 private void cancelFocus() { 685 if (mAnimation != null) { 686 mAnimation.cancel(); 687 } 688 mOverlay.removeCallbacks(mDisappear); 689 mFocused = false; 690 mFocusFromTap = false; 691 } 692 693 @Override 694 public void clear() { 695 if (mState == STATE_PIE) return; 696 mAnimation.cancel(); 697 mFocused = false; 698 mFocusFromTap = false; 699 mOverlay.removeCallbacks(mDisappear); 700 mDisappear.run(); 701 } 702 703 private void startAnimation(long duration, boolean timeout, 704 float toScale) { 705 startAnimation(duration, timeout, mDialAngle, 706 toScale); 707 } 708 709 private void startAnimation(long duration, boolean timeout, 710 float fromScale, float toScale) { 711 setVisible(true); 712 mAnimation.cancel(); 713 mAnimation.reset(); 714 mAnimation.setDuration(duration); 715 mAnimation.setScale(fromScale, toScale); 716 mAnimation.setAnimationListener(timeout ? mEndAction : null); 717 mOverlay.startAnimation(mAnimation); 718 update(); 719 } 720 721 private class EndAction implements Animation.AnimationListener { 722 @Override 723 public void onAnimationEnd(Animation animation) { 724 // Keep the focus indicator for some time. 725 mOverlay.postDelayed(mDisappear, DISAPPEAR_TIMEOUT); 726 } 727 728 @Override 729 public void onAnimationRepeat(Animation animation) { 730 } 731 732 @Override 733 public void onAnimationStart(Animation animation) { 734 } 735 } 736 737 private class Disappear implements Runnable { 738 @Override 739 public void run() { 740 if (mState == STATE_PIE) return; 741 setVisible(false); 742 mFocusX = mCenterX; 743 mFocusY = mCenterY; 744 mState = STATE_IDLE; 745 setCircle(mFocusX, mFocusY); 746 setupPie(mFocusX, mFocusY); 747 mFocused = false; 748 } 749 } 750 751 private class ScaleAnimation extends Animation { 752 private float mFrom = 1f; 753 private float mTo = 1f; 754 755 public ScaleAnimation() { 756 setFillAfter(true); 757 } 758 759 public void setScale(float from, float to) { 760 mFrom = from; 761 mTo = to; 762 } 763 764 @Override 765 protected void applyTransformation(float interpolatedTime, Transformation t) { 766 mDialAngle = (int)(mFrom + (mTo - mFrom) * interpolatedTime); 767 } 768 } 769 770} 771