PieMenu.java revision ee8ede1146cefb85d0b9e7f1fc796fcc8808629a
1/* 2 * Copyright (C) 2010 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.browser.view; 18 19import android.animation.Animator; 20import android.animation.Animator.AnimatorListener; 21import android.animation.AnimatorListenerAdapter; 22import android.animation.ValueAnimator; 23import android.animation.ValueAnimator.AnimatorUpdateListener; 24import android.content.Context; 25import android.content.res.Resources; 26import android.graphics.Canvas; 27import android.graphics.Paint; 28import android.graphics.Path; 29import android.graphics.Point; 30import android.graphics.PointF; 31import android.graphics.RectF; 32import android.graphics.drawable.Drawable; 33import android.util.AttributeSet; 34import android.view.MotionEvent; 35import android.view.SoundEffectConstants; 36import android.view.View; 37import android.view.ViewGroup; 38import android.widget.FrameLayout; 39 40import com.android.browser.R; 41 42import java.util.ArrayList; 43import java.util.List; 44 45public class PieMenu extends FrameLayout { 46 47 private static final int MAX_LEVELS = 5; 48 private static final long ANIMATION = 80; 49 50 public interface PieController { 51 /** 52 * called before menu opens to customize menu 53 * returns if pie state has been changed 54 */ 55 public boolean onOpen(); 56 public void stopEditingUrl(); 57 58 } 59 60 /** 61 * A view like object that lives off of the pie menu 62 */ 63 public interface PieView { 64 65 public interface OnLayoutListener { 66 public void onLayout(int ax, int ay, boolean left); 67 } 68 69 public void setLayoutListener(OnLayoutListener l); 70 71 public void layout(int anchorX, int anchorY, boolean onleft, float angle, 72 int parentHeight); 73 74 public void draw(Canvas c); 75 76 public boolean onTouchEvent(MotionEvent evt); 77 78 } 79 80 private Point mCenter; 81 private int mRadius; 82 private int mRadiusInc; 83 private int mSlop; 84 private int mTouchOffset; 85 private Path mPath; 86 87 private boolean mOpen; 88 private PieController mController; 89 90 private List<PieItem> mItems; 91 private int mLevels; 92 private int[] mCounts; 93 private PieView mPieView = null; 94 95 // sub menus 96 private List<PieItem> mCurrentItems; 97 private PieItem mOpenItem; 98 99 private Drawable mBackground; 100 private Paint mNormalPaint; 101 private Paint mSelectedPaint; 102 private Paint mSubPaint; 103 104 // touch handling 105 private PieItem mCurrentItem; 106 107 private boolean mUseBackground; 108 private boolean mAnimating; 109 110 /** 111 * @param context 112 * @param attrs 113 * @param defStyle 114 */ 115 public PieMenu(Context context, AttributeSet attrs, int defStyle) { 116 super(context, attrs, defStyle); 117 init(context); 118 } 119 120 /** 121 * @param context 122 * @param attrs 123 */ 124 public PieMenu(Context context, AttributeSet attrs) { 125 super(context, attrs); 126 init(context); 127 } 128 129 /** 130 * @param context 131 */ 132 public PieMenu(Context context) { 133 super(context); 134 init(context); 135 } 136 137 private void init(Context ctx) { 138 mItems = new ArrayList<PieItem>(); 139 mLevels = 0; 140 mCounts = new int[MAX_LEVELS]; 141 Resources res = ctx.getResources(); 142 mRadius = (int) res.getDimension(R.dimen.qc_radius_start); 143 mRadiusInc = (int) res.getDimension(R.dimen.qc_radius_increment); 144 mSlop = (int) res.getDimension(R.dimen.qc_slop); 145 mTouchOffset = (int) res.getDimension(R.dimen.qc_touch_offset); 146 mOpen = false; 147 setWillNotDraw(false); 148 setDrawingCacheEnabled(false); 149 mCenter = new Point(0,0); 150 mBackground = res.getDrawable(R.drawable.qc_background_normal); 151 mNormalPaint = new Paint(); 152 mNormalPaint.setColor(res.getColor(R.color.qc_normal)); 153 mNormalPaint.setAntiAlias(true); 154 mSelectedPaint = new Paint(); 155 mSelectedPaint.setColor(res.getColor(R.color.qc_selected)); 156 mSelectedPaint.setAntiAlias(true); 157 mSubPaint = new Paint(); 158 mSubPaint.setAntiAlias(true); 159 mSubPaint.setColor(res.getColor(R.color.qc_sub)); 160 } 161 162 public void setController(PieController ctl) { 163 mController = ctl; 164 } 165 166 public void setUseBackground(boolean useBackground) { 167 mUseBackground = useBackground; 168 } 169 170 public void addItem(PieItem item) { 171 // add the item to the pie itself 172 mItems.add(item); 173 int l = item.getLevel(); 174 mLevels = Math.max(mLevels, l); 175 mCounts[l]++; 176 } 177 178 public void removeItem(PieItem item) { 179 mItems.remove(item); 180 } 181 182 public void clearItems() { 183 mItems.clear(); 184 } 185 186 private boolean onTheLeft() { 187 return mCenter.x < mSlop; 188 } 189 190 /** 191 * guaranteed has center set 192 * @param show 193 */ 194 private void show(boolean show) { 195 mOpen = show; 196 if (mOpen) { 197 // ensure clean state 198 mAnimating = false; 199 mCurrentItem = null; 200 mOpenItem = null; 201 mPieView = null; 202 mController.stopEditingUrl(); 203 mCurrentItems = mItems; 204 for (PieItem item : mCurrentItems) { 205 item.setSelected(false); 206 } 207 if (mController != null) { 208 boolean changed = mController.onOpen(); 209 } 210 layoutPie(); 211 animateOpen(); 212 } 213 invalidate(); 214 } 215 216 private void animateOpen() { 217 ValueAnimator anim = ValueAnimator.ofFloat(0, 1); 218 anim.addUpdateListener(new AnimatorUpdateListener() { 219 @Override 220 public void onAnimationUpdate(ValueAnimator animation) { 221 for (PieItem item : mCurrentItems) { 222 item.setAnimationAngle((1 - animation.getAnimatedFraction()) * (- item.getStart())); 223 } 224 invalidate(); 225 } 226 227 }); 228 anim.setDuration(2*ANIMATION); 229 anim.start(); 230 } 231 232 private void setCenter(int x, int y) { 233 if (x < mSlop) { 234 mCenter.x = 0; 235 } else { 236 mCenter.x = getWidth(); 237 } 238 mCenter.y = y; 239 } 240 241 private void layoutPie() { 242 float emptyangle = (float) Math.PI / 16; 243 int rgap = 2; 244 int inner = mRadius + rgap; 245 int outer = mRadius + mRadiusInc - rgap; 246 int gap = 1; 247 for (int i = 0; i < mLevels; i++) { 248 int level = i + 1; 249 float sweep = (float) (Math.PI - 2 * emptyangle) / mCounts[level]; 250 float angle = emptyangle + sweep / 2; 251 mPath = makeSlice(getDegrees(0) - gap, getDegrees(sweep) + gap, outer, inner, mCenter); 252 for (PieItem item : mCurrentItems) { 253 if (item.getLevel() == level) { 254 View view = item.getView(); 255 if (view != null) { 256 view.measure(view.getLayoutParams().width, 257 view.getLayoutParams().height); 258 int w = view.getMeasuredWidth(); 259 int h = view.getMeasuredHeight(); 260 int r = inner + (outer - inner) * 2 / 3; 261 int x = (int) (r * Math.sin(angle)); 262 int y = mCenter.y - (int) (r * Math.cos(angle)) - h / 2; 263 if (onTheLeft()) { 264 x = mCenter.x + x - w / 2; 265 } else { 266 x = mCenter.x - x - w / 2; 267 } 268 view.layout(x, y, x + w, y + h); 269 } 270 float itemstart = angle - sweep / 2; 271 item.setGeometry(itemstart, sweep, inner, outer); 272 angle += sweep; 273 } 274 } 275 inner += mRadiusInc; 276 outer += mRadiusInc; 277 } 278 } 279 280 281 /** 282 * converts a 283 * 284 * @param angle from 0..PI to Android degrees (clockwise starting at 3 285 * o'clock) 286 * @return skia angle 287 */ 288 private float getDegrees(double angle) { 289 return (float) (270 - 180 * angle / Math.PI); 290 } 291 292 @Override 293 protected void onDraw(Canvas canvas) { 294 if (mOpen) { 295 int state; 296 if (mUseBackground) { 297 int w = mBackground.getIntrinsicWidth(); 298 int h = mBackground.getIntrinsicHeight(); 299 int left = mCenter.x - w; 300 int top = mCenter.y - h / 2; 301 mBackground.setBounds(left, top, left + w, top + h); 302 state = canvas.save(); 303 if (onTheLeft()) { 304 canvas.scale(-1, 1); 305 } 306 mBackground.draw(canvas); 307 canvas.restoreToCount(state); 308 } 309 // draw base menu 310 PieItem last = mCurrentItem; 311 if (mOpenItem != null) { 312 last = mOpenItem; 313 } 314 for (PieItem item : mCurrentItems) { 315 if (item != last) { 316 drawItem(canvas, item); 317 } 318 } 319 if (last != null) { 320 drawItem(canvas, last); 321 } 322 if (mPieView != null) { 323 mPieView.draw(canvas); 324 } 325 } 326 } 327 328 private void drawItem(Canvas canvas, PieItem item) { 329 if (item.getView() != null) { 330 Paint p = item.isSelected() ? mSelectedPaint : mNormalPaint; 331 if (!mItems.contains(item)) { 332 p = item.isSelected() ? mSelectedPaint : mSubPaint; 333 } 334 int state = canvas.save(); 335 if (onTheLeft()) { 336 canvas.scale(-1, 1); 337 } 338 float r = getDegrees(item.getStartAngle()) - 270; // degrees(0) 339 canvas.rotate(r, mCenter.x, mCenter.y); 340 canvas.drawPath(mPath, p); 341 canvas.restoreToCount(state); 342 // draw the item view 343 View view = item.getView(); 344 state = canvas.save(); 345 canvas.translate(view.getX(), view.getY()); 346 view.draw(canvas); 347 canvas.restoreToCount(state); 348 } 349 } 350 351 private Path makeSlice(float start, float end, int outer, int inner, Point center) { 352 RectF bb = 353 new RectF(center.x - outer, center.y - outer, center.x + outer, 354 center.y + outer); 355 RectF bbi = 356 new RectF(center.x - inner, center.y - inner, center.x + inner, 357 center.y + inner); 358 Path path = new Path(); 359 path.arcTo(bb, start, end - start, true); 360 path.arcTo(bbi, end, start - end); 361 path.close(); 362 return path; 363 } 364 365 // touch handling for pie 366 367 @Override 368 public boolean onTouchEvent(MotionEvent evt) { 369 float x = evt.getX(); 370 float y = evt.getY(); 371 int action = evt.getActionMasked(); 372 if (MotionEvent.ACTION_DOWN == action) { 373 if ((x > getWidth() - mSlop) || (x < mSlop)) { 374 setCenter((int) x, (int) y); 375 show(true); 376 return true; 377 } 378 } else if (MotionEvent.ACTION_UP == action) { 379 if (mOpen) { 380 boolean handled = false; 381 if (mPieView != null) { 382 handled = mPieView.onTouchEvent(evt); 383 } 384 PieItem item = mCurrentItem; 385 if (!mAnimating) { 386 deselect(); 387 } 388 show(false); 389 if (!mAnimating && !handled && (item != null) && (item.getView() != null)) { 390 item.getView().performClick(); 391 } 392 return true; 393 } 394 } else if (MotionEvent.ACTION_CANCEL == action) { 395 if (mOpen) { 396 show(false); 397 } 398 if (!mAnimating) { 399 deselect(); 400 invalidate(); 401 } 402 return false; 403 } else if (MotionEvent.ACTION_MOVE == action) { 404 if (mAnimating) return false; 405 boolean handled = false; 406 PointF polar = getPolar(x, y); 407 int maxr = mRadius + mLevels * mRadiusInc + 50; 408 if (mPieView != null) { 409 handled = mPieView.onTouchEvent(evt); 410 } 411 if (handled) { 412 invalidate(); 413 return false; 414 } 415 if (polar.y < mRadius) { 416 if (mOpenItem != null) { 417 closeSub(); 418 } else if (!mAnimating) { 419 deselect(); 420 invalidate(); 421 } 422 return false; 423 } 424 if (polar.y > maxr) { 425 deselect(); 426 show(false); 427 evt.setAction(MotionEvent.ACTION_DOWN); 428 if (getParent() != null) { 429 ((ViewGroup) getParent()).dispatchTouchEvent(evt); 430 } 431 return false; 432 } 433 PieItem item = findItem(polar); 434 if (item == null) { 435 } else if (mCurrentItem != item) { 436 onEnter(item); 437 if ((item != null) && item.isPieView() && (item.getView() != null)) { 438 int cx = item.getView().getLeft() + (onTheLeft() 439 ? item.getView().getWidth() : 0); 440 int cy = item.getView().getTop(); 441 mPieView = item.getPieView(); 442 layoutPieView(mPieView, cx, cy, 443 (item.getStartAngle() + item.getSweep()) / 2); 444 } 445 invalidate(); 446 } 447 } 448 // always re-dispatch event 449 return false; 450 } 451 452 private void layoutPieView(PieView pv, int x, int y, float angle) { 453 pv.layout(x, y, onTheLeft(), angle, getHeight()); 454 } 455 456 /** 457 * enter a slice for a view 458 * updates model only 459 * @param item 460 */ 461 private void onEnter(PieItem item) { 462 // deselect 463 if (mCurrentItem != null) { 464 mCurrentItem.setSelected(false); 465 } 466 if (item != null) { 467 // clear up stack 468 playSoundEffect(SoundEffectConstants.CLICK); 469 item.setSelected(true); 470 mPieView = null; 471 mCurrentItem = item; 472 if ((mCurrentItem != mOpenItem) && mCurrentItem.hasItems()) { 473 openSub(mCurrentItem); 474 mOpenItem = item; 475 } 476 } else { 477 mCurrentItem = null; 478 } 479 480 } 481 482 private void animateOut(final PieItem fixed, AnimatorListener listener) { 483 if ((mCurrentItems == null) || (fixed == null)) return; 484 final float target = fixed.getStartAngle(); 485 ValueAnimator anim = ValueAnimator.ofFloat(0, 1); 486 anim.addUpdateListener(new AnimatorUpdateListener() { 487 @Override 488 public void onAnimationUpdate(ValueAnimator animation) { 489 for (PieItem item : mCurrentItems) { 490 if (item != fixed) { 491 item.setAnimationAngle(animation.getAnimatedFraction() 492 * (target - item.getStart())); 493 } 494 } 495 invalidate(); 496 } 497 }); 498 anim.setDuration(ANIMATION); 499 anim.addListener(listener); 500 anim.start(); 501 } 502 503 private void animateIn(final PieItem fixed, AnimatorListener listener) { 504 if ((mCurrentItems == null) || (fixed == null)) return; 505 final float target = fixed.getStartAngle(); 506 ValueAnimator anim = ValueAnimator.ofFloat(0, 1); 507 anim.addUpdateListener(new AnimatorUpdateListener() { 508 @Override 509 public void onAnimationUpdate(ValueAnimator animation) { 510 for (PieItem item : mCurrentItems) { 511 if (item != fixed) { 512 item.setAnimationAngle((1 - animation.getAnimatedFraction()) 513 * (target - item.getStart())); 514 } 515 } 516 invalidate(); 517 518 } 519 520 }); 521 anim.setDuration(ANIMATION); 522 anim.addListener(listener); 523 anim.start(); 524 } 525 526 private void openSub(final PieItem item) { 527 mAnimating = true; 528 animateOut(item, new AnimatorListenerAdapter() { 529 public void onAnimationEnd(Animator a) { 530 for (PieItem item : mCurrentItems) { 531 item.setAnimationAngle(0); 532 } 533 mCurrentItems = new ArrayList<PieItem>(mItems.size()); 534 int i = 0, j = 0; 535 while (i < mItems.size()) { 536 if (mItems.get(i) == item) { 537 mCurrentItems.add(item); 538 } else { 539 mCurrentItems.add(item.getItems().get(j++)); 540 } 541 i++; 542 } 543 layoutPie(); 544 animateIn(item, new AnimatorListenerAdapter() { 545 public void onAnimationEnd(Animator a) { 546 for (PieItem item : mCurrentItems) { 547 item.setAnimationAngle(0); 548 } 549 mAnimating = false; 550 } 551 }); 552 } 553 }); 554 } 555 556 private void closeSub() { 557 mAnimating = true; 558 if (mCurrentItem != null) { 559 mCurrentItem.setSelected(false); 560 } 561 animateOut(mOpenItem, new AnimatorListenerAdapter() { 562 public void onAnimationEnd(Animator a) { 563 for (PieItem item : mCurrentItems) { 564 item.setAnimationAngle(0); 565 } 566 mCurrentItems = mItems; 567 mPieView = null; 568 animateIn(mOpenItem, new AnimatorListenerAdapter() { 569 public void onAnimationEnd(Animator a) { 570 for (PieItem item : mCurrentItems) { 571 item.setAnimationAngle(0); 572 } 573 mAnimating = false; 574 mOpenItem = null; 575 mCurrentItem = null; 576 } 577 }); 578 } 579 }); 580 } 581 582 private void deselect() { 583 if (mCurrentItem != null) { 584 mCurrentItem.setSelected(false); 585 } 586 if (mOpenItem != null) { 587 mOpenItem = null; 588 mCurrentItems = mItems; 589 } 590 mCurrentItem = null; 591 mPieView = null; 592 } 593 594 private PointF getPolar(float x, float y) { 595 PointF res = new PointF(); 596 // get angle and radius from x/y 597 res.x = (float) Math.PI / 2; 598 x = mCenter.x - x; 599 if (mCenter.x < mSlop) { 600 x = -x; 601 } 602 y = mCenter.y - y; 603 res.y = (float) Math.sqrt(x * x + y * y); 604 if (y > 0) { 605 res.x = (float) Math.asin(x / res.y); 606 } else if (y < 0) { 607 res.x = (float) (Math.PI - Math.asin(x / res.y )); 608 } 609 return res; 610 } 611 612 /** 613 * 614 * @param polar x: angle, y: dist 615 * @return the item at angle/dist or null 616 */ 617 private PieItem findItem(PointF polar) { 618 // find the matching item: 619 for (PieItem item : mCurrentItems) { 620 if (inside(polar, mTouchOffset, item)) { 621 return item; 622 } 623 } 624 return null; 625 } 626 627 private boolean inside(PointF polar, float offset, PieItem item) { 628 return (item.getInnerRadius() - offset < polar.y) 629 && (item.getOuterRadius() - offset > polar.y) 630 && (item.getStartAngle() < polar.x) 631 && (item.getStartAngle() + item.getSweep() > polar.x); 632 } 633 634} 635