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