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