PieMenu.java revision 2a56ecaf153d788a1acebc54b670347a1a58d693
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 com.android.browser.R; 20 21import android.content.Context; 22import android.content.res.Resources; 23import android.graphics.Bitmap; 24import android.graphics.BitmapShader; 25import android.graphics.Canvas; 26import android.graphics.Matrix; 27import android.graphics.Paint; 28import android.graphics.Path; 29import android.graphics.Point; 30import android.graphics.PointF; 31import android.graphics.Rect; 32import android.graphics.RectF; 33import android.graphics.Shader; 34import android.graphics.drawable.Drawable; 35import android.util.AttributeSet; 36import android.view.MotionEvent; 37import android.view.SoundEffectConstants; 38import android.view.View; 39import android.view.ViewGroup; 40import android.widget.FrameLayout; 41 42import java.util.ArrayList; 43import java.util.HashMap; 44import java.util.List; 45import java.util.Map; 46 47public class PieMenu extends FrameLayout { 48 49 private static final int RADIUS_GAP = 10; 50 51 public interface PieController { 52 /** 53 * called before menu opens to customize menu 54 * returns if pie state has been changed 55 */ 56 public boolean onOpen(); 57 } 58 private Point mCenter; 59 private int mRadius; 60 private int mRadiusInc; 61 private int mSlop; 62 63 private boolean mOpen; 64 private Paint mPaint; 65 private Paint mSelectedPaint; 66 private PieController mController; 67 68 private Map<View, List<View>> mMenu; 69 private List<View> mStack; 70 71 private boolean mDirty; 72 73 private Drawable mActiveDrawable; 74 private Drawable mInactiveDrawable; 75 private final Paint mActiveShaderPaint = new Paint(); 76 private final Paint mInactiveShaderPaint = new Paint(); 77 private final Matrix mActiveMatrix = new Matrix(); 78 private final Matrix mInactiveMatrix = new Matrix(); 79 80 private BitmapShader mActiveShader; 81 private BitmapShader mInactiveShader; 82 83 84 /** 85 * @param context 86 * @param attrs 87 * @param defStyle 88 */ 89 public PieMenu(Context context, AttributeSet attrs, int defStyle) { 90 super(context, attrs, defStyle); 91 init(context); 92 } 93 94 /** 95 * @param context 96 * @param attrs 97 */ 98 public PieMenu(Context context, AttributeSet attrs) { 99 super(context, attrs); 100 init(context); 101 } 102 103 /** 104 * @param context 105 */ 106 public PieMenu(Context context) { 107 super(context); 108 init(context); 109 } 110 111 private void init(Context ctx) { 112 this.setTag(new MenuTag(0)); 113 mStack = new ArrayList<View>(); 114 mStack.add(this); 115 Resources res = ctx.getResources(); 116 mRadius = (int) res.getDimension(R.dimen.qc_radius); 117 mRadiusInc = (int) res.getDimension(R.dimen.qc_radius_inc); 118 mSlop = (int) res.getDimension(R.dimen.qc_slop); 119 mPaint = new Paint(); 120 mPaint.setAntiAlias(true); 121 mPaint.setColor(res.getColor(R.color.qc_slice_normal)); 122 mSelectedPaint = new Paint(); 123 mSelectedPaint.setAntiAlias(true); 124 mSelectedPaint.setColor(res.getColor(R.color.qc_slice_active)); 125 mOpen = false; 126 mMenu = new HashMap<View, List<View>>(); 127 setWillNotDraw(false); 128 setDrawingCacheEnabled(false); 129 mCenter = new Point(0,0); 130 mDirty = true; 131 mActiveShaderPaint.setStyle(Paint.Style.FILL); 132 mActiveShaderPaint.setAntiAlias(true); 133 134 mInactiveShaderPaint.setStyle(Paint.Style.FILL); 135 mInactiveShaderPaint.setAntiAlias(true); 136 mActiveDrawable = res.getDrawable(R.drawable.qc_background_selected); 137 mInactiveDrawable = res.getDrawable(R.drawable.qc_background_normal); 138 139 Bitmap activeTexture = getDrawableAsBitmap(mActiveDrawable, 140 mActiveDrawable.getIntrinsicWidth(), 141 mActiveDrawable.getIntrinsicHeight()); 142 Bitmap inactiveTexture = getDrawableAsBitmap(mInactiveDrawable, 143 mInactiveDrawable.getIntrinsicWidth(), 144 mInactiveDrawable.getIntrinsicHeight()); 145 146 mActiveShader = new BitmapShader(activeTexture, 147 Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); 148 mActiveShaderPaint.setShader(mActiveShader); 149 150 mInactiveShader = new BitmapShader(inactiveTexture, 151 Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); 152 mInactiveShaderPaint.setShader(mInactiveShader); 153 154 } 155 156 private static Bitmap getDrawableAsBitmap(Drawable drawable, int width, int height) { 157 Bitmap b = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 158 Canvas c = new Canvas(b); 159 drawable.setBounds(0, 0, width, height); 160 drawable.draw(c); 161 return b; 162 } 163 164 public void setController(PieController ctl) { 165 mController = ctl; 166 } 167 168 public void setRadius(int r) { 169 mRadius = r; 170 requestLayout(); 171 } 172 173 public void setRadiusIncrement(int ri) { 174 mRadiusInc = ri; 175 requestLayout(); 176 } 177 178 /** 179 * add a menu item to another item as a submenu 180 * @param item 181 * @param parent 182 */ 183 public void addItem(View item, View parent) { 184 List<View> subs = mMenu.get(parent); 185 if (subs == null) { 186 subs = new ArrayList<View>(); 187 mMenu.put(parent, subs); 188 } 189 subs.add(item); 190 MenuTag tag = new MenuTag(((MenuTag) parent.getTag()).level + 1); 191 item.setTag(tag); 192 } 193 194 public void addItem(View view) { 195 // add the item to the pie itself 196 addItem(view, this); 197 } 198 199 public void removeItem(View view) { 200 List<View> subs = mMenu.get(view); 201 mMenu.remove(view); 202 for (View p : mMenu.keySet()) { 203 List<View> sl = mMenu.get(p); 204 if (sl != null) { 205 sl.remove(view); 206 } 207 } 208 } 209 210 public void clearItems(View parent) { 211 List<View> subs = mMenu.remove(parent); 212 if (subs != null) { 213 for (View sub: subs) { 214 clearItems(sub); 215 } 216 } 217 } 218 219 public void clearItems() { 220 mMenu.clear(); 221 } 222 223 224 public void show(boolean show) { 225 mOpen = show; 226 if (mOpen) { 227 if (mController != null) { 228 boolean changed = mController.onOpen(); 229 } 230 mDirty = true; 231 } 232 if (!show) { 233 // hide sub items 234 mStack.clear(); 235 mStack.add(this); 236 } 237 invalidate(); 238 } 239 240 private void setCenter(int x, int y) { 241 if (x < mSlop) { 242 mCenter.x = 0; 243 } else { 244 mCenter.x = getWidth(); 245 } 246 mCenter.y = y; 247 } 248 249 private boolean onTheLeft() { 250 return mCenter.x < mSlop; 251 } 252 253 @Override 254 protected void onDraw(Canvas canvas) { 255 if (mOpen) { 256 int radius = mRadius; 257 // start in the center for 0 level menu 258 float anchor = (float) Math.PI / 2; 259 PointF angles = new PointF(); 260 int state = canvas.save(); 261 if (onTheLeft()) { 262 // left handed 263 canvas.scale(-1, 1); 264 } 265 for (View parent : mStack) { 266 List<View> subs = mMenu.get(parent); 267 if (subs != null) { 268 setGeometry(anchor, subs.size(), angles); 269 } 270 anchor = drawSlices(canvas, subs, radius, angles.x, angles.y); 271 radius += mRadiusInc + RADIUS_GAP; 272 } 273 canvas.restoreToCount(state); 274 mDirty = false; 275 } 276 } 277 278 /** 279 * draw the set of slices 280 * @param canvas 281 * @param items 282 * @param radius 283 * @param start 284 * @param sweep 285 * @return the angle of the selected slice 286 */ 287 private float drawSlices(Canvas canvas, List<View> items, int radius, 288 float start, float sweep) { 289 float angle = start + sweep / 2; 290 // gap between slices in degrees 291 float gap = 1f; 292 float newanchor = 0f; 293 for (View item : items) { 294 if (mDirty) { 295 item.measure(item.getLayoutParams().width, 296 item.getLayoutParams().height); 297 int w = item.getMeasuredWidth(); 298 int h = item.getMeasuredHeight(); 299 int x = (int) (radius * Math.sin(angle)); 300 int y = mCenter.y - (int) (radius * Math.cos(angle)) - h / 2; 301 if (onTheLeft()) { 302 x = mCenter.x + x - w / 2; 303 } else { 304 x = mCenter.x - x - w / 2; 305 } 306 item.layout(x, y, x + w, y + h); 307 } 308 float itemstart = angle - sweep / 2; 309 int inner = radius - mRadiusInc / 2; 310 int outer = radius + mRadiusInc / 2; 311 Path slice = makeSlice(getDegrees(itemstart) - gap, 312 getDegrees(itemstart + sweep) + gap, 313 outer, inner, mCenter); 314 MenuTag tag = (MenuTag) item.getTag(); 315 tag.start = itemstart; 316 tag.sweep = sweep; 317 tag.inner = inner; 318 tag.outer = outer; 319 int state = canvas.save(); 320 int[] topLeft = new int[2]; 321 getLocationInWindow(topLeft); 322 topLeft[0] = mCenter.x - outer; 323 topLeft[1] = mCenter.y - outer; 324 Paint paint = item.isPressed() ? mActiveShaderPaint : mInactiveShaderPaint; 325 drawClipped(canvas, paint, slice, topLeft, item.isPressed()); 326 canvas.restoreToCount(state); 327 state = canvas.save(); 328 if (onTheLeft()) { 329 canvas.scale(-1, 1); 330 } 331 canvas.translate(item.getX(), item.getY()); 332 item.draw(canvas); 333 canvas.restoreToCount(state); 334 if (mStack.contains(item)) { 335 // item is anchor for sub menu 336 newanchor = angle; 337 } 338 angle += sweep; 339 } 340 return newanchor; 341 } 342 343 private void drawClipped(Canvas canvas, Paint paint, Path clipPath, int[] pos, 344 boolean selected) { 345 // TODO: We should change the matrix/shader only when needed 346 final Matrix matrix = selected ? mActiveMatrix : mInactiveMatrix; 347 matrix.setTranslate(pos[0], pos[1]); 348 (selected ? mActiveShader : mInactiveShader).setLocalMatrix(matrix); 349 canvas.drawPath(clipPath, paint); 350 } 351 352 353 /** 354 * converts a 355 * @param angle from 0..PI to Android degrees (clockwise starting at 3 o'clock) 356 * @return skia angle 357 */ 358 private float getDegrees(double angle) { 359 return (float) (270 - 180 * angle / Math.PI); 360 } 361 362 private Path makeSlice(float startangle, float endangle, int outerradius, 363 int innerradius, Point center) { 364 RectF bb = new RectF(center.x - outerradius, center.y - outerradius, 365 center.x + outerradius, center.y + outerradius); 366 RectF bbi = new RectF(center.x - innerradius, center.y - innerradius, 367 center.x + innerradius, center.y + innerradius); 368 Path path = new Path(); 369 path.arcTo(bb, startangle, endangle - startangle, true); 370 path.arcTo(bbi, endangle, startangle - endangle); 371 path.close(); 372 return path; 373 } 374 375 /** 376 * all angles are 0 .. MATH.PI where 0 points up, and rotate counterclockwise 377 * set the startangle and slice sweep in result 378 * @param anchorangle : angle at which the menu is anchored 379 * @param nslices 380 * @param result : x : start, y : sweep 381 */ 382 private void setGeometry(float anchorangle, int nslices, PointF result) { 383 float span = (float) Math.min(anchorangle, Math.PI - anchorangle); 384 float sweep = 2 * span / (nslices + 1); 385 result.x = anchorangle - span + sweep / 2; 386 result.y = sweep; 387 } 388 389 // touch handling for pie 390 391 View mCurrentView; 392 Rect mHitRect = new Rect(); 393 394 @Override 395 public boolean onTouchEvent(MotionEvent evt) { 396 float x = evt.getX(); 397 float y = evt.getY(); 398 int action = evt.getActionMasked(); 399 int edges = evt.getEdgeFlags(); 400 if (MotionEvent.ACTION_DOWN == action) { 401 if ((x > getWidth() - mSlop) || (x < mSlop)) { 402 setCenter((int) x, (int) y); 403 show(true); 404 return true; 405 } 406 } else if (MotionEvent.ACTION_UP == action) { 407 if (mOpen) { 408 View v = mCurrentView; 409 deselect(); 410 if (v != null) { 411 v.performClick(); 412 } 413 show(false); 414 return true; 415 } 416 } else if (MotionEvent.ACTION_CANCEL == action) { 417 if (mOpen) { 418 show(false); 419 } 420 deselect(); 421 return false; 422 } else if (MotionEvent.ACTION_MOVE == action) { 423 PointF polar = getPolar(x, y); 424 if (polar.y > mRadius + 2 * mRadiusInc) { 425 show(false); 426 deselect(); 427 evt.setAction(MotionEvent.ACTION_DOWN); 428 if (getParent() != null) { 429 ((ViewGroup) getParent()).dispatchTouchEvent(evt); 430 } 431 return false; 432 } 433 View v = findView(polar); 434 if (mCurrentView != v) { 435 onEnter(v); 436 invalidate(); 437 } 438 } 439 // always re-dispatch event 440 return false; 441 } 442 443 /** 444 * enter a slice for a view 445 * updates model only 446 * @param view 447 */ 448 private void onEnter(View view) { 449 // deselect 450 if (mCurrentView != null) { 451 if (getLevel(mCurrentView) >= getLevel(view)) { 452 mCurrentView.setPressed(false); 453 } 454 } 455 if (view != null) { 456 // clear up stack 457 playSoundEffect(SoundEffectConstants.CLICK); 458 MenuTag tag = (MenuTag) view.getTag(); 459 int i = mStack.size() - 1; 460 while (i > 0) { 461 View v = mStack.get(i); 462 if (((MenuTag) v.getTag()).level >= tag.level) { 463 v.setPressed(false); 464 mStack.remove(i); 465 } else { 466 break; 467 } 468 i--; 469 } 470 List<View> items = mMenu.get(view); 471 if (items != null) { 472 mStack.add(view); 473 mDirty = true; 474 } 475 view.setPressed(true); 476 } 477 mCurrentView = view; 478 } 479 480 private void deselect() { 481 if (mCurrentView != null) { 482 mCurrentView.setPressed(false); 483 } 484 mCurrentView = null; 485 } 486 487 private int getLevel(View v) { 488 if (v == null) return -1; 489 return ((MenuTag) v.getTag()).level; 490 } 491 492 private PointF getPolar(float x, float y) { 493 PointF res = new PointF(); 494 // get angle and radius from x/y 495 res.x = (float) Math.PI / 2; 496 x = mCenter.x - x; 497 if (mCenter.x < mSlop) { 498 x = -x; 499 } 500 y = mCenter.y - y; 501 res.y = (float) Math.sqrt(x * x + y * y); 502 if (y > 0) { 503 res.x = (float) Math.asin(x / res.y); 504 } else if (y < 0) { 505 res.x = (float) (Math.PI - Math.asin(x / res.y )); 506 } 507 return res; 508 } 509 510 /** 511 * 512 * @param polar x: angle, y: dist 513 * @return 514 */ 515 private View findView(PointF polar) { 516 // find the matching item: 517 for (View parent : mStack) { 518 List<View> subs = mMenu.get(parent); 519 if (subs != null) { 520 for (View item : subs) { 521 MenuTag tag = (MenuTag) item.getTag(); 522 if ((tag.inner < polar.y) 523 && (tag.outer > polar.y) 524 && (tag.start < polar.x) 525 && (tag.start + tag.sweep > polar.x)) { 526 return item; 527 } 528 } 529 } 530 } 531 return null; 532 } 533 534 class MenuTag { 535 536 int level; 537 float start; 538 float sweep; 539 int inner; 540 int outer; 541 542 public MenuTag(int l) { 543 level = l; 544 } 545 546 } 547 548} 549