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