PieRenderer.java revision 8042e7045f3e11dd49d40eebe603eb5fe12d711a
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 Paint p = item.isSelected() ? mSelectedPaint : mNormalPaint; 380 int state = canvas.save(); 381 float r = getDegrees(item.getStartAngle()); 382 canvas.rotate(r, mCenter.x, mCenter.y); 383 canvas.drawPath(item.getPath(), p); 384 canvas.restoreToCount(state); 385 // draw the item view 386 View view = item.getView(); 387 state = canvas.save(); 388 canvas.translate(view.getX(), view.getY()); 389 view.draw(canvas); 390 canvas.restoreToCount(state); 391 } 392 } 393 } 394 395 @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) 396 private void setViewAlpha(View v, float alpha) { 397 if (ApiHelper.HAS_VIEW_TRANSFORM_PROPERTIES) { 398 v.setAlpha(alpha); 399 } 400 } 401 402 // touch handling for pie 403 404 @Override 405 public boolean onTouchEvent(MotionEvent evt) { 406 float x = evt.getX(); 407 float y = evt.getY(); 408 int action = evt.getActionMasked(); 409 if (MotionEvent.ACTION_DOWN == action) { 410 setCenter((int) x, (int) y); 411 show(true); 412 return true; 413 } else if (MotionEvent.ACTION_UP == action) { 414 if (isVisible()) { 415 PieItem item = mCurrentItem; 416 if (!mAnimating) { 417 deselect(); 418 } 419 show(false); 420 if ((item != null) && (item.getView() != null)) { 421 if ((item == mOpenItem) || !mAnimating) { 422 item.getView().performClick(); 423 } 424 } 425 return true; 426 } 427 } else if (MotionEvent.ACTION_CANCEL == action) { 428 if (isVisible()) { 429 show(false); 430 } 431 if (!mAnimating) { 432 deselect(); 433 } 434 return false; 435 } else if (MotionEvent.ACTION_MOVE == action) { 436 if (mAnimating) return false; 437 PointF polar = getPolar(x, y); 438 int maxr = mRadius + mRadiusInc + 50; 439 if (polar.y < mRadius) { 440 if (mOpenItem != null) { 441 mOpenItem = null; 442 } else if (!mAnimating) { 443 deselect(); 444 } 445 return false; 446 } 447 if (polar.y > maxr) { 448 deselect(); 449 show(false); 450 evt.setAction(MotionEvent.ACTION_DOWN); 451 return false; 452 } 453 PieItem item = findItem(polar); 454 if (item == null) { 455 } else if (mCurrentItem != item) { 456 onEnter(item); 457 } 458 } 459 return false; 460 } 461 462 /** 463 * enter a slice for a view 464 * updates model only 465 * @param item 466 */ 467 private void onEnter(PieItem item) { 468 if (mCurrentItem != null) { 469 mCurrentItem.setSelected(false); 470 } 471 if (item != null && item.isEnabled()) { 472 item.setSelected(true); 473 mCurrentItem = item; 474 if ((mCurrentItem != mOpenItem) && mCurrentItem.hasItems()) { 475 mHandler.sendEmptyMessageDelayed(MSG_SUBMENU, PIE_OPEN_DELAY); 476 } 477 } else { 478 mCurrentItem = null; 479 } 480 } 481 482 private void deselect() { 483 if (mCurrentItem != null) { 484 mCurrentItem.setSelected(false); 485 mHandler.removeMessages(MSG_SUBMENU); 486 } 487 if (mOpenItem != null) { 488 mOpenItem = null; 489 } 490 mCurrentItem = null; 491 } 492 493 private void openCurrentItem() { 494 if ((mCurrentItem != null) && mCurrentItem.hasItems()) { 495 mOpenItem = mCurrentItem; 496 } 497 } 498 499 private PointF getPolar(float x, float y) { 500 PointF res = new PointF(); 501 // get angle and radius from x/y 502 res.x = (float) Math.PI / 2; 503 x = x - mCenter.x; 504 y = mCenter.y - y; 505 res.y = (float) Math.sqrt(x * x + y * y); 506 if (x != 0) { 507 res.x = (float) Math.atan2(y, x); 508 if (res.x < 0) { 509 res.x = (float) (2 * Math.PI + res.x); 510 } 511 } 512 res.y = res.y + mTouchOffset; 513 return res; 514 } 515 516 /** 517 * @param polar x: angle, y: dist 518 * @return the item at angle/dist or null 519 */ 520 private PieItem findItem(PointF polar) { 521 // find the matching item: 522 List<PieItem> items = (mOpenItem != null) ? mOpenItem.getItems() : mItems; 523 for (PieItem item : items) { 524 if (inside(polar, item)) { 525 return item; 526 } 527 } 528 return null; 529 } 530 531 private boolean inside(PointF polar, PieItem item) { 532 return (item.getInnerRadius() < polar.y) 533 && (item.getOuterRadius() > polar.y) 534 && (item.getStartAngle() < polar.x) 535 && (item.getStartAngle() + item.getSweep() > polar.x); 536 } 537 538 @Override 539 public boolean handlesTouch() { 540 return true; 541 } 542 543 private class AlphaAnimation extends Animation { 544 @Override 545 protected void applyTransformation(float interpolatedTime, Transformation t) { 546 mAlpha = 1 - interpolatedTime; 547 } 548 } 549 550 // focus specific code 551 552 public void setFocus(int x, int y) { 553 switch(mOverlay.getOrientation()) { 554 case 0: 555 mFocusX = x; 556 mFocusY = y; 557 break; 558 case 180: 559 mFocusX = getWidth() - x; 560 mFocusY = getHeight() - y; 561 break; 562 case 90: 563 mFocusX = getWidth() - y; 564 mFocusY = x; 565 break; 566 case 270: 567 mFocusX = y ; 568 mFocusY = getHeight() - x; 569 break; 570 } 571 setCircle(mFocusX, mFocusY); 572 setupPie(mFocusX, mFocusY); 573 } 574 575 public void alignFocus(int x, int y) { 576 mOverlay.removeCallbacks(mDisappear); 577 mAnimation.cancel(); 578 mAnimation.reset(); 579 mFocusX = x; 580 mFocusY = y; 581 mDialAngle = DIAL_HORIZONTAL; 582 setCircle(x, y); 583 mFocused = false; 584 } 585 586 public int getSize() { 587 return 2 * mCircleSize; 588 } 589 590 private int getRandomAngle() { 591 return (int)(90 * Math.random()); 592 } 593 594 private int getRandomRange() { 595 return (int)(120 * Math.random()); 596 } 597 598 @Override 599 public void layout(int l, int t, int r, int b) { 600 super.layout(l, t, r, b); 601 mCircleSize = Math.min(200, Math.min(getWidth(), getHeight()) / 5); 602 mCenterX = (r - l) / 2; 603 mCenterY = (b - t) / 2; 604 mFocusX = mCenterX; 605 mFocusY = mCenterY; 606 setCircle(mFocusX, mFocusY); 607 if (mFirstTime) { 608 fade(); 609 } 610 } 611 612 private void setCircle(int cx, int cy) { 613 mCircle.set(cx - mCircleSize, cy - mCircleSize, 614 cx + mCircleSize, cy + mCircleSize); 615 mDial.set(cx - mCircleSize + mInnerOffset, cy - mCircleSize + mInnerOffset, 616 cx + mCircleSize - mInnerOffset, cy + mCircleSize - mInnerOffset); 617 } 618 619 public void drawFocus(Canvas canvas) { 620 mFocusPaint.setStrokeWidth(mOuterStroke); 621 canvas.drawCircle((float) mFocusX, (float) mFocusY, (float) mCircleSize, mFocusPaint); 622 Paint inner = (mFocused ? mSuccessPaint : mFocusPaint); 623 inner.setStrokeWidth(mInnerStroke); 624 canvas.drawArc(mDial, mDialAngle, 45, false, inner); 625 canvas.drawArc(mDial, mDialAngle + 180, 45, false, inner); 626 drawLine(canvas, mDialAngle, inner); 627 drawLine(canvas, mDialAngle + 45, inner); 628 drawLine(canvas, mDialAngle + 180, inner); 629 drawLine(canvas, mDialAngle + 225, inner); 630 } 631 632 private void drawLine(Canvas canvas, int angle, Paint p) { 633 convertCart(angle, mCircleSize - mInnerOffset, mPoint1); 634 convertCart(angle, mCircleSize - mInnerOffset + mInnerOffset / 3, mPoint2); 635 canvas.drawLine(mPoint1.x + mFocusX, mPoint1.y + mFocusY, 636 mPoint2.x + mFocusX, mPoint2.y + mFocusY, p); 637 } 638 639 private static void convertCart(int angle, int radius, Point out) { 640 double a = 2 * Math.PI * (angle % 360) / 360; 641 out.x = (int) (radius * Math.cos(a) + 0.5); 642 out.y = (int) (radius * Math.sin(a) + 0.5); 643 } 644 645 @Override 646 public void showStart() { 647 if (mState == STATE_IDLE) { 648 int angle = getRandomAngle(); 649 int range = getRandomRange(); 650 startAnimation(R.drawable.ic_focus_focusing, SCALING_UP_TIME, 651 false, angle, angle + range); 652 mState = STATE_FOCUSING; 653 mStartAnimationAngle = angle; 654 } 655 } 656 657 @Override 658 public void showSuccess(boolean timeout) { 659 if (mState == STATE_FOCUSING) { 660 startAnimation(R.drawable.ic_focus_focused, SCALING_DOWN_TIME, 661 timeout, mStartAnimationAngle); 662 mState = STATE_FINISHING; 663 mFocused = true; 664 } 665 } 666 667 @Override 668 public void showFail(boolean timeout) { 669 if (mState == STATE_FOCUSING) { 670 startAnimation(R.drawable.ic_focus_failed, SCALING_DOWN_TIME, 671 timeout, mStartAnimationAngle); 672 mState = STATE_FINISHING; 673 mFocused = false; 674 } 675 } 676 677 @Override 678 public void clear() { 679 mAnimation.cancel(); 680 mFocused = false; 681 mOverlay.removeCallbacks(mDisappear); 682 mDisappear.run(); 683 } 684 685 private void startAnimation(int resid, long duration, boolean timeout, 686 float toScale) { 687 startAnimation(resid, duration, timeout, mDialAngle, 688 toScale); 689 } 690 691 private void startAnimation(int resid, long duration, boolean timeout, 692 float fromScale, float toScale) { 693 setVisible(true); 694 mAnimation.cancel(); 695 mAnimation.reset(); 696 mAnimation.setDuration(duration); 697 mAnimation.setScale(fromScale, toScale); 698 mAnimation.setAnimationListener(timeout ? mEndAction : null); 699 mOverlay.startAnimation(mAnimation); 700 update(); 701 } 702 703 private class EndAction implements Animation.AnimationListener { 704 @Override 705 public void onAnimationEnd(Animation animation) { 706 // Keep the focus indicator for some time. 707 mOverlay.postDelayed(mDisappear, DISAPPEAR_TIMEOUT); 708 } 709 710 @Override 711 public void onAnimationRepeat(Animation animation) { 712 } 713 714 @Override 715 public void onAnimationStart(Animation animation) { 716 } 717 } 718 719 private class Disappear implements Runnable { 720 @Override 721 public void run() { 722 setVisible(false); 723 mFocusX = mCenterX; 724 mFocusY = mCenterY; 725 mState = STATE_IDLE; 726 setCircle(mFocusX, mFocusY); 727 setupPie(mFocusX, mFocusY); 728 mFocused = false; 729 } 730 } 731 732 private class ScaleAnimation extends Animation { 733 private float mFrom = 1f; 734 private float mTo = 1f; 735 736 public ScaleAnimation() { 737 setFillAfter(true); 738 } 739 740 public void setScale(float from, float to) { 741 mFrom = from; 742 mTo = to; 743 } 744 745 @Override 746 protected void applyTransformation(float interpolatedTime, Transformation t) { 747 mDialAngle = (int)(mFrom + (mTo - mFrom) * interpolatedTime); 748 } 749 } 750 751} 752