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