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