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