PieRenderer.java revision 3daa3518c3bfa1a2c1da37c56e1a34e9a0cac259
1/* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.camera.ui; 18 19import android.content.Context; 20import android.content.res.Resources; 21import android.graphics.Canvas; 22import android.graphics.Color; 23import android.graphics.Paint; 24import android.graphics.Path; 25import android.graphics.Point; 26import android.graphics.PointF; 27import android.graphics.RectF; 28import android.os.Handler; 29import android.os.Message; 30import android.util.Log; 31import android.view.MotionEvent; 32import android.view.ViewConfiguration; 33import android.view.animation.Animation; 34import android.view.animation.Animation.AnimationListener; 35import android.view.animation.LinearInterpolator; 36import android.view.animation.Transformation; 37 38import com.android.gallery3d.R; 39 40import java.util.ArrayList; 41import java.util.List; 42 43public class PieRenderer extends OverlayRenderer 44 implements FocusIndicator { 45 46 private static final String TAG = "CAM Pie"; 47 48 // Sometimes continuous autofocus starts and stops several times quickly. 49 // These states are used to make sure the animation is run for at least some 50 // time. 51 private volatile int mState; 52 private ScaleAnimation mAnimation = new ScaleAnimation(); 53 private static final int STATE_IDLE = 0; 54 private static final int STATE_FOCUSING = 1; 55 private static final int STATE_FINISHING = 2; 56 private static final int STATE_PIE = 8; 57 58 private static final float MATH_PI_2 = (float)(Math.PI / 2); 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 // fade out timings 67 private static final int PIE_FADE_OUT_DURATION = 600; 68 69 private static final long PIE_FADE_IN_DURATION = 200; 70 private static final long PIE_XFADE_DURATION = 200; 71 private static final long PIE_SELECT_FADE_DURATION = 300; 72 private static final long PIE_OPEN_SUB_DELAY = 400; 73 74 private static final int MSG_OPEN = 0; 75 private static final int MSG_CLOSE = 1; 76 private static final int MSG_OPENSUBMENU = 2; 77 78 protected static float CENTER = (float) Math.PI / 2; 79 protected static final float SWEEP_SLICE = 0.14f; 80 protected static final float SWEEP_ARC = 0.23f; 81 82 // geometry 83 private int mRadius; 84 private int mRadiusInc; 85 86 // the detection if touch is inside a slice is offset 87 // inbounds by this amount to allow the selection to show before the 88 // finger covers it 89 private int mTouchOffset; 90 91 private List<PieItem> mOpen; 92 93 private Paint mSelectedPaint; 94 private Paint mSubPaint; 95 private Paint mMenuArcPaint; 96 97 // touch handling 98 private PieItem mCurrentItem; 99 100 private Paint mFocusPaint; 101 private int mSuccessColor; 102 private int mFailColor; 103 private int mCircleSize; 104 private int mFocusX; 105 private int mFocusY; 106 private int mCenterX; 107 private int mCenterY; 108 private int mArcCenterY; 109 private int mSliceCenterY; 110 private int mPieCenterX; 111 private int mPieCenterY; 112 private int mSliceRadius; 113 private int mArcRadius; 114 private int mArcOffset; 115 116 private int mDialAngle; 117 private RectF mCircle; 118 private RectF mDial; 119 private Point mPoint1; 120 private Point mPoint2; 121 private int mStartAnimationAngle; 122 private boolean mFocused; 123 private int mInnerOffset; 124 private int mOuterStroke; 125 private int mInnerStroke; 126 private boolean mTapMode; 127 private boolean mBlockFocus; 128 private int mTouchSlopSquared; 129 private Point mDown; 130 private boolean mOpening; 131 private LinearAnimation mXFade; 132 private LinearAnimation mFadeIn; 133 private FadeOutAnimation mFadeOut; 134 private volatile boolean mFocusCancelled; 135 private PointF mPolar = new PointF(); 136 137 138 139 private Handler mHandler = new Handler() { 140 public void handleMessage(Message msg) { 141 switch(msg.what) { 142 case MSG_OPEN: 143 if (mListener != null) { 144 mListener.onPieOpened(mPieCenterX, mPieCenterY); 145 } 146 break; 147 case MSG_CLOSE: 148 if (mListener != null) { 149 mListener.onPieClosed(); 150 } 151 break; 152 case MSG_OPENSUBMENU: 153 onEnterOpen(); 154 break; 155 } 156 157 } 158 }; 159 160 private PieListener mListener; 161 162 static public interface PieListener { 163 public void onPieOpened(int centerX, int centerY); 164 public void onPieClosed(); 165 } 166 167 public void setPieListener(PieListener pl) { 168 mListener = pl; 169 } 170 171 public PieRenderer(Context context) { 172 init(context); 173 } 174 175 private void init(Context ctx) { 176 setVisible(false); 177 mOpen = new ArrayList<PieItem>(); 178 mOpen.add(new PieItem(null, 0)); 179 Resources res = ctx.getResources(); 180 mRadius = (int) res.getDimensionPixelSize(R.dimen.pie_radius_start); 181 mRadiusInc = (int) res.getDimensionPixelSize(R.dimen.pie_radius_increment); 182 mCircleSize = mRadius - res.getDimensionPixelSize(R.dimen.focus_radius_offset); 183 mTouchOffset = (int) res.getDimensionPixelSize(R.dimen.pie_touch_offset); 184 mSelectedPaint = new Paint(); 185 mSelectedPaint.setColor(Color.argb(255, 51, 181, 229)); 186 mSelectedPaint.setAntiAlias(true); 187 mSubPaint = new Paint(); 188 mSubPaint.setAntiAlias(true); 189 mSubPaint.setColor(Color.argb(200, 250, 230, 128)); 190 mFocusPaint = new Paint(); 191 mFocusPaint.setAntiAlias(true); 192 mFocusPaint.setColor(Color.WHITE); 193 mFocusPaint.setStyle(Paint.Style.STROKE); 194 mSuccessColor = Color.GREEN; 195 mFailColor = Color.RED; 196 mCircle = new RectF(); 197 mDial = new RectF(); 198 mPoint1 = new Point(); 199 mPoint2 = new Point(); 200 mInnerOffset = res.getDimensionPixelSize(R.dimen.focus_inner_offset); 201 mOuterStroke = res.getDimensionPixelSize(R.dimen.focus_outer_stroke); 202 mInnerStroke = res.getDimensionPixelSize(R.dimen.focus_inner_stroke); 203 mState = STATE_IDLE; 204 mBlockFocus = false; 205 mTouchSlopSquared = ViewConfiguration.get(ctx).getScaledTouchSlop(); 206 mTouchSlopSquared = mTouchSlopSquared * mTouchSlopSquared; 207 mDown = new Point(); 208 mMenuArcPaint = new Paint(); 209 mMenuArcPaint.setAntiAlias(true); 210 mMenuArcPaint.setColor(Color.argb(140, 255, 255, 255)); 211 mMenuArcPaint.setStrokeWidth(10); 212 mMenuArcPaint.setStyle(Paint.Style.STROKE); 213 mSliceRadius = res.getDimensionPixelSize(R.dimen.pie_item_radius); 214 mArcRadius = res.getDimensionPixelSize(R.dimen.pie_arc_radius); 215 mArcOffset = res.getDimensionPixelSize(R.dimen.pie_arc_offset); 216 } 217 218 private PieItem getRoot() { 219 return mOpen.get(0); 220 } 221 222 public boolean showsItems() { 223 return mTapMode; 224 } 225 226 public void addItem(PieItem item) { 227 // add the item to the pie itself 228 getRoot().addItem(item); 229 } 230 231 public void clearItems() { 232 getRoot().clearItems(); 233 } 234 235 public void showInCenter() { 236 if ((mState == STATE_PIE) && isVisible()) { 237 mTapMode = false; 238 show(false); 239 } else { 240 if (mState != STATE_IDLE) { 241 cancelFocus(); 242 } 243 mState = STATE_PIE; 244 resetPieCenter(); 245 setCenter(mPieCenterX, mPieCenterY); 246 mTapMode = true; 247 show(true); 248 } 249 } 250 251 public void hide() { 252 show(false); 253 } 254 255 /** 256 * guaranteed has center set 257 * @param show 258 */ 259 private void show(boolean show) { 260 if (show) { 261 mState = STATE_PIE; 262 // ensure clean state 263 mCurrentItem = null; 264 PieItem root = getRoot(); 265 for (PieItem openItem : mOpen) { 266 if (openItem.hasItems()) { 267 for (PieItem item : openItem.getItems()) { 268 item.setSelected(false); 269 } 270 } 271 } 272 mOpen.clear(); 273 mOpen.add(root); 274 layoutPie(); 275 fadeIn(); 276 } else { 277 mState = STATE_IDLE; 278 mTapMode = false; 279 if (mXFade != null) { 280 mXFade.cancel(); 281 } 282 } 283 setVisible(show); 284 mHandler.sendEmptyMessage(show ? MSG_OPEN : MSG_CLOSE); 285 } 286 287 private void fadeIn() { 288 mFadeIn = new LinearAnimation(0, 1); 289 mFadeIn.setDuration(PIE_FADE_IN_DURATION); 290 mFadeIn.setAnimationListener(new AnimationListener() { 291 @Override 292 public void onAnimationStart(Animation animation) { 293 } 294 295 @Override 296 public void onAnimationEnd(Animation animation) { 297 mFadeIn = null; 298 } 299 300 @Override 301 public void onAnimationRepeat(Animation animation) { 302 } 303 }); 304 mFadeIn.startNow(); 305 mOverlay.startAnimation(mFadeIn); 306 } 307 308 public void setCenter(int x, int y) { 309 mPieCenterX = x; 310 mPieCenterY = y; 311 mSliceCenterY = y - mArcOffset + mSliceRadius; 312 mArcCenterY = y - mArcOffset + mArcRadius; 313 } 314 315 @Override 316 public void layout(int l, int t, int r, int b) { 317 super.layout(l, t, r, b); 318 mCenterX = (r - l) / 2; 319 mCenterY = (b - t) / 2; 320 321 mFocusX = mCenterX; 322 mFocusY = mCenterY; 323 resetPieCenter(); 324 setCircle(mFocusX, mFocusY); 325 if (isVisible() && mState == STATE_PIE) { 326 setCenter(mPieCenterX, mPieCenterY); 327 layoutPie(); 328 } 329 } 330 331 private void resetPieCenter() { 332 mPieCenterX = mCenterX; 333 mPieCenterY = mCenterY + mCenterY / 3; 334 } 335 336 private void layoutPie() { 337 layoutItems(0, getRoot().getItems()); 338 } 339 340 private void layoutItems(int level, List<PieItem> items) { 341 int extend = 1; 342 Path path = makeSlice(getDegrees(0) + extend, getDegrees(SWEEP_ARC) - extend, 343 mArcRadius, mArcRadius + mRadiusInc + mRadiusInc / 4, 344 mPieCenterX, mArcCenterY - level * mRadiusInc); 345 for (PieItem item : items) { 346 // shared between items 347 item.setPath(path); 348 float angle = getArcCenter(item); 349 int w = item.getIntrinsicWidth(); 350 int h = item.getIntrinsicHeight(); 351 // move views to outer border 352 int r = mArcRadius + mRadiusInc * 2 / 3; 353 int x = (int) (r * Math.cos(angle)); 354 int y = mArcCenterY - (level * mRadiusInc) - (int) (r * Math.sin(angle)) - h / 2; 355 x = mPieCenterX + x - w / 2; 356 item.setBounds(x, y, x + w, y + h); 357 item.setLevel(level); 358 if (item.hasItems()) { 359 layoutItems(level + 1, item.getItems()); 360 } 361 } 362 } 363 364 private Path makeSlice(float start, float end, int inner, int outer, int cx, int cy) { 365 RectF bb = 366 new RectF(cx - outer, cy - outer, cx + outer, 367 cy + outer); 368 RectF bbi = 369 new RectF(cx - inner, cy - inner, cx + inner, 370 cy + inner); 371 Path path = new Path(); 372 path.arcTo(bb, start, end - start, true); 373 path.arcTo(bbi, end, start - end); 374 path.close(); 375 return path; 376 } 377 378 private float getArcCenter(PieItem item) { 379 return getCenter(item.getPosition(), item.getCount(), SWEEP_ARC); 380 } 381 382 private float getSliceCenter(PieItem item) { 383 return getCenter(item.getPosition(), item.getCount(), SWEEP_SLICE); 384 } 385 386 private float getCenter(int pos, int count, float sweep) { 387 return CENTER + (count - 1) * sweep / 2f - pos * sweep; 388 } 389 390 /** 391 * converts a 392 * @param angle from 0..PI to Android degrees (clockwise starting at 3 o'clock) 393 * @return skia angle 394 */ 395 private float getDegrees(double angle) { 396 return (float) (360 - 180 * angle / Math.PI); 397 } 398 399 private void startFadeOut(final PieItem item) { 400 if (mFadeIn != null) { 401 mFadeIn.cancel(); 402 } 403 if (mXFade != null) { 404 mXFade.cancel(); 405 } 406 mFadeOut = new FadeOutAnimation(); 407 mFadeOut.setDuration(PIE_FADE_OUT_DURATION); 408 mFadeOut.setAnimationListener(new AnimationListener() { 409 @Override 410 public void onAnimationStart(Animation animation) { 411 } 412 413 @Override 414 public void onAnimationEnd(Animation animation) { 415 item.performClick(); 416 mFadeOut = null; 417 deselect(); 418 show(false); 419 mOverlay.setAlpha(1); 420 } 421 422 @Override 423 public void onAnimationRepeat(Animation animation) { 424 } 425 }); 426 mFadeOut.startNow(); 427 mOverlay.startAnimation(mFadeOut); 428 } 429 430 // root does not count 431 private boolean hasOpenItem() { 432 return mOpen.size() > 1; 433 } 434 435 // pop an item of the open item stack 436 private PieItem closeOpenItem() { 437 PieItem item = getOpenItem(); 438 mOpen.remove(mOpen.size() -1); 439 return item; 440 } 441 442 private PieItem getOpenItem() { 443 return mOpen.get(mOpen.size() - 1); 444 } 445 446 // return the children either the root or parent of the current open item 447 private PieItem getParent() { 448 return mOpen.get(Math.max(0, mOpen.size() - 2)); 449 } 450 451 private int getLevel() { 452 return mOpen.size() - 1; 453 } 454 455 @Override 456 public void onDraw(Canvas canvas) { 457 float alpha = 1; 458 if (mXFade != null) { 459 alpha = mXFade.getValue(); 460 } else if (mFadeIn != null) { 461 alpha = mFadeIn.getValue(); 462 } else if (mFadeOut != null) { 463 alpha = mFadeOut.getValue(); 464 } 465 int state = canvas.save(); 466 if (mFadeIn != null) { 467 float sf = 0.9f + alpha * 0.1f; 468 canvas.scale(sf, sf, mPieCenterX, mPieCenterY); 469 } 470 if (mState != STATE_PIE) { 471 drawFocus(canvas); 472 } 473 if (mState == STATE_FINISHING) { 474 canvas.restoreToCount(state); 475 return; 476 } 477 if (!hasOpenItem() || (mXFade != null)) { 478 // draw base menu 479 drawArc(canvas, getLevel(), getParent()); 480 for (PieItem item : getParent().getItems()) { 481 drawItem(Math.max(0, mOpen.size() - 2), canvas, item, alpha); 482 } 483 } 484 if (hasOpenItem()) { 485 int level = getLevel(); 486 drawArc(canvas, level, getOpenItem()); 487 for (PieItem inner : getOpenItem().getItems()) { 488 if (mFadeOut != null) { 489 drawItem(level, canvas, inner, alpha); 490 } else { 491 drawItem(level, canvas, inner, (mXFade != null) ? (1 - 0.5f * alpha) : 1); 492 } 493 } 494 } 495 canvas.restoreToCount(state); 496 } 497 498 private void drawArc(Canvas canvas, int level, PieItem item) { 499 // arc 500 if (mState == STATE_PIE) { 501 int min = Integer.MAX_VALUE; 502 int max = Integer.MIN_VALUE; 503 int count = 0; 504 for (PieItem child : item.getItems()) { 505 final int p = child.getPosition(); 506 count = child.getCount(); 507 if (p < min) min = p; 508 if (p > max) max = p; 509 } 510 float start = CENTER + (count - 1) * SWEEP_ARC / 2f - min * SWEEP_ARC + SWEEP_ARC / 2f; 511 float end = CENTER + (count - 1) * SWEEP_ARC / 2f - max * SWEEP_ARC - SWEEP_ARC / 2f; 512 int cy = mArcCenterY - level * mRadiusInc; 513 canvas.drawArc(new RectF(mPieCenterX - mArcRadius, cy - mArcRadius, 514 mPieCenterX + mArcRadius, cy + mArcRadius), 515 getDegrees(end), getDegrees(start) - getDegrees(end), false, mMenuArcPaint); 516 } 517 } 518 519 private void drawItem(int level, Canvas canvas, PieItem item, float alpha) { 520 if (mState == STATE_PIE) { 521 if (item.getPath() != null) { 522 int y = mArcCenterY - level * mRadiusInc; 523 if (item.isSelected()) { 524 Paint p = mSelectedPaint; 525 int state = canvas.save(); 526 float angle = getArcCenter(item) - SWEEP_ARC / 2f; 527 angle = getDegrees(angle); 528 canvas.rotate(angle, mPieCenterX, y); 529 if (mFadeOut != null) { 530 p.setAlpha((int)(255 * alpha)); 531 } 532 canvas.drawPath(item.getPath(), p); 533 if (mFadeOut != null) { 534 p.setAlpha(255); 535 } 536 canvas.restoreToCount(state); 537 } 538 if (mFadeOut == null) { 539 alpha = alpha * (item.isEnabled() ? 1 : 0.3f); 540 // draw the item view 541 item.setAlpha(alpha); 542 } 543 item.draw(canvas); 544 } 545 } 546 } 547 548 @Override 549 public boolean onTouchEvent(MotionEvent evt) { 550 float x = evt.getX(); 551 float y = evt.getY(); 552 int action = evt.getActionMasked(); 553 getPolar(x, y, !mTapMode, mPolar); 554 if (MotionEvent.ACTION_DOWN == action) { 555 mDown.x = (int) evt.getX(); 556 mDown.y = (int) evt.getY(); 557 mOpening = false; 558 if (mTapMode) { 559 PieItem item = findItem(mPolar); 560 if ((item != null) && (mCurrentItem != item)) { 561 mState = STATE_PIE; 562 onEnter(item); 563 } 564 } else { 565 setCenter((int) x, (int) y); 566 show(true); 567 } 568 return true; 569 } else if (MotionEvent.ACTION_UP == action) { 570 if (isVisible()) { 571 PieItem item = mCurrentItem; 572 if (mTapMode) { 573 item = findItem(mPolar); 574 if (mOpening) { 575 mOpening = false; 576 return true; 577 } 578 } 579 if (item == null) { 580 mTapMode = false; 581 show(false); 582 } else if (!mOpening && !item.hasItems()) { 583 startFadeOut(item); 584 mTapMode = false; 585 } else { 586 mTapMode = true; 587 } 588 return true; 589 } 590 } else if (MotionEvent.ACTION_CANCEL == action) { 591 if (isVisible() || mTapMode) { 592 show(false); 593 } 594 deselect(); 595 mHandler.removeMessages(MSG_OPENSUBMENU); 596 return false; 597 } else if (MotionEvent.ACTION_MOVE == action) { 598 if (pulledToCenter(mPolar)) { 599 mHandler.removeMessages(MSG_OPENSUBMENU); 600 if (hasOpenItem()) { 601 if (mCurrentItem != null) { 602 mCurrentItem.setSelected(false); 603 } 604 closeOpenItem(); 605 mCurrentItem = null; 606 } else { 607 deselect(); 608 } 609 return false; 610 } 611 PieItem item = findItem(mPolar); 612 boolean moved = hasMoved(evt); 613 if ((item != null) && (mCurrentItem != item) && (!mOpening || moved)) { 614 mHandler.removeMessages(MSG_OPENSUBMENU); 615 // only select if we didn't just open or have moved past slop 616 if (moved) { 617 // switch back to swipe mode 618 mTapMode = false; 619 } 620 onEnterSelect(item); 621 mHandler.sendEmptyMessageDelayed(MSG_OPENSUBMENU, PIE_OPEN_SUB_DELAY); 622 } 623 } 624 return false; 625 } 626 627 private boolean pulledToCenter(PointF polarCoords) { 628 return polarCoords.y < mArcRadius - mRadiusInc; 629 } 630 631 private boolean inside(PointF polar, PieItem item) { 632 float start = getSliceCenter(item) - SWEEP_SLICE / 2f; 633 boolean res = (mArcRadius < polar.y) 634 && (start < polar.x) 635 && (start + SWEEP_SLICE > polar.x) 636 && (!mTapMode || (mArcRadius + mRadiusInc > polar.y)); 637 return res; 638 } 639 640 private void getPolar(float x, float y, boolean useOffset, PointF res) { 641 // get angle and radius from x/y 642 res.x = (float) Math.PI / 2; 643 x = x - mPieCenterX; 644 float y1 = mSliceCenterY - getLevel() * mRadiusInc - y; 645 float y2 = mArcCenterY - getLevel() * mRadiusInc - y; 646 res.y = (float) Math.sqrt(x * x + y2 * y2); 647 if (x != 0) { 648 res.x = (float) Math.atan2(y1, x); 649 if (res.x < 0) { 650 res.x = (float) (2 * Math.PI + res.x); 651 } 652 } 653 res.y = res.y + (useOffset ? mTouchOffset : 0); 654 } 655 656 private boolean hasMoved(MotionEvent e) { 657 return mTouchSlopSquared < (e.getX() - mDown.x) * (e.getX() - mDown.x) 658 + (e.getY() - mDown.y) * (e.getY() - mDown.y); 659 } 660 661 private void onEnterSelect(PieItem item) { 662 if (mCurrentItem != null) { 663 mCurrentItem.setSelected(false); 664 } 665 if (item != null && item.isEnabled()) { 666 item.setSelected(true); 667 mCurrentItem = item; 668 } else { 669 mCurrentItem = null; 670 } 671 } 672 673 private void onEnterOpen() { 674 if ((mCurrentItem != getOpenItem()) && mCurrentItem.hasItems()) { 675 openCurrentItem(); 676 } 677 } 678 679 /** 680 * enter a slice for a view 681 * updates model only 682 * @param item 683 */ 684 private void onEnter(PieItem item) { 685 if (mCurrentItem != null) { 686 mCurrentItem.setSelected(false); 687 } 688 if (item != null && item.isEnabled()) { 689 item.setSelected(true); 690 mCurrentItem = item; 691 if ((mCurrentItem != getOpenItem()) && mCurrentItem.hasItems()) { 692 openCurrentItem(); 693 } 694 } else { 695 mCurrentItem = null; 696 } 697 } 698 699 private void deselect() { 700 if (mCurrentItem != null) { 701 mCurrentItem.setSelected(false); 702 } 703 if (hasOpenItem()) { 704 PieItem item = closeOpenItem(); 705 onEnter(item); 706 } else { 707 mCurrentItem = null; 708 } 709 } 710 711 private void openCurrentItem() { 712 if ((mCurrentItem != null) && mCurrentItem.hasItems()) { 713 mOpen.add(mCurrentItem); 714 mOpening = true; 715 if (mFadeIn != null) { 716 mFadeIn.cancel(); 717 } 718 mXFade = new LinearAnimation(1, 0); 719 mXFade.setDuration(PIE_XFADE_DURATION); 720 final PieItem ci = mCurrentItem; 721 mXFade.setAnimationListener(new AnimationListener() { 722 @Override 723 public void onAnimationStart(Animation animation) { 724 } 725 726 @Override 727 public void onAnimationEnd(Animation animation) { 728 mXFade = null; 729 ci.setSelected(false); 730 mOpening = false; 731 } 732 733 @Override 734 public void onAnimationRepeat(Animation animation) { 735 } 736 }); 737 mXFade.startNow(); 738 mOverlay.startAnimation(mXFade); 739 } 740 } 741 742 /** 743 * @param polar x: angle, y: dist 744 * @return the item at angle/dist or null 745 */ 746 private PieItem findItem(PointF polar) { 747 // find the matching item: 748 List<PieItem> items = getOpenItem().getItems(); 749 for (PieItem item : items) { 750 if (inside(polar, item)) { 751 return item; 752 } 753 } 754 return null; 755 } 756 757 758 @Override 759 public boolean handlesTouch() { 760 return true; 761 } 762 763 // focus specific code 764 765 public void setBlockFocus(boolean blocked) { 766 mBlockFocus = blocked; 767 if (blocked) { 768 clear(); 769 } 770 } 771 772 public void setFocus(int x, int y) { 773 mFocusX = x; 774 mFocusY = y; 775 setCircle(mFocusX, mFocusY); 776 } 777 778 public void alignFocus(int x, int y) { 779 mOverlay.removeCallbacks(mDisappear); 780 mAnimation.cancel(); 781 mAnimation.reset(); 782 mFocusX = x; 783 mFocusY = y; 784 mDialAngle = DIAL_HORIZONTAL; 785 setCircle(x, y); 786 mFocused = false; 787 } 788 789 public int getSize() { 790 return 2 * mCircleSize; 791 } 792 793 private int getRandomRange() { 794 return (int)(-60 + 120 * Math.random()); 795 } 796 797 private void setCircle(int cx, int cy) { 798 mCircle.set(cx - mCircleSize, cy - mCircleSize, 799 cx + mCircleSize, cy + mCircleSize); 800 mDial.set(cx - mCircleSize + mInnerOffset, cy - mCircleSize + mInnerOffset, 801 cx + mCircleSize - mInnerOffset, cy + mCircleSize - mInnerOffset); 802 } 803 804 public void drawFocus(Canvas canvas) { 805 if (mBlockFocus) return; 806 mFocusPaint.setStrokeWidth(mOuterStroke); 807 canvas.drawCircle((float) mFocusX, (float) mFocusY, (float) mCircleSize, mFocusPaint); 808 if (mState == STATE_PIE) return; 809 int color = mFocusPaint.getColor(); 810 if (mState == STATE_FINISHING) { 811 mFocusPaint.setColor(mFocused ? mSuccessColor : mFailColor); 812 } 813 mFocusPaint.setStrokeWidth(mInnerStroke); 814 drawLine(canvas, mDialAngle, mFocusPaint); 815 drawLine(canvas, mDialAngle + 45, mFocusPaint); 816 drawLine(canvas, mDialAngle + 180, mFocusPaint); 817 drawLine(canvas, mDialAngle + 225, mFocusPaint); 818 canvas.save(); 819 // rotate the arc instead of its offset to better use framework's shape caching 820 canvas.rotate(mDialAngle, mFocusX, mFocusY); 821 canvas.drawArc(mDial, 0, 45, false, mFocusPaint); 822 canvas.drawArc(mDial, 180, 45, false, mFocusPaint); 823 canvas.restore(); 824 mFocusPaint.setColor(color); 825 } 826 827 private void drawLine(Canvas canvas, int angle, Paint p) { 828 convertCart(angle, mCircleSize - mInnerOffset, mPoint1); 829 convertCart(angle, mCircleSize - mInnerOffset + mInnerOffset / 3, mPoint2); 830 canvas.drawLine(mPoint1.x + mFocusX, mPoint1.y + mFocusY, 831 mPoint2.x + mFocusX, mPoint2.y + mFocusY, p); 832 } 833 834 private static void convertCart(int angle, int radius, Point out) { 835 double a = 2 * Math.PI * (angle % 360) / 360; 836 out.x = (int) (radius * Math.cos(a) + 0.5); 837 out.y = (int) (radius * Math.sin(a) + 0.5); 838 } 839 840 @Override 841 public void showStart() { 842 if (mState == STATE_PIE) return; 843 cancelFocus(); 844 mStartAnimationAngle = 67; 845 int range = getRandomRange(); 846 startAnimation(SCALING_UP_TIME, 847 false, mStartAnimationAngle, mStartAnimationAngle + range); 848 mState = STATE_FOCUSING; 849 } 850 851 @Override 852 public void showSuccess(boolean timeout) { 853 if (mState == STATE_FOCUSING) { 854 startAnimation(SCALING_DOWN_TIME, 855 timeout, mStartAnimationAngle); 856 mState = STATE_FINISHING; 857 mFocused = true; 858 } 859 } 860 861 @Override 862 public void showFail(boolean timeout) { 863 if (mState == STATE_FOCUSING) { 864 startAnimation(SCALING_DOWN_TIME, 865 timeout, mStartAnimationAngle); 866 mState = STATE_FINISHING; 867 mFocused = false; 868 } 869 } 870 871 private void cancelFocus() { 872 mFocusCancelled = true; 873 mOverlay.removeCallbacks(mDisappear); 874 if (mAnimation != null && !mAnimation.hasEnded()) { 875 mAnimation.cancel(); 876 } 877 mFocusCancelled = false; 878 mFocused = false; 879 mState = STATE_IDLE; 880 } 881 882 @Override 883 public void clear() { 884 if (mState == STATE_PIE) return; 885 cancelFocus(); 886 mOverlay.post(mDisappear); 887 } 888 889 private void startAnimation(long duration, boolean timeout, 890 float toScale) { 891 startAnimation(duration, timeout, mDialAngle, 892 toScale); 893 } 894 895 private void startAnimation(long duration, boolean timeout, 896 float fromScale, float toScale) { 897 setVisible(true); 898 mAnimation.reset(); 899 mAnimation.setDuration(duration); 900 mAnimation.setScale(fromScale, toScale); 901 mAnimation.setAnimationListener(timeout ? mEndAction : null); 902 mOverlay.startAnimation(mAnimation); 903 update(); 904 } 905 906 private class EndAction implements Animation.AnimationListener { 907 @Override 908 public void onAnimationEnd(Animation animation) { 909 // Keep the focus indicator for some time. 910 if (!mFocusCancelled) { 911 mOverlay.postDelayed(mDisappear, DISAPPEAR_TIMEOUT); 912 } 913 } 914 915 @Override 916 public void onAnimationRepeat(Animation animation) { 917 } 918 919 @Override 920 public void onAnimationStart(Animation animation) { 921 } 922 } 923 924 private class Disappear implements Runnable { 925 @Override 926 public void run() { 927 if (mState == STATE_PIE) return; 928 setVisible(false); 929 mFocusX = mCenterX; 930 mFocusY = mCenterY; 931 mState = STATE_IDLE; 932 setCircle(mFocusX, mFocusY); 933 mFocused = false; 934 } 935 } 936 937 private class FadeOutAnimation extends Animation { 938 939 private float mAlpha; 940 941 public float getValue() { 942 return mAlpha; 943 } 944 945 @Override 946 protected void applyTransformation(float interpolatedTime, Transformation t) { 947 if (interpolatedTime < 0.2) { 948 mAlpha = 1; 949 } else if (interpolatedTime < 0.3) { 950 mAlpha = 0; 951 } else { 952 mAlpha = 1 - (interpolatedTime - 0.3f) / 0.7f; 953 } 954 } 955 } 956 957 private class ScaleAnimation extends Animation { 958 private float mFrom = 1f; 959 private float mTo = 1f; 960 961 public ScaleAnimation() { 962 setFillAfter(true); 963 } 964 965 public void setScale(float from, float to) { 966 mFrom = from; 967 mTo = to; 968 } 969 970 @Override 971 protected void applyTransformation(float interpolatedTime, Transformation t) { 972 mDialAngle = (int)(mFrom + (mTo - mFrom) * interpolatedTime); 973 } 974 } 975 976 private class LinearAnimation extends Animation { 977 private float mFrom; 978 private float mTo; 979 private float mValue; 980 981 public LinearAnimation(float from, float to) { 982 setFillAfter(true); 983 setInterpolator(new LinearInterpolator()); 984 mFrom = from; 985 mTo = to; 986 } 987 988 public float getValue() { 989 return mValue; 990 } 991 992 @Override 993 protected void applyTransformation(float interpolatedTime, Transformation t) { 994 mValue = (mFrom + (mTo - mFrom) * interpolatedTime); 995 } 996 } 997 998} 999