PieRenderer.java revision 1404be918ef3e5e5150c13c5a89a66b83a816846
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.annotation.TargetApi; 20import android.content.Context; 21import android.content.res.Resources; 22import android.graphics.Canvas; 23import android.graphics.Color; 24import android.graphics.Paint; 25import android.graphics.Path; 26import android.graphics.Point; 27import android.graphics.PointF; 28import android.graphics.RectF; 29import android.os.Handler; 30import android.os.Message; 31import android.util.Log; 32import android.view.MotionEvent; 33import android.view.View; 34import android.view.animation.Animation; 35import android.view.animation.Animation.AnimationListener; 36import android.view.animation.Transformation; 37 38import com.android.camera.R; 39import com.android.gallery3d.common.ApiHelper; 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 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 = 3; 58 59 private Runnable mDisappear = new Disappear(); 60 private Animation.AnimationListener mEndAction = new EndAction(); 61 private static final int SCALING_UP_TIME = 1000; 62 private static final int SCALING_DOWN_TIME = 200; 63 private static final int DISAPPEAR_TIMEOUT = 200; 64 private static final int DIAL_HORIZONTAL = 157; 65 66 private static final long PIE_OPEN_DELAY = 200; 67 68 private static final int MSG_OPEN = 2; 69 private static final int MSG_CLOSE = 3; 70 private static final int MSG_SUBMENU = 4; 71 private static final float PIE_SWEEP = (float)(Math.PI * 2 / 3); 72 // geometry 73 private Point mCenter; 74 private int mRadius; 75 private int mRadiusInc; 76 private int mSlop; 77 78 // the detection if touch is inside a slice is offset 79 // inbounds by this amount to allow the selection to show before the 80 // finger covers it 81 private int mTouchOffset; 82 83 private List<PieItem> mItems; 84 85 private PieItem mOpenItem; 86 87 private Paint mNormalPaint; 88 private Paint mSelectedPaint; 89 private Paint mSubPaint; 90 91 // touch handling 92 private PieItem mCurrentItem; 93 94 private boolean mAnimating; 95 private float mAlpha; 96 97 private Paint mFocusPaint; 98 private Paint mSuccessPaint; 99 private Paint mDotPaint; 100 private int mCircleSize; 101 private int mDotRadius; 102 private int mFocusX; 103 private int mFocusY; 104 private int mCenterX; 105 private int mCenterY; 106 107 private int mDialAngle; 108 private RectF mCircle; 109 private RectF mDial; 110 private Point mPoint1; 111 private Point mPoint2; 112 private int mStartAnimationAngle; 113 private boolean mFocused; 114 private int mInnerOffset; 115 private int mOuterStroke; 116 private int mInnerStroke; 117 private boolean mFirstTime = true; 118 119 120 private Handler mHandler = new Handler() { 121 public void handleMessage(Message msg) { 122 switch(msg.what) { 123 case MSG_OPEN: 124 if (mListener != null && !mAnimating) { 125 mListener.onPieOpened(mCenter.x, mCenter.y); 126 } 127 break; 128 case MSG_CLOSE: 129 if (mListener != null && !mAnimating) { 130 mListener.onPieClosed(); 131 } 132 break; 133 case MSG_SUBMENU: 134 openCurrentItem(); 135 break; 136 } 137 } 138 }; 139 140 private PieListener mListener; 141 142 static public interface PieListener { 143 public void onPieOpened(int centerX, int centerY); 144 public void onPieClosed(); 145 } 146 147 public void setPieListener(PieListener pl) { 148 mListener = pl; 149 } 150 151 public PieRenderer(Context context) { 152 init(context); 153 } 154 private void init(Context ctx) { 155 setVisible(false); 156 mItems = new ArrayList<PieItem>(); 157 Resources res = ctx.getResources(); 158 mRadius = (int) res.getDimensionPixelSize(R.dimen.pie_radius_start); 159 mRadiusInc = (int) res.getDimensionPixelSize(R.dimen.pie_radius_increment); 160 mSlop = (int) res.getDimensionPixelSize(R.dimen.pie_touch_slop); 161 mTouchOffset = (int) res.getDimensionPixelSize(R.dimen.pie_touch_offset); 162 mCenter = new Point(0,0); 163 mNormalPaint = new Paint(); 164 mNormalPaint.setColor(Color.argb(0, 0, 0, 0)); 165 mNormalPaint.setAntiAlias(true); 166 mSelectedPaint = new Paint(); 167 mSelectedPaint.setColor(Color.argb(128, 0, 0, 0)); //res.getColor(R.color.qc_selected)); 168 mSelectedPaint.setAntiAlias(true); 169 mSubPaint = new Paint(); 170 mSubPaint.setAntiAlias(true); 171 mSubPaint.setColor(Color.argb(200, 250, 230, 128)); //res.getColor(R.color.qc_sub)); 172 mFocusPaint = new Paint(); 173 mFocusPaint.setAntiAlias(true); 174 mFocusPaint.setColor(Color.WHITE); 175 mFocusPaint.setStyle(Paint.Style.STROKE); 176 mSuccessPaint = new Paint(mFocusPaint); 177 mSuccessPaint.setColor(Color.GREEN); 178 mDotPaint = new Paint(); 179 mDotPaint.setAntiAlias(true); 180 mDotPaint.setColor(Color.argb(80, 255, 255, 255)); 181 mDotPaint.setStyle(Paint.Style.FILL); 182 mCircle = new RectF(); 183 mDial = new RectF(); 184 mPoint1 = new Point(); 185 mPoint2 = new Point(); 186 mInnerOffset = res.getDimensionPixelSize(R.dimen.focus_inner_offset); 187 mOuterStroke = res.getDimensionPixelSize(R.dimen.focus_outer_stroke); 188 mInnerStroke = res.getDimensionPixelSize(R.dimen.focus_inner_stroke); 189 mDotRadius = res.getDimensionPixelSize(R.dimen.focus_dot_radius); 190 setVisible(false); 191 192 } 193 194 public void addItem(PieItem item) { 195 // add the item to the pie itself 196 mItems.add(item); 197 } 198 199 public void removeItem(PieItem item) { 200 mItems.remove(item); 201 } 202 203 public void clearItems() { 204 mItems.clear(); 205 } 206 207 public void fade() { 208 mFirstTime = false; 209 setCenter(mCenterX, mCenterY); 210 Animation anim = new AlphaAnimation(); 211 anim.setFillAfter(true); 212 anim.setAnimationListener(new AnimationListener() { 213 @Override 214 public void onAnimationStart(Animation animation) { 215 mAnimating = true; 216 update(); 217 } 218 @Override 219 public void onAnimationEnd(Animation animation) { 220 show(false); 221 mAlpha = 0f; 222 mAnimating = false; 223 setViewAlpha(mOverlay, 1); 224 } 225 @Override 226 public void onAnimationRepeat(Animation animation) { 227 } 228 }); 229 anim.reset(); 230 anim.setDuration(1000); 231 show(true); 232 mOverlay.startAnimation(anim); 233 } 234 235 /** 236 * guaranteed has center set 237 * @param show 238 */ 239 private void show(boolean show) { 240 if (show) { 241 mState = STATE_PIE; 242 // ensure clean state 243 mAnimating = false; 244 mCurrentItem = null; 245 mOpenItem = null; 246 for (PieItem item : mItems) { 247 item.setSelected(false); 248 } 249 layoutPie(); 250 } 251 setVisible(show); 252 mHandler.sendEmptyMessage(show ? MSG_OPEN : MSG_CLOSE); 253 } 254 255 public void setCenter(int x, int y) { 256 mCenter.x = x; 257 mCenter.y = y; 258 // when using the pie menu, align the focus ring 259 alignFocus(x, y); 260 } 261 262 private void setupPie(int x, int y) { 263 // when using the focus ring, align pie items 264 mCenter.x = x; 265 mCenter.y = y; 266 mAnimating = false; 267 mCurrentItem = null; 268 mOpenItem = null; 269 for (PieItem item : mItems) { 270 item.setSelected(false); 271 } 272 layoutPie(); 273 } 274 275 private void layoutPie() { 276 int rgap = 2; 277 int inner = mRadius + rgap; 278 int outer = mRadius + mRadiusInc - rgap; 279 int gap = 1; 280 layoutItems(mItems, (float) (Math.PI / 2), inner, outer, gap); 281 } 282 283 private void layoutItems(List<PieItem> items, float centerAngle, int inner, 284 int outer, int gap) { 285 float emptyangle = PIE_SWEEP / 16; 286 float sweep = (float) (PIE_SWEEP - 2 * emptyangle) / items.size(); 287 float angle = centerAngle - PIE_SWEEP / 2 + emptyangle + sweep / 2; 288 // check if we have custom geometry 289 // first item we find triggers custom sweep for all 290 // this allows us to re-use the path 291 for (PieItem item : items) { 292 if (item.getCenter() >= 0) { 293 sweep = item.getSweep(); 294 break; 295 } 296 } 297 Path path = makeSlice(getDegrees(0) - gap, getDegrees(sweep) + gap, 298 outer, inner, mCenter); 299 for (PieItem item : items) { 300 // shared between items 301 item.setPath(path); 302 View view = item.getView(); 303 if (item.getCenter() >= 0) { 304 angle = item.getCenter(); 305 } 306 if (view != null) { 307 view.measure(view.getLayoutParams().width, 308 view.getLayoutParams().height); 309 int w = view.getMeasuredWidth(); 310 int h = view.getMeasuredHeight(); 311 // move views to outer border 312 int r = inner + (outer - inner) * 2 / 3; 313 int x = (int) (r * Math.cos(angle)); 314 int y = mCenter.y - (int) (r * Math.sin(angle)) - h / 2; 315 x = mCenter.x + x - w / 2; 316 view.layout(x, y, x + w, y + h); 317 } 318 float itemstart = angle - sweep / 2; 319 item.setGeometry(itemstart, sweep, inner, outer); 320 if (item.hasItems()) { 321 layoutItems(item.getItems(), angle, inner, 322 outer + mRadiusInc / 2, gap); 323 } 324 angle += sweep; 325 } 326 } 327 328 private Path makeSlice(float start, float end, int outer, int inner, Point center) { 329 outer = inner + (outer - inner) * 2 / 3; 330 RectF bb = 331 new RectF(center.x - outer, center.y - outer, center.x + outer, 332 center.y + outer); 333 RectF bbi = 334 new RectF(center.x - inner, center.y - inner, center.x + inner, 335 center.y + inner); 336 Path path = new Path(); 337 path.arcTo(bb, start, end - start, true); 338 path.arcTo(bbi, end, start - end); 339 path.close(); 340 return path; 341 } 342 343 /** 344 * converts a 345 * @param angle from 0..PI to Android degrees (clockwise starting at 3 o'clock) 346 * @return skia angle 347 */ 348 private float getDegrees(double angle) { 349 return (float) (360 - 180 * angle / Math.PI); 350 } 351 352 @Override 353 public void onDraw(Canvas canvas) { 354 drawFocus(canvas); 355 if (mState == STATE_FINISHING) return; 356 if (mAnimating) { 357 setViewAlpha(mOverlay, mAlpha); 358 } 359 if (mOpenItem == null) { 360 // draw base menu 361 for (PieItem item : mItems) { 362 drawItem(canvas, item); 363 } 364 } else { 365 for (PieItem inner : mOpenItem.getItems()) { 366 drawItem(canvas, inner); 367 } 368 } 369 } 370 371 private void drawItem(Canvas canvas, PieItem item) { 372 if (item.getView() != null) { 373 if (mState == STATE_FOCUSING) { 374 View view = item.getView(); 375 canvas.drawCircle(view.getLeft() + view.getWidth() / 2, 376 view.getTop() + view.getHeight() / 2, 377 mDotRadius, mDotPaint); 378 } else { 379 if (item.getPath() != null) { 380 Paint p = item.isSelected() ? mSelectedPaint : mNormalPaint; 381 int state = canvas.save(); 382 float r = getDegrees(item.getStartAngle()); 383 canvas.rotate(r, mCenter.x, mCenter.y); 384 canvas.drawPath(item.getPath(), p); 385 canvas.restoreToCount(state); 386 // draw the item view 387 View view = item.getView(); 388 state = canvas.save(); 389 canvas.translate(view.getX(), view.getY()); 390 view.draw(canvas); 391 canvas.restoreToCount(state); 392 } 393 } 394 } 395 } 396 397 @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) 398 private void setViewAlpha(View v, float alpha) { 399 if (ApiHelper.HAS_VIEW_TRANSFORM_PROPERTIES) { 400 v.setAlpha(alpha); 401 } 402 } 403 404 // touch handling for pie 405 406 @Override 407 public boolean onTouchEvent(MotionEvent evt) { 408 float x = evt.getX(); 409 float y = evt.getY(); 410 int action = evt.getActionMasked(); 411 if (MotionEvent.ACTION_DOWN == action) { 412 setCenter((int) x, (int) y); 413 show(true); 414 return true; 415 } else if (MotionEvent.ACTION_UP == action) { 416 if (isVisible()) { 417 PieItem item = mCurrentItem; 418 if (!mAnimating) { 419 deselect(); 420 } 421 show(false); 422 if ((item != null) && (item.getView() != null)) { 423 if ((item == mOpenItem) || !mAnimating) { 424 item.getView().performClick(); 425 } 426 } 427 return true; 428 } 429 } else if (MotionEvent.ACTION_CANCEL == action) { 430 if (isVisible()) { 431 show(false); 432 } 433 if (!mAnimating) { 434 deselect(); 435 } 436 return false; 437 } else if (MotionEvent.ACTION_MOVE == action) { 438 if (mAnimating) return false; 439 PointF polar = getPolar(x, y); 440 int maxr = mRadius + mRadiusInc + 50; 441 if (polar.y < mRadius) { 442 if (mOpenItem != null) { 443 mOpenItem = null; 444 } else if (!mAnimating) { 445 deselect(); 446 } 447 return false; 448 } 449 if (polar.y > maxr) { 450 deselect(); 451 show(false); 452 evt.setAction(MotionEvent.ACTION_DOWN); 453 return false; 454 } 455 PieItem item = findItem(polar); 456 if (item == null) { 457 } else if (mCurrentItem != item) { 458 onEnter(item); 459 } 460 } 461 return false; 462 } 463 464 /** 465 * enter a slice for a view 466 * updates model only 467 * @param item 468 */ 469 private void onEnter(PieItem item) { 470 if (mCurrentItem != null) { 471 mCurrentItem.setSelected(false); 472 } 473 if (item != null && item.isEnabled()) { 474 item.setSelected(true); 475 mCurrentItem = item; 476 if ((mCurrentItem != mOpenItem) && mCurrentItem.hasItems()) { 477 mHandler.sendEmptyMessageDelayed(MSG_SUBMENU, PIE_OPEN_DELAY); 478 } 479 } else { 480 mCurrentItem = null; 481 } 482 } 483 484 private void deselect() { 485 if (mCurrentItem != null) { 486 mCurrentItem.setSelected(false); 487 mHandler.removeMessages(MSG_SUBMENU); 488 } 489 if (mOpenItem != null) { 490 mOpenItem = null; 491 } 492 mCurrentItem = null; 493 } 494 495 private void openCurrentItem() { 496 if ((mCurrentItem != null) && mCurrentItem.hasItems()) { 497 mOpenItem = mCurrentItem; 498 } 499 } 500 501 private PointF getPolar(float x, float y) { 502 PointF res = new PointF(); 503 // get angle and radius from x/y 504 res.x = (float) Math.PI / 2; 505 x = x - mCenter.x; 506 y = mCenter.y - y; 507 res.y = (float) Math.sqrt(x * x + y * y); 508 if (x != 0) { 509 res.x = (float) Math.atan2(y, x); 510 if (res.x < 0) { 511 res.x = (float) (2 * Math.PI + res.x); 512 } 513 } 514 res.y = res.y + mTouchOffset; 515 return res; 516 } 517 518 /** 519 * @param polar x: angle, y: dist 520 * @return the item at angle/dist or null 521 */ 522 private PieItem findItem(PointF polar) { 523 // find the matching item: 524 List<PieItem> items = (mOpenItem != null) ? mOpenItem.getItems() : mItems; 525 for (PieItem item : items) { 526 if (inside(polar, item)) { 527 return item; 528 } 529 } 530 return null; 531 } 532 533 private boolean inside(PointF polar, PieItem item) { 534 return (item.getInnerRadius() < polar.y) 535 && (item.getOuterRadius() > polar.y) 536 && (item.getStartAngle() < polar.x) 537 && (item.getStartAngle() + item.getSweep() > polar.x); 538 } 539 540 @Override 541 public boolean handlesTouch() { 542 return true; 543 } 544 545 private class AlphaAnimation extends Animation { 546 @Override 547 protected void applyTransformation(float interpolatedTime, Transformation t) { 548 mAlpha = 1 - interpolatedTime; 549 } 550 } 551 552 // focus specific code 553 554 public void setFocus(int x, int y) { 555 switch(mOverlay.getOrientation()) { 556 case 0: 557 mFocusX = x; 558 mFocusY = y; 559 break; 560 case 180: 561 mFocusX = getWidth() - x; 562 mFocusY = getHeight() - y; 563 break; 564 case 90: 565 mFocusX = getWidth() - y; 566 mFocusY = x; 567 break; 568 case 270: 569 mFocusX = y ; 570 mFocusY = getHeight() - x; 571 break; 572 } 573 setCircle(mFocusX, mFocusY); 574 setupPie(mFocusX, mFocusY); 575 } 576 577 public void alignFocus(int x, int y) { 578 mOverlay.removeCallbacks(mDisappear); 579 mAnimation.cancel(); 580 mAnimation.reset(); 581 mFocusX = x; 582 mFocusY = y; 583 mDialAngle = DIAL_HORIZONTAL; 584 setCircle(x, y); 585 mFocused = false; 586 } 587 588 public int getSize() { 589 return 2 * mCircleSize; 590 } 591 592 private int getRandomAngle() { 593 return (int)(90 * Math.random()); 594 } 595 596 private int getRandomRange() { 597 return (int)(120 * Math.random()); 598 } 599 600 @Override 601 public void layout(int l, int t, int r, int b) { 602 super.layout(l, t, r, b); 603 mCircleSize = Math.min(200, Math.min(getWidth(), getHeight()) / 5); 604 mCenterX = (r - l) / 2; 605 mCenterY = (b - t) / 2; 606 mFocusX = mCenterX; 607 mFocusY = mCenterY; 608 setCircle(mFocusX, mFocusY); 609 if (mFirstTime) { 610 fade(); 611 } 612 } 613 614 private void setCircle(int cx, int cy) { 615 mCircle.set(cx - mCircleSize, cy - mCircleSize, 616 cx + mCircleSize, cy + mCircleSize); 617 mDial.set(cx - mCircleSize + mInnerOffset, cy - mCircleSize + mInnerOffset, 618 cx + mCircleSize - mInnerOffset, cy + mCircleSize - mInnerOffset); 619 } 620 621 public void drawFocus(Canvas canvas) { 622 mFocusPaint.setStrokeWidth(mOuterStroke); 623 canvas.drawCircle((float) mFocusX, (float) mFocusY, (float) mCircleSize, mFocusPaint); 624 Paint inner = (mFocused ? mSuccessPaint : mFocusPaint); 625 inner.setStrokeWidth(mInnerStroke); 626 canvas.drawArc(mDial, mDialAngle, 45, false, inner); 627 canvas.drawArc(mDial, mDialAngle + 180, 45, false, inner); 628 drawLine(canvas, mDialAngle, inner); 629 drawLine(canvas, mDialAngle + 45, inner); 630 drawLine(canvas, mDialAngle + 180, inner); 631 drawLine(canvas, mDialAngle + 225, inner); 632 } 633 634 private void drawLine(Canvas canvas, int angle, Paint p) { 635 convertCart(angle, mCircleSize - mInnerOffset, mPoint1); 636 convertCart(angle, mCircleSize - mInnerOffset + mInnerOffset / 3, mPoint2); 637 canvas.drawLine(mPoint1.x + mFocusX, mPoint1.y + mFocusY, 638 mPoint2.x + mFocusX, mPoint2.y + mFocusY, p); 639 } 640 641 private static void convertCart(int angle, int radius, Point out) { 642 double a = 2 * Math.PI * (angle % 360) / 360; 643 out.x = (int) (radius * Math.cos(a) + 0.5); 644 out.y = (int) (radius * Math.sin(a) + 0.5); 645 } 646 647 @Override 648 public void showStart() { 649 if (mState == STATE_IDLE) { 650 int angle = getRandomAngle(); 651 int range = getRandomRange(); 652 startAnimation(R.drawable.ic_focus_focusing, SCALING_UP_TIME, 653 false, angle, angle + range); 654 mState = STATE_FOCUSING; 655 mStartAnimationAngle = angle; 656 } 657 } 658 659 @Override 660 public void showSuccess(boolean timeout) { 661 if (mState == STATE_FOCUSING) { 662 startAnimation(R.drawable.ic_focus_focused, SCALING_DOWN_TIME, 663 timeout, mStartAnimationAngle); 664 mState = STATE_FINISHING; 665 mFocused = true; 666 } 667 } 668 669 @Override 670 public void showFail(boolean timeout) { 671 if (mState == STATE_FOCUSING) { 672 startAnimation(R.drawable.ic_focus_failed, SCALING_DOWN_TIME, 673 timeout, mStartAnimationAngle); 674 mState = STATE_FINISHING; 675 mFocused = false; 676 } 677 } 678 679 @Override 680 public void clear() { 681 mAnimation.cancel(); 682 mFocused = false; 683 mOverlay.removeCallbacks(mDisappear); 684 mDisappear.run(); 685 } 686 687 private void startAnimation(int resid, long duration, boolean timeout, 688 float toScale) { 689 startAnimation(resid, duration, timeout, mDialAngle, 690 toScale); 691 } 692 693 private void startAnimation(int resid, long duration, boolean timeout, 694 float fromScale, float toScale) { 695 setVisible(true); 696 mAnimation.cancel(); 697 mAnimation.reset(); 698 mAnimation.setDuration(duration); 699 mAnimation.setScale(fromScale, toScale); 700 mAnimation.setAnimationListener(timeout ? mEndAction : null); 701 mOverlay.startAnimation(mAnimation); 702 update(); 703 } 704 705 private class EndAction implements Animation.AnimationListener { 706 @Override 707 public void onAnimationEnd(Animation animation) { 708 // Keep the focus indicator for some time. 709 mOverlay.postDelayed(mDisappear, DISAPPEAR_TIMEOUT); 710 } 711 712 @Override 713 public void onAnimationRepeat(Animation animation) { 714 } 715 716 @Override 717 public void onAnimationStart(Animation animation) { 718 } 719 } 720 721 private class Disappear implements Runnable { 722 @Override 723 public void run() { 724 setVisible(false); 725 mFocusX = mCenterX; 726 mFocusY = mCenterY; 727 mState = STATE_IDLE; 728 setCircle(mFocusX, mFocusY); 729 setupPie(mFocusX, mFocusY); 730 mFocused = false; 731 } 732 } 733 734 private class ScaleAnimation extends Animation { 735 private float mFrom = 1f; 736 private float mTo = 1f; 737 738 public ScaleAnimation() { 739 setFillAfter(true); 740 } 741 742 public void setScale(float from, float to) { 743 mFrom = from; 744 mTo = to; 745 } 746 747 @Override 748 protected void applyTransformation(float interpolatedTime, Transformation t) { 749 mDialAngle = (int)(mFrom + (mTo - mFrom) * interpolatedTime); 750 } 751 } 752 753} 754