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 17 package com.android.browser.view; 18 19 import android.animation.Animator; 20 import android.animation.Animator.AnimatorListener; 21 import android.animation.AnimatorListenerAdapter; 22 import android.animation.ValueAnimator; 23 import android.animation.ValueAnimator.AnimatorUpdateListener; 24 import android.content.Context; 25 import android.content.res.Resources; 26 import android.graphics.Canvas; 27 import android.graphics.Paint; 28 import android.graphics.Path; 29 import android.graphics.Point; 30 import android.graphics.PointF; 31 import android.graphics.RectF; 32 import android.graphics.drawable.Drawable; 33 import android.util.AttributeSet; 34 import android.view.MotionEvent; 35 import android.view.SoundEffectConstants; 36 import android.view.View; 37 import android.view.ViewGroup; 38 import android.widget.FrameLayout; 39 40 import com.android.browser.R; 41 42 import java.util.ArrayList; 43 import java.util.List; 44 45 public 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 (!handled && (item != null) && (item.getView() != null)) { 390 if ((item == mOpenItem) || !mAnimating) { 391 item.getView().performClick(); 392 } 393 } 394 return true; 395 } 396 } else if (MotionEvent.ACTION_CANCEL == action) { 397 if (mOpen) { 398 show(false); 399 } 400 if (!mAnimating) { 401 deselect(); 402 invalidate(); 403 } 404 return false; 405 } else if (MotionEvent.ACTION_MOVE == action) { 406 if (mAnimating) return false; 407 boolean handled = false; 408 PointF polar = getPolar(x, y); 409 int maxr = mRadius + mLevels * mRadiusInc + 50; 410 if (mPieView != null) { 411 handled = mPieView.onTouchEvent(evt); 412 } 413 if (handled) { 414 invalidate(); 415 return false; 416 } 417 if (polar.y < mRadius) { 418 if (mOpenItem != null) { 419 closeSub(); 420 } else if (!mAnimating) { 421 deselect(); 422 invalidate(); 423 } 424 return false; 425 } 426 if (polar.y > maxr) { 427 deselect(); 428 show(false); 429 evt.setAction(MotionEvent.ACTION_DOWN); 430 if (getParent() != null) { 431 ((ViewGroup) getParent()).dispatchTouchEvent(evt); 432 } 433 return false; 434 } 435 PieItem item = findItem(polar); 436 if (item == null) { 437 } else if (mCurrentItem != item) { 438 onEnter(item); 439 if ((item != null) && item.isPieView() && (item.getView() != null)) { 440 int cx = item.getView().getLeft() + (onTheLeft() 441 ? item.getView().getWidth() : 0); 442 int cy = item.getView().getTop(); 443 mPieView = item.getPieView(); 444 layoutPieView(mPieView, cx, cy, 445 (item.getStartAngle() + item.getSweep()) / 2); 446 } 447 invalidate(); 448 } 449 } 450 // always re-dispatch event 451 return false; 452 } 453 454 private void layoutPieView(PieView pv, int x, int y, float angle) { 455 pv.layout(x, y, onTheLeft(), angle, getHeight()); 456 } 457 458 /** 459 * enter a slice for a view 460 * updates model only 461 * @param item 462 */ 463 private void onEnter(PieItem item) { 464 // deselect 465 if (mCurrentItem != null) { 466 mCurrentItem.setSelected(false); 467 } 468 if (item != null) { 469 // clear up stack 470 playSoundEffect(SoundEffectConstants.CLICK); 471 item.setSelected(true); 472 mPieView = null; 473 mCurrentItem = item; 474 if ((mCurrentItem != mOpenItem) && mCurrentItem.hasItems()) { 475 openSub(mCurrentItem); 476 mOpenItem = item; 477 } 478 } else { 479 mCurrentItem = null; 480 } 481 482 } 483 484 private void animateOut(final PieItem fixed, AnimatorListener listener) { 485 if ((mCurrentItems == null) || (fixed == null)) return; 486 final float target = fixed.getStartAngle(); 487 ValueAnimator anim = ValueAnimator.ofFloat(0, 1); 488 anim.addUpdateListener(new AnimatorUpdateListener() { 489 @Override 490 public void onAnimationUpdate(ValueAnimator animation) { 491 for (PieItem item : mCurrentItems) { 492 if (item != fixed) { 493 item.setAnimationAngle(animation.getAnimatedFraction() 494 * (target - item.getStart())); 495 } 496 } 497 invalidate(); 498 } 499 }); 500 anim.setDuration(ANIMATION); 501 anim.addListener(listener); 502 anim.start(); 503 } 504 505 private void animateIn(final PieItem fixed, AnimatorListener listener) { 506 if ((mCurrentItems == null) || (fixed == null)) return; 507 final float target = fixed.getStartAngle(); 508 ValueAnimator anim = ValueAnimator.ofFloat(0, 1); 509 anim.addUpdateListener(new AnimatorUpdateListener() { 510 @Override 511 public void onAnimationUpdate(ValueAnimator animation) { 512 for (PieItem item : mCurrentItems) { 513 if (item != fixed) { 514 item.setAnimationAngle((1 - animation.getAnimatedFraction()) 515 * (target - item.getStart())); 516 } 517 } 518 invalidate(); 519 520 } 521 522 }); 523 anim.setDuration(ANIMATION); 524 anim.addListener(listener); 525 anim.start(); 526 } 527 528 private void openSub(final PieItem item) { 529 mAnimating = true; 530 animateOut(item, new AnimatorListenerAdapter() { 531 public void onAnimationEnd(Animator a) { 532 for (PieItem item : mCurrentItems) { 533 item.setAnimationAngle(0); 534 } 535 mCurrentItems = new ArrayList<PieItem>(mItems.size()); 536 int i = 0, j = 0; 537 while (i < mItems.size()) { 538 if (mItems.get(i) == item) { 539 mCurrentItems.add(item); 540 } else { 541 mCurrentItems.add(item.getItems().get(j++)); 542 } 543 i++; 544 } 545 layoutPie(); 546 animateIn(item, new AnimatorListenerAdapter() { 547 public void onAnimationEnd(Animator a) { 548 for (PieItem item : mCurrentItems) { 549 item.setAnimationAngle(0); 550 } 551 mAnimating = false; 552 } 553 }); 554 } 555 }); 556 } 557 558 private void closeSub() { 559 mAnimating = true; 560 if (mCurrentItem != null) { 561 mCurrentItem.setSelected(false); 562 } 563 animateOut(mOpenItem, new AnimatorListenerAdapter() { 564 public void onAnimationEnd(Animator a) { 565 for (PieItem item : mCurrentItems) { 566 item.setAnimationAngle(0); 567 } 568 mCurrentItems = mItems; 569 mPieView = null; 570 animateIn(mOpenItem, new AnimatorListenerAdapter() { 571 public void onAnimationEnd(Animator a) { 572 for (PieItem item : mCurrentItems) { 573 item.setAnimationAngle(0); 574 } 575 mAnimating = false; 576 mOpenItem = null; 577 mCurrentItem = null; 578 } 579 }); 580 } 581 }); 582 } 583 584 private void deselect() { 585 if (mCurrentItem != null) { 586 mCurrentItem.setSelected(false); 587 } 588 if (mOpenItem != null) { 589 mOpenItem = null; 590 mCurrentItems = mItems; 591 } 592 mCurrentItem = null; 593 mPieView = null; 594 } 595 596 private PointF getPolar(float x, float y) { 597 PointF res = new PointF(); 598 // get angle and radius from x/y 599 res.x = (float) Math.PI / 2; 600 x = mCenter.x - x; 601 if (mCenter.x < mSlop) { 602 x = -x; 603 } 604 y = mCenter.y - y; 605 res.y = (float) Math.sqrt(x * x + y * y); 606 if (y > 0) { 607 res.x = (float) Math.asin(x / res.y); 608 } else if (y < 0) { 609 res.x = (float) (Math.PI - Math.asin(x / res.y )); 610 } 611 return res; 612 } 613 614 /** 615 * 616 * @param polar x: angle, y: dist 617 * @return the item at angle/dist or null 618 */ 619 private PieItem findItem(PointF polar) { 620 // find the matching item: 621 for (PieItem item : mCurrentItems) { 622 if (inside(polar, mTouchOffset, item)) { 623 return item; 624 } 625 } 626 return null; 627 } 628 629 private boolean inside(PointF polar, float offset, PieItem item) { 630 return (item.getInnerRadius() - offset < polar.y) 631 && (item.getOuterRadius() - offset > polar.y) 632 && (item.getStartAngle() < polar.x) 633 && (item.getStartAngle() + item.getSweep() > polar.x); 634 } 635 636 } 637