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