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