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