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