PieRenderer.java revision 8c5c6f27146086393591336b74b7742ba0c6fe7d
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 drawFocus(canvas); 392 if (mState == STATE_FINISHING) return; 393 float alpha = 1; 394 if (mXFade != null) { 395 alpha = mXFade.getValue(); 396 } else if (mFadeIn != null) { 397 alpha = mFadeIn.getValue(); 398 } 399 if ((mOpenItem == null) || (mXFade != null)) { 400 // draw base menu 401 for (PieItem item : mItems) { 402 drawItem(canvas, item, alpha); 403 } 404 } 405 if (mOpenItem != null) { 406 for (PieItem inner : mOpenItem.getItems()) { 407 drawItem(canvas, inner, (mXFade != null) ? (1 - 0.5f * alpha) : 1); 408 } 409 } 410 } 411 412 private void drawItem(Canvas canvas, PieItem item, float alpha) { 413 if (mState == STATE_PIE) { 414 if (item.getPath() != null) { 415 int state = -1; 416 if (item.isSelected()) { 417 Paint p = mSelectedPaint; 418 state = canvas.save(); 419 float r = getDegrees(item.getStartAngle()); 420 canvas.rotate(r, mCenter.x, mCenter.y); 421 canvas.drawPath(item.getPath(), p); 422 canvas.restoreToCount(state); 423 } 424 // draw the item view 425 state = canvas.save(); 426 if (mFadeIn != null) { 427 float sf = 0.9f + alpha * 0.1f; 428 canvas.scale(sf, sf, mCenter.x, mCenter.y); 429 } 430 item.setAlpha(alpha); 431 item.draw(canvas); 432 canvas.restoreToCount(state); 433 } 434 } 435 } 436 437 @Override 438 public boolean onTouchEvent(MotionEvent evt) { 439 float x = evt.getX(); 440 float y = evt.getY(); 441 int action = evt.getActionMasked(); 442 PointF polar = getPolar(x, y, !(mTapMode)); 443 if (MotionEvent.ACTION_DOWN == action) { 444 mDown.x = (int) evt.getX(); 445 mDown.y = (int) evt.getY(); 446 mOpening = false; 447 if (mTapMode) { 448 PieItem item = findItem(polar); 449 if ((item != null) && (mCurrentItem != item)) { 450 mHandler.removeMessages(MSG_FOCUS_TAP); 451 mState = STATE_PIE; 452 onEnter(item); 453 } 454 } else { 455 setCenter((int) x, (int) y); 456 show(true); 457 } 458 return true; 459 } else if (MotionEvent.ACTION_UP == action) { 460 if (isVisible()) { 461 PieItem item = mCurrentItem; 462 if (mTapMode) { 463 item = findItem(polar); 464 if (item != null && mOpening) { 465 mOpening = false; 466 return true; 467 } 468 } 469 if (item == null) { 470 mTapMode = false; 471 show(false); 472 } else if (!mOpening 473 && !item.hasItems()) { 474 item.performClick(); 475 startFadeOut(); 476 mTapMode = false; 477 } 478 return true; 479 } 480 } else if (MotionEvent.ACTION_CANCEL == action) { 481 if (isVisible() || mTapMode) { 482 show(false); 483 } 484 deselect(); 485 return false; 486 } else if (MotionEvent.ACTION_MOVE == action) { 487 if (polar.y < mRadius) { 488 if (mOpenItem != null) { 489 mOpenItem = null; 490 } else { 491 deselect(); 492 } 493 return false; 494 } 495 PieItem item = findItem(polar); 496 boolean moved = hasMoved(evt); 497 if ((item != null) && (mCurrentItem != item) && (!mOpening || moved)) { 498 // only select if we didn't just open or have moved past slop 499 mOpening = false; 500 if (moved) { 501 // switch back to swipe mode 502 mTapMode = false; 503 } 504 onEnter(item); 505 } 506 } 507 return false; 508 } 509 510 private boolean hasMoved(MotionEvent e) { 511 return mTouchSlopSquared < (e.getX() - mDown.x) * (e.getX() - mDown.x) 512 + (e.getY() - mDown.y) * (e.getY() - mDown.y); 513 } 514 515 /** 516 * enter a slice for a view 517 * updates model only 518 * @param item 519 */ 520 private void onEnter(PieItem item) { 521 if (mCurrentItem != null) { 522 mCurrentItem.setSelected(false); 523 } 524 if (item != null && item.isEnabled()) { 525 item.setSelected(true); 526 mCurrentItem = item; 527 if ((mCurrentItem != mOpenItem) && mCurrentItem.hasItems()) { 528 openCurrentItem(); 529 } 530 } else { 531 mCurrentItem = null; 532 } 533 } 534 535 private void deselect() { 536 if (mCurrentItem != null) { 537 mCurrentItem.setSelected(false); 538 } 539 if (mOpenItem != null) { 540 mOpenItem = null; 541 } 542 mCurrentItem = null; 543 } 544 545 private void openCurrentItem() { 546 if ((mCurrentItem != null) && mCurrentItem.hasItems()) { 547 mCurrentItem.setSelected(false); 548 mOpenItem = mCurrentItem; 549 mOpening = true; 550 mXFade = new LinearAnimation(1, 0); 551 mXFade.setDuration(PIE_XFADE_DURATION); 552 mXFade.setAnimationListener(new AnimationListener() { 553 @Override 554 public void onAnimationStart(Animation animation) { 555 } 556 557 @Override 558 public void onAnimationEnd(Animation animation) { 559 mXFade = null; 560 } 561 562 @Override 563 public void onAnimationRepeat(Animation animation) { 564 } 565 }); 566 mXFade.startNow(); 567 mOverlay.startAnimation(mXFade); 568 } 569 } 570 571 private PointF getPolar(float x, float y, boolean useOffset) { 572 PointF res = new PointF(); 573 // get angle and radius from x/y 574 res.x = (float) Math.PI / 2; 575 x = x - mCenter.x; 576 y = mCenter.y - y; 577 res.y = (float) Math.sqrt(x * x + y * y); 578 if (x != 0) { 579 res.x = (float) Math.atan2(y, x); 580 if (res.x < 0) { 581 res.x = (float) (2 * Math.PI + res.x); 582 } 583 } 584 res.y = res.y + (useOffset ? mTouchOffset : 0); 585 return res; 586 } 587 588 /** 589 * @param polar x: angle, y: dist 590 * @return the item at angle/dist or null 591 */ 592 private PieItem findItem(PointF polar) { 593 // find the matching item: 594 List<PieItem> items = (mOpenItem != null) ? mOpenItem.getItems() : mItems; 595 for (PieItem item : items) { 596 if (inside(polar, item)) { 597 return item; 598 } 599 } 600 return null; 601 } 602 603 private boolean inside(PointF polar, PieItem item) { 604 return (item.getInnerRadius() < polar.y) 605 && (item.getStartAngle() < polar.x) 606 && (item.getStartAngle() + item.getSweep() > polar.x) 607 && (!mTapMode || (item.getOuterRadius() > polar.y)); 608 } 609 610 @Override 611 public boolean handlesTouch() { 612 return true; 613 } 614 615 // focus specific code 616 617 public void setBlockFocus(boolean blocked) { 618 mBlockFocus = blocked; 619 if (blocked) { 620 clear(); 621 } 622 } 623 624 public void setFocus(int x, int y, boolean startImmediately) { 625 mFocusFromTap = true; 626 mTapMode = true; 627 mFocusX = x; 628 mFocusY = y; 629 setCircle(mFocusX, mFocusY); 630 setupPie(mFocusX, mFocusY); 631 if (startImmediately) { 632 // cameras that don't support focus still need to show menu 633 setVisible(true); 634 mState = STATE_PIE; 635 mHandler.removeMessages(MSG_FOCUS_TAP); 636 mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_FOCUS_TAP), 637 FOCUS_TAP_TIMEOUT); 638 update(); 639 } 640 } 641 642 public void alignFocus(int x, int y) { 643 mOverlay.removeCallbacks(mDisappear); 644 mAnimation.cancel(); 645 mAnimation.reset(); 646 mFocusX = x; 647 mFocusY = y; 648 mDialAngle = DIAL_HORIZONTAL; 649 setCircle(x, y); 650 mFocused = false; 651 } 652 653 public int getSize() { 654 return 2 * mCircleSize; 655 } 656 657 private int getRandomRange() { 658 return (int)(-60 + 120 * Math.random()); 659 } 660 661 @Override 662 public void layout(int l, int t, int r, int b) { 663 super.layout(l, t, r, b); 664 mCenterX = (r - l) / 2; 665 mCenterY = (b - t) / 2; 666 mFocusX = mCenterX; 667 mFocusY = mCenterY; 668 setCircle(mFocusX, mFocusY); 669 if (isVisible() && mState == STATE_PIE) { 670 setCenter(mCenterX, mCenterY); 671 layoutPie(); 672 } 673 } 674 675 private void setCircle(int cx, int cy) { 676 mCircle.set(cx - mCircleSize, cy - mCircleSize, 677 cx + mCircleSize, cy + mCircleSize); 678 mDial.set(cx - mCircleSize + mInnerOffset, cy - mCircleSize + mInnerOffset, 679 cx + mCircleSize - mInnerOffset, cy + mCircleSize - mInnerOffset); 680 } 681 682 public void drawFocus(Canvas canvas) { 683 if (mBlockFocus) return; 684 mFocusPaint.setStrokeWidth(mOuterStroke); 685 if (mState == STATE_PIE && mFadeIn != null) { 686 canvas.save(); 687 float sf = 0.9f + mFadeIn.getValue() * 0.1f; 688 canvas.scale(sf, sf, mCenterX, mCenterY); 689 } 690 canvas.drawCircle((float) mFocusX, (float) mFocusY, (float) mCircleSize, mFocusPaint); 691 if (mState == STATE_PIE && mFadeIn != null) { 692 canvas.restore(); 693 } 694 if (mState == STATE_PIE) return; 695 int color = mFocusPaint.getColor(); 696 if (mState == STATE_FINISHING) { 697 mFocusPaint.setColor(mFocused ? mSuccessColor : mFailColor); 698 } 699 mFocusPaint.setStrokeWidth(mInnerStroke); 700 drawLine(canvas, mDialAngle, mFocusPaint); 701 drawLine(canvas, mDialAngle + 45, mFocusPaint); 702 drawLine(canvas, mDialAngle + 180, mFocusPaint); 703 drawLine(canvas, mDialAngle + 225, mFocusPaint); 704 canvas.save(); 705 // rotate the arc instead of its offset to better use framework's shape caching 706 canvas.rotate(mDialAngle, mFocusX, mFocusY); 707 canvas.drawArc(mDial, 0, 45, false, mFocusPaint); 708 canvas.drawArc(mDial, 180, 45, false, mFocusPaint); 709 canvas.restore(); 710 mFocusPaint.setColor(color); 711 } 712 713 private void drawLine(Canvas canvas, int angle, Paint p) { 714 convertCart(angle, mCircleSize - mInnerOffset, mPoint1); 715 convertCart(angle, mCircleSize - mInnerOffset + mInnerOffset / 3, mPoint2); 716 canvas.drawLine(mPoint1.x + mFocusX, mPoint1.y + mFocusY, 717 mPoint2.x + mFocusX, mPoint2.y + mFocusY, p); 718 } 719 720 private static void convertCart(int angle, int radius, Point out) { 721 double a = 2 * Math.PI * (angle % 360) / 360; 722 out.x = (int) (radius * Math.cos(a) + 0.5); 723 out.y = (int) (radius * Math.sin(a) + 0.5); 724 } 725 726 @Override 727 public void showStart() { 728 if (mState == STATE_IDLE) { 729 if (mFocusFromTap) { 730 mHandler.removeMessages(MSG_FOCUS_TAP); 731 mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_FOCUS_TAP), 732 FOCUS_TAP_TIMEOUT); 733 } 734 mStartAnimationAngle = 67; 735 int range = getRandomRange(); 736 startAnimation(SCALING_UP_TIME, 737 false, mStartAnimationAngle, mStartAnimationAngle + range); 738 mState = STATE_FOCUSING; 739 } 740 } 741 742 @Override 743 public void showSuccess(boolean timeout) { 744 if (mState == STATE_FOCUSING) { 745 startAnimation(SCALING_DOWN_TIME, 746 timeout, mStartAnimationAngle); 747 mState = STATE_FINISHING; 748 mFocused = true; 749 } 750 } 751 752 @Override 753 public void showFail(boolean timeout) { 754 if (mState == STATE_FOCUSING) { 755 startAnimation(SCALING_DOWN_TIME, 756 timeout, mStartAnimationAngle); 757 mState = STATE_FINISHING; 758 mFocused = false; 759 } 760 } 761 762 private void cancelFocus() { 763 if (mAnimation != null) { 764 mAnimation.cancel(); 765 } 766 mOverlay.removeCallbacks(mDisappear); 767 mFocused = false; 768 mFocusFromTap = false; 769 } 770 771 @Override 772 public void clear() { 773 if (mState == STATE_PIE) return; 774 mAnimation.cancel(); 775 mFocused = false; 776 mFocusFromTap = false; 777 mOverlay.removeCallbacks(mDisappear); 778 mDisappear.run(); 779 } 780 781 private void startAnimation(long duration, boolean timeout, 782 float toScale) { 783 startAnimation(duration, timeout, mDialAngle, 784 toScale); 785 } 786 787 private void startAnimation(long duration, boolean timeout, 788 float fromScale, float toScale) { 789 setVisible(true); 790 mAnimation.cancel(); 791 mAnimation.reset(); 792 mAnimation.setDuration(duration); 793 mAnimation.setScale(fromScale, toScale); 794 mAnimation.setAnimationListener(timeout ? mEndAction : null); 795 mOverlay.startAnimation(mAnimation); 796 update(); 797 } 798 799 private class EndAction implements Animation.AnimationListener { 800 @Override 801 public void onAnimationEnd(Animation animation) { 802 // Keep the focus indicator for some time. 803 mOverlay.postDelayed(mDisappear, DISAPPEAR_TIMEOUT); 804 } 805 806 @Override 807 public void onAnimationRepeat(Animation animation) { 808 } 809 810 @Override 811 public void onAnimationStart(Animation animation) { 812 } 813 } 814 815 private class Disappear implements Runnable { 816 @Override 817 public void run() { 818 if (mState == STATE_PIE) return; 819 setVisible(false); 820 mFocusX = mCenterX; 821 mFocusY = mCenterY; 822 mState = STATE_IDLE; 823 setCircle(mFocusX, mFocusY); 824 setupPie(mFocusX, mFocusY); 825 mFocused = false; 826 } 827 } 828 829 private class ScaleAnimation extends Animation { 830 private float mFrom = 1f; 831 private float mTo = 1f; 832 833 public ScaleAnimation() { 834 setFillAfter(true); 835 } 836 837 public void setScale(float from, float to) { 838 mFrom = from; 839 mTo = to; 840 } 841 842 @Override 843 protected void applyTransformation(float interpolatedTime, Transformation t) { 844 mDialAngle = (int)(mFrom + (mTo - mFrom) * interpolatedTime); 845 } 846 } 847 848 849 private class LinearAnimation extends Animation { 850 private float mFrom; 851 private float mTo; 852 private float mValue; 853 854 public LinearAnimation(float from, float to) { 855 setFillAfter(true); 856 setInterpolator(new LinearInterpolator()); 857 mFrom = from; 858 mTo = to; 859 } 860 861 public float getValue() { 862 return mValue; 863 } 864 865 @Override 866 protected void applyTransformation(float interpolatedTime, Transformation t) { 867 mValue = (mFrom + (mTo - mFrom) * interpolatedTime); 868 } 869 } 870 871} 872