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