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