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