PieRenderer.java revision ebcf6fed531d00cfb6900a1fb5d83c60aa7f9693
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.ViewConfiguration; 32import android.view.animation.Animation; 33import android.view.animation.Animation.AnimationListener; 34import android.view.animation.LinearInterpolator; 35import android.view.animation.Transformation; 36 37import com.android.gallery3d.R; 38 39import java.util.ArrayList; 40import java.util.List; 41 42public class PieRenderer extends OverlayRenderer 43 implements FocusIndicator { 44 45 private static final String TAG = "CAM Pie"; 46 47 // Sometimes continuous autofocus starts and stops several times quickly. 48 // These states are used to make sure the animation is run for at least some 49 // time. 50 private volatile int mState; 51 private ScaleAnimation mAnimation = new ScaleAnimation(); 52 private static final int STATE_IDLE = 0; 53 private static final int STATE_FOCUSING = 1; 54 private static final int STATE_FINISHING = 2; 55 private static final int STATE_PIE = 8; 56 57 private Runnable mDisappear = new Disappear(); 58 private Animation.AnimationListener mEndAction = new EndAction(); 59 private static final int SCALING_UP_TIME = 600; 60 private static final int SCALING_DOWN_TIME = 100; 61 private static final int DISAPPEAR_TIMEOUT = 200; 62 private static final int DIAL_HORIZONTAL = 157; 63 // fade out timings 64 private static final int PIE_FADE_OUT_DURATION = 600; 65 66 private static final long PIE_FADE_IN_DURATION = 200; 67 private static final long PIE_XFADE_DURATION = 200; 68 private static final long PIE_SELECT_FADE_DURATION = 300; 69 70 private static final int MSG_OPEN = 0; 71 private static final int MSG_CLOSE = 1; 72 private static final float PIE_SWEEP = (float)(Math.PI * 2 / 3); 73 // geometry 74 private Point mCenter; 75 private int mRadius; 76 private int mRadiusInc; 77 78 // the detection if touch is inside a slice is offset 79 // inbounds by this amount to allow the selection to show before the 80 // finger covers it 81 private int mTouchOffset; 82 83 private List<PieItem> mItems; 84 85 private PieItem mOpenItem; 86 87 private Paint mSelectedPaint; 88 private Paint mSubPaint; 89 90 // touch handling 91 private PieItem mCurrentItem; 92 93 private Paint mFocusPaint; 94 private int mSuccessColor; 95 private int mFailColor; 96 private int mCircleSize; 97 private int mFocusX; 98 private int mFocusY; 99 private int mCenterX; 100 private int mCenterY; 101 102 private int mDialAngle; 103 private RectF mCircle; 104 private RectF mDial; 105 private Point mPoint1; 106 private Point mPoint2; 107 private int mStartAnimationAngle; 108 private boolean mFocused; 109 private int mInnerOffset; 110 private int mOuterStroke; 111 private int mInnerStroke; 112 private boolean mTapMode; 113 private boolean mBlockFocus; 114 private int mTouchSlopSquared; 115 private Point mDown; 116 private boolean mOpening; 117 private LinearAnimation mXFade; 118 private LinearAnimation mFadeIn; 119 private FadeOutAnimation mFadeOut; 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(final PieItem item) { 351 if (mFadeIn != null) { 352 mFadeIn.cancel(); 353 } 354 if (mXFade != null) { 355 mXFade.cancel(); 356 } 357 mFadeOut = new FadeOutAnimation(); 358 mFadeOut.setDuration(PIE_FADE_OUT_DURATION); 359 mFadeOut.setAnimationListener(new AnimationListener() { 360 @Override 361 public void onAnimationStart(Animation animation) { 362 } 363 364 @Override 365 public void onAnimationEnd(Animation animation) { 366 item.performClick(); 367 mFadeOut = null; 368 deselect(); 369 show(false); 370 mOverlay.setAlpha(1); 371 } 372 373 @Override 374 public void onAnimationRepeat(Animation animation) { 375 } 376 }); 377 mFadeOut.startNow(); 378 mOverlay.startAnimation(mFadeOut); 379 } 380 381 @Override 382 public void onDraw(Canvas canvas) { 383 float alpha = 1; 384 if (mXFade != null) { 385 alpha = mXFade.getValue(); 386 } else if (mFadeIn != null) { 387 alpha = mFadeIn.getValue(); 388 } else if (mFadeOut != null) { 389 alpha = mFadeOut.getValue(); 390 } 391 int state = canvas.save(); 392 if (mFadeIn != null) { 393 float sf = 0.9f + alpha * 0.1f; 394 canvas.scale(sf, sf, mCenter.x, mCenter.y); 395 } 396 drawFocus(canvas); 397 if (mState == STATE_FINISHING) { 398 canvas.restoreToCount(state); 399 return; 400 } 401 if ((mOpenItem == null) || (mXFade != null)) { 402 // draw base menu 403 for (PieItem item : mItems) { 404 drawItem(canvas, item, alpha); 405 } 406 } 407 if (mOpenItem != null) { 408 for (PieItem inner : mOpenItem.getItems()) { 409 if (mFadeOut != null) { 410 drawItem(canvas, inner, alpha); 411 } else { 412 drawItem(canvas, inner, (mXFade != null) ? (1 - 0.5f * alpha) : 1); 413 } 414 } 415 } 416 canvas.restoreToCount(state); 417 } 418 419 private void drawItem(Canvas canvas, PieItem item, float alpha) { 420 if (mState == STATE_PIE) { 421 if (item.getPath() != null) { 422 if (item.isSelected()) { 423 Paint p = mSelectedPaint; 424 int state = canvas.save(); 425 float r = getDegrees(item.getStartAngle()); 426 canvas.rotate(r, mCenter.x, mCenter.y); 427 if (mFadeOut != null) { 428 p.setAlpha((int)(255 * alpha)); 429 } 430 canvas.drawPath(item.getPath(), p); 431 if (mFadeOut != null) { 432 p.setAlpha(255); 433 } 434 canvas.restoreToCount(state); 435 } 436 if (mFadeOut == null) { 437 alpha = alpha * (item.isEnabled() ? 1 : 0.3f); 438 // draw the item view 439 item.setAlpha(alpha); 440 } 441 item.draw(canvas); 442 } 443 } 444 } 445 446 @Override 447 public boolean onTouchEvent(MotionEvent evt) { 448 float x = evt.getX(); 449 float y = evt.getY(); 450 int action = evt.getActionMasked(); 451 PointF polar = getPolar(x, y, !(mTapMode)); 452 if (MotionEvent.ACTION_DOWN == action) { 453 mDown.x = (int) evt.getX(); 454 mDown.y = (int) evt.getY(); 455 mOpening = false; 456 if (mTapMode) { 457 PieItem item = findItem(polar); 458 if ((item != null) && (mCurrentItem != item)) { 459 mState = STATE_PIE; 460 onEnter(item); 461 } 462 } else { 463 setCenter((int) x, (int) y); 464 show(true); 465 } 466 return true; 467 } else if (MotionEvent.ACTION_UP == action) { 468 if (isVisible()) { 469 PieItem item = mCurrentItem; 470 if (mTapMode) { 471 item = findItem(polar); 472 if (item != null && mOpening) { 473 mOpening = false; 474 return true; 475 } 476 } 477 if (item == null) { 478 mTapMode = false; 479 show(false); 480 } else if (!mOpening 481 && !item.hasItems()) { 482 startFadeOut(item); 483 mTapMode = false; 484 } 485 return true; 486 } 487 } else if (MotionEvent.ACTION_CANCEL == action) { 488 if (isVisible() || mTapMode) { 489 show(false); 490 } 491 deselect(); 492 return false; 493 } else if (MotionEvent.ACTION_MOVE == action) { 494 if (polar.y < mRadius) { 495 if (mOpenItem != null) { 496 mOpenItem = null; 497 } else { 498 deselect(); 499 } 500 return false; 501 } 502 PieItem item = findItem(polar); 503 boolean moved = hasMoved(evt); 504 if ((item != null) && (mCurrentItem != item) && (!mOpening || moved)) { 505 // only select if we didn't just open or have moved past slop 506 mOpening = false; 507 if (moved) { 508 // switch back to swipe mode 509 mTapMode = false; 510 } 511 onEnter(item); 512 } 513 } 514 return false; 515 } 516 517 private boolean hasMoved(MotionEvent e) { 518 return mTouchSlopSquared < (e.getX() - mDown.x) * (e.getX() - mDown.x) 519 + (e.getY() - mDown.y) * (e.getY() - mDown.y); 520 } 521 522 /** 523 * enter a slice for a view 524 * updates model only 525 * @param item 526 */ 527 private void onEnter(PieItem item) { 528 if (mCurrentItem != null) { 529 mCurrentItem.setSelected(false); 530 } 531 if (item != null && item.isEnabled()) { 532 item.setSelected(true); 533 mCurrentItem = item; 534 if ((mCurrentItem != mOpenItem) && mCurrentItem.hasItems()) { 535 openCurrentItem(); 536 } 537 } else { 538 mCurrentItem = null; 539 } 540 } 541 542 private void deselect() { 543 if (mCurrentItem != null) { 544 mCurrentItem.setSelected(false); 545 } 546 if (mOpenItem != null) { 547 mOpenItem = null; 548 } 549 mCurrentItem = null; 550 } 551 552 private void openCurrentItem() { 553 if ((mCurrentItem != null) && mCurrentItem.hasItems()) { 554 mCurrentItem.setSelected(false); 555 mOpenItem = mCurrentItem; 556 mOpening = true; 557 if (mFadeIn != null) { 558 mFadeIn.cancel(); 559 } 560 mXFade = new LinearAnimation(1, 0); 561 mXFade.setDuration(PIE_XFADE_DURATION); 562 mXFade.setAnimationListener(new AnimationListener() { 563 @Override 564 public void onAnimationStart(Animation animation) { 565 } 566 567 @Override 568 public void onAnimationEnd(Animation animation) { 569 mXFade = null; 570 } 571 572 @Override 573 public void onAnimationRepeat(Animation animation) { 574 } 575 }); 576 mXFade.startNow(); 577 mOverlay.startAnimation(mXFade); 578 } 579 } 580 581 private PointF getPolar(float x, float y, boolean useOffset) { 582 PointF res = new PointF(); 583 // get angle and radius from x/y 584 res.x = (float) Math.PI / 2; 585 x = x - mCenter.x; 586 y = mCenter.y - y; 587 res.y = (float) Math.sqrt(x * x + y * y); 588 if (x != 0) { 589 res.x = (float) Math.atan2(y, x); 590 if (res.x < 0) { 591 res.x = (float) (2 * Math.PI + res.x); 592 } 593 } 594 res.y = res.y + (useOffset ? mTouchOffset : 0); 595 return res; 596 } 597 598 /** 599 * @param polar x: angle, y: dist 600 * @return the item at angle/dist or null 601 */ 602 private PieItem findItem(PointF polar) { 603 // find the matching item: 604 List<PieItem> items = (mOpenItem != null) ? mOpenItem.getItems() : mItems; 605 for (PieItem item : items) { 606 if (inside(polar, item)) { 607 return item; 608 } 609 } 610 return null; 611 } 612 613 private boolean inside(PointF polar, PieItem item) { 614 return (item.getInnerRadius() < polar.y) 615 && (item.getStartAngle() < polar.x) 616 && (item.getStartAngle() + item.getSweep() > polar.x) 617 && (!mTapMode || (item.getOuterRadius() > polar.y)); 618 } 619 620 @Override 621 public boolean handlesTouch() { 622 return true; 623 } 624 625 // focus specific code 626 627 public void setBlockFocus(boolean blocked) { 628 mBlockFocus = blocked; 629 if (blocked) { 630 clear(); 631 } 632 } 633 634 public void setFocus(int x, int y) { 635 mFocusX = x; 636 mFocusY = y; 637 setCircle(mFocusX, mFocusY); 638 } 639 640 public void alignFocus(int x, int y) { 641 mOverlay.removeCallbacks(mDisappear); 642 mAnimation.cancel(); 643 mAnimation.reset(); 644 mFocusX = x; 645 mFocusY = y; 646 mDialAngle = DIAL_HORIZONTAL; 647 setCircle(x, y); 648 mFocused = false; 649 } 650 651 public int getSize() { 652 return 2 * mCircleSize; 653 } 654 655 private int getRandomRange() { 656 return (int)(-60 + 120 * Math.random()); 657 } 658 659 @Override 660 public void layout(int l, int t, int r, int b) { 661 super.layout(l, t, r, b); 662 mCenterX = (r - l) / 2; 663 mCenterY = (b - t) / 2; 664 mFocusX = mCenterX; 665 mFocusY = mCenterY; 666 setCircle(mFocusX, mFocusY); 667 if (isVisible() && mState == STATE_PIE) { 668 setCenter(mCenterX, mCenterY); 669 layoutPie(); 670 } 671 } 672 673 private void setCircle(int cx, int cy) { 674 mCircle.set(cx - mCircleSize, cy - mCircleSize, 675 cx + mCircleSize, cy + mCircleSize); 676 mDial.set(cx - mCircleSize + mInnerOffset, cy - mCircleSize + mInnerOffset, 677 cx + mCircleSize - mInnerOffset, cy + mCircleSize - mInnerOffset); 678 } 679 680 public void drawFocus(Canvas canvas) { 681 if (mBlockFocus) return; 682 mFocusPaint.setStrokeWidth(mOuterStroke); 683 canvas.drawCircle((float) mFocusX, (float) mFocusY, (float) mCircleSize, mFocusPaint); 684 if (mState == STATE_PIE) return; 685 int color = mFocusPaint.getColor(); 686 if (mState == STATE_FINISHING) { 687 mFocusPaint.setColor(mFocused ? mSuccessColor : mFailColor); 688 } 689 mFocusPaint.setStrokeWidth(mInnerStroke); 690 drawLine(canvas, mDialAngle, mFocusPaint); 691 drawLine(canvas, mDialAngle + 45, mFocusPaint); 692 drawLine(canvas, mDialAngle + 180, mFocusPaint); 693 drawLine(canvas, mDialAngle + 225, mFocusPaint); 694 canvas.save(); 695 // rotate the arc instead of its offset to better use framework's shape caching 696 canvas.rotate(mDialAngle, mFocusX, mFocusY); 697 canvas.drawArc(mDial, 0, 45, false, mFocusPaint); 698 canvas.drawArc(mDial, 180, 45, false, mFocusPaint); 699 canvas.restore(); 700 mFocusPaint.setColor(color); 701 } 702 703 private void drawLine(Canvas canvas, int angle, Paint p) { 704 convertCart(angle, mCircleSize - mInnerOffset, mPoint1); 705 convertCart(angle, mCircleSize - mInnerOffset + mInnerOffset / 3, mPoint2); 706 canvas.drawLine(mPoint1.x + mFocusX, mPoint1.y + mFocusY, 707 mPoint2.x + mFocusX, mPoint2.y + mFocusY, p); 708 } 709 710 private static void convertCart(int angle, int radius, Point out) { 711 double a = 2 * Math.PI * (angle % 360) / 360; 712 out.x = (int) (radius * Math.cos(a) + 0.5); 713 out.y = (int) (radius * Math.sin(a) + 0.5); 714 } 715 716 @Override 717 public void showStart() { 718 if (mState == STATE_PIE) return; 719 cancelFocus(); 720 mStartAnimationAngle = 67; 721 int range = getRandomRange(); 722 startAnimation(SCALING_UP_TIME, 723 false, mStartAnimationAngle, mStartAnimationAngle + range); 724 mState = STATE_FOCUSING; 725 } 726 727 @Override 728 public void showSuccess(boolean timeout) { 729 if (mState == STATE_FOCUSING) { 730 startAnimation(SCALING_DOWN_TIME, 731 timeout, mStartAnimationAngle); 732 mState = STATE_FINISHING; 733 mFocused = true; 734 } 735 } 736 737 @Override 738 public void showFail(boolean timeout) { 739 if (mState == STATE_FOCUSING) { 740 startAnimation(SCALING_DOWN_TIME, 741 timeout, mStartAnimationAngle); 742 mState = STATE_FINISHING; 743 mFocused = false; 744 } 745 } 746 747 private void cancelFocus() { 748 mFocusCancelled = true; 749 mOverlay.removeCallbacks(mDisappear); 750 if (mAnimation != null && !mAnimation.hasEnded()) { 751 mAnimation.cancel(); 752 } 753 mFocusCancelled = false; 754 mFocused = false; 755 mState = STATE_IDLE; 756 } 757 758 @Override 759 public void clear() { 760 if (mState == STATE_PIE) return; 761 cancelFocus(); 762 mOverlay.post(mDisappear); 763 } 764 765 private void startAnimation(long duration, boolean timeout, 766 float toScale) { 767 startAnimation(duration, timeout, mDialAngle, 768 toScale); 769 } 770 771 private void startAnimation(long duration, boolean timeout, 772 float fromScale, float toScale) { 773 setVisible(true); 774 mAnimation.reset(); 775 mAnimation.setDuration(duration); 776 mAnimation.setScale(fromScale, toScale); 777 mAnimation.setAnimationListener(timeout ? mEndAction : null); 778 mOverlay.startAnimation(mAnimation); 779 update(); 780 } 781 782 private class EndAction implements Animation.AnimationListener { 783 @Override 784 public void onAnimationEnd(Animation animation) { 785 // Keep the focus indicator for some time. 786 if (!mFocusCancelled) { 787 mOverlay.postDelayed(mDisappear, DISAPPEAR_TIMEOUT); 788 } 789 } 790 791 @Override 792 public void onAnimationRepeat(Animation animation) { 793 } 794 795 @Override 796 public void onAnimationStart(Animation animation) { 797 } 798 } 799 800 private class Disappear implements Runnable { 801 @Override 802 public void run() { 803 if (mState == STATE_PIE) return; 804 setVisible(false); 805 mFocusX = mCenterX; 806 mFocusY = mCenterY; 807 mState = STATE_IDLE; 808 setCircle(mFocusX, mFocusY); 809 mFocused = false; 810 } 811 } 812 813 private class FadeOutAnimation extends Animation { 814 815 private float mAlpha; 816 817 public float getValue() { 818 return mAlpha; 819 } 820 821 @Override 822 protected void applyTransformation(float interpolatedTime, Transformation t) { 823 if (interpolatedTime < 0.2) { 824 mAlpha = 1; 825 } else if (interpolatedTime < 0.3) { 826 mAlpha = 0; 827 } else { 828 mAlpha = 1 - (interpolatedTime - 0.3f) / 0.7f; 829 } 830 } 831 } 832 833 private class ScaleAnimation extends Animation { 834 private float mFrom = 1f; 835 private float mTo = 1f; 836 837 public ScaleAnimation() { 838 setFillAfter(true); 839 } 840 841 public void setScale(float from, float to) { 842 mFrom = from; 843 mTo = to; 844 } 845 846 @Override 847 protected void applyTransformation(float interpolatedTime, Transformation t) { 848 mDialAngle = (int)(mFrom + (mTo - mFrom) * interpolatedTime); 849 } 850 } 851 852 853 private class LinearAnimation extends Animation { 854 private float mFrom; 855 private float mTo; 856 private float mValue; 857 858 public LinearAnimation(float from, float to) { 859 setFillAfter(true); 860 setInterpolator(new LinearInterpolator()); 861 mFrom = from; 862 mTo = to; 863 } 864 865 public float getValue() { 866 return mValue; 867 } 868 869 @Override 870 protected void applyTransformation(float interpolatedTime, Transformation t) { 871 mValue = (mFrom + (mTo - mFrom) * interpolatedTime); 872 } 873 } 874 875} 876