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