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