PieMenu.java revision 565752e1025129ad16c030ceca9dee7a695ed73e
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.RectF; 29import android.graphics.drawable.Drawable; 30import android.util.AttributeSet; 31import android.view.MotionEvent; 32import android.view.SoundEffectConstants; 33import android.view.View; 34import android.view.ViewGroup; 35import android.widget.FrameLayout; 36 37import java.util.ArrayList; 38import java.util.List; 39 40public class PieMenu extends FrameLayout { 41 42 private static final int MAX_LEVELS = 5; 43 44 public interface PieController { 45 /** 46 * called before menu opens to customize menu 47 * returns if pie state has been changed 48 */ 49 public boolean onOpen(); 50 } 51 52 /** 53 * A view like object that lives off of the pie menu 54 */ 55 public interface PieView { 56 57 public interface OnLayoutListener { 58 public void onLayout(int ax, int ay, boolean left); 59 } 60 61 public void setLayoutListener(OnLayoutListener l); 62 63 public void layout(int anchorX, int anchorY, boolean onleft, float angle); 64 65 public void draw(Canvas c); 66 67 public boolean onTouchEvent(MotionEvent evt); 68 69 } 70 71 private Point mCenter; 72 private int mRadius; 73 private int mRadiusInc; 74 private int mSlop; 75 private int mTouchOffset; 76 77 private boolean mOpen; 78 private PieController mController; 79 80 private List<PieItem> mItems; 81 private int mLevels; 82 private int[] mCounts; 83 private PieView mPieView = null; 84 85 private Drawable mBackground; 86 private Paint mNormalPaint; 87 private Paint mSelectedPaint; 88 89 // touch handling 90 PieItem mCurrentItem; 91 92 /** 93 * @param context 94 * @param attrs 95 * @param defStyle 96 */ 97 public PieMenu(Context context, AttributeSet attrs, int defStyle) { 98 super(context, attrs, defStyle); 99 init(context); 100 } 101 102 /** 103 * @param context 104 * @param attrs 105 */ 106 public PieMenu(Context context, AttributeSet attrs) { 107 super(context, attrs); 108 init(context); 109 } 110 111 /** 112 * @param context 113 */ 114 public PieMenu(Context context) { 115 super(context); 116 init(context); 117 } 118 119 private void init(Context ctx) { 120 mItems = new ArrayList<PieItem>(); 121 mLevels = 0; 122 mCounts = new int[MAX_LEVELS]; 123 Resources res = ctx.getResources(); 124 mRadius = (int) res.getDimension(R.dimen.qc_radius_start); 125 mRadiusInc = (int) res.getDimension(R.dimen.qc_radius_increment); 126 mSlop = (int) res.getDimension(R.dimen.qc_slop); 127 mTouchOffset = (int) res.getDimension(R.dimen.qc_touch_offset); 128 mOpen = false; 129 setWillNotDraw(false); 130 setDrawingCacheEnabled(false); 131 mCenter = new Point(0,0); 132 mBackground = res.getDrawable(R.drawable.qc_background_normal); 133 mNormalPaint = new Paint(); 134 mNormalPaint.setColor(res.getColor(R.color.qc_normal)); 135 mNormalPaint.setAntiAlias(true); 136 mSelectedPaint = new Paint(); 137 mSelectedPaint.setColor(res.getColor(R.color.qc_selected)); 138 mSelectedPaint.setAntiAlias(true); 139 } 140 141 public void setController(PieController ctl) { 142 mController = ctl; 143 } 144 145 public void addItem(PieItem item) { 146 // add the item to the pie itself 147 mItems.add(item); 148 int l = item.getLevel(); 149 mLevels = Math.max(mLevels, l); 150 mCounts[l]++; 151 } 152 153 public void removeItem(PieItem item) { 154 mItems.remove(item); 155 } 156 157 public void clearItems() { 158 mItems.clear(); 159 } 160 161 private boolean onTheLeft() { 162 return mCenter.x < mSlop; 163 } 164 165 /** 166 * guaranteed has center set 167 * @param show 168 */ 169 private void show(boolean show) { 170 mOpen = show; 171 if (mOpen) { 172 if (mController != null) { 173 boolean changed = mController.onOpen(); 174 } 175 layoutPie(); 176 } 177 if (!show) { 178 mCurrentItem = null; 179 mPieView = null; 180 } 181 invalidate(); 182 } 183 184 private void setCenter(int x, int y) { 185 if (x < mSlop) { 186 mCenter.x = 0; 187 } else { 188 mCenter.x = getWidth(); 189 } 190 mCenter.y = y; 191 } 192 193 private void layoutPie() { 194 float emptyangle = (float) Math.PI / 16; 195 int rgap = 2; 196 int inner = mRadius + rgap; 197 int outer = mRadius + mRadiusInc - rgap; 198 int radius = mRadius; 199 int gap = 1; 200 for (int i = 0; i < mLevels; i++) { 201 int level = i + 1; 202 float sweep = (float) (Math.PI - 2 * emptyangle) / mCounts[level]; 203 float angle = emptyangle + sweep / 2; 204 for (PieItem item : mItems) { 205 if (item.getLevel() == level) { 206 View view = item.getView(); 207 view.measure(view.getLayoutParams().width, 208 view.getLayoutParams().height); 209 int w = view.getMeasuredWidth(); 210 int h = view.getMeasuredHeight(); 211 int r = inner + (outer - inner) * 2 / 3; 212 int x = (int) (r * Math.sin(angle)); 213 int y = mCenter.y - (int) (r * Math.cos(angle)) - h / 2; 214 if (onTheLeft()) { 215 x = mCenter.x + x - w / 2; 216 } else { 217 x = mCenter.x - x - w / 2; 218 } 219 view.layout(x, y, x + w, y + h); 220 float itemstart = angle - sweep / 2; 221 Path slice = makeSlice(getDegrees(itemstart) - gap, 222 getDegrees(itemstart + sweep) + gap, 223 outer, inner, mCenter); 224 item.setGeometry(itemstart, sweep, inner, outer, slice); 225 angle += sweep; 226 } 227 } 228 inner += mRadiusInc; 229 outer += mRadiusInc; 230 } 231 } 232 233 234 /** 235 * converts a 236 * 237 * @param angle from 0..PI to Android degrees (clockwise starting at 3 238 * o'clock) 239 * @return skia angle 240 */ 241 private float getDegrees(double angle) { 242 return (float) (270 - 180 * angle / Math.PI); 243 } 244 245 @Override 246 protected void onDraw(Canvas canvas) { 247 if (mOpen) { 248 int w = mBackground.getIntrinsicWidth(); 249 int h = mBackground.getIntrinsicHeight(); 250 int left = mCenter.x - w; 251 int top = mCenter.y - h / 2; 252 mBackground.setBounds(left, top, left + w, top + h); 253 int state; 254 state = canvas.save(); 255 if (onTheLeft()) { 256 canvas.scale(-1, 1); 257 } 258 mBackground.draw(canvas); 259 canvas.restoreToCount(state); 260 for (PieItem item : mItems) { 261 Paint p = item.isSelected() ? mSelectedPaint : mNormalPaint; 262 state = canvas.save(); 263 if (onTheLeft()) { 264 canvas.scale(-1, 1); 265 } 266 drawPath(canvas, item.getPath(), p); 267 canvas.restoreToCount(state); 268 drawItem(canvas, item); 269 } 270 if (mPieView != null) { 271 mPieView.draw(canvas); 272 } 273 } 274 } 275 276 private void drawItem(Canvas canvas, PieItem item) { 277 int outer = item.getOuterRadius(); 278 int left = mCenter.x - outer; 279 int top = mCenter.y - outer; 280 // draw the item view 281 View view = item.getView(); 282 int state = canvas.save(); 283 canvas.translate(view.getX(), view.getY()); 284 view.draw(canvas); 285 canvas.restoreToCount(state); 286 } 287 288 private void drawPath(Canvas canvas, Path path, Paint paint) { 289 canvas.drawPath(path, paint); 290 } 291 292 private Path makeSlice(float start, float end, int outer, int inner, Point center) { 293 RectF bb = 294 new RectF(center.x - outer, center.y - outer, center.x + outer, 295 center.y + outer); 296 RectF bbi = 297 new RectF(center.x - inner, center.y - inner, center.x + inner, 298 center.y + inner); 299 Path path = new Path(); 300 path.arcTo(bb, start, end - start, true); 301 path.arcTo(bbi, end, start - end); 302 path.close(); 303 return path; 304 } 305 306 // touch handling for pie 307 308 @Override 309 public boolean onTouchEvent(MotionEvent evt) { 310 float x = evt.getX(); 311 float y = evt.getY(); 312 int action = evt.getActionMasked(); 313 int edges = evt.getEdgeFlags(); 314 if (MotionEvent.ACTION_DOWN == action) { 315 if ((x > getWidth() - mSlop) || (x < mSlop)) { 316 setCenter((int) x, (int) y); 317 show(true); 318 return true; 319 } 320 } else if (MotionEvent.ACTION_UP == action) { 321 if (mOpen) { 322 boolean handled = false; 323 if (mPieView != null) { 324 handled = mPieView.onTouchEvent(evt); 325 } 326 PieItem item = mCurrentItem; 327 deselect(); 328 show(false); 329 if (!handled && (item != null)) { 330 item.getView().performClick(); 331 } 332 return true; 333 } 334 } else if (MotionEvent.ACTION_CANCEL == action) { 335 if (mOpen) { 336 show(false); 337 } 338 deselect(); 339 return false; 340 } else if (MotionEvent.ACTION_MOVE == action) { 341 boolean handled = false; 342 PointF polar = getPolar(x, y); 343 int maxr = mRadius + mLevels * mRadiusInc + 50; 344 if (mPieView != null) { 345 handled = mPieView.onTouchEvent(evt); 346 } 347 if (handled) { 348 invalidate(); 349 return false; 350 } 351 if (polar.y > maxr) { 352 deselect(); 353 show(false); 354 evt.setAction(MotionEvent.ACTION_DOWN); 355 if (getParent() != null) { 356 ((ViewGroup) getParent()).dispatchTouchEvent(evt); 357 } 358 return false; 359 } 360 PieItem item = findItem(polar); 361 if (mCurrentItem != item) { 362 onEnter(item); 363 if ((item != null) && item.isPieView()) { 364 int cx = item.getView().getLeft() + (onTheLeft() 365 ? item.getView().getWidth() : 0); 366 int cy = item.getView().getTop(); 367 mPieView = item.getPieView(); 368 layoutPieView(mPieView, cx, cy, 369 (item.getStartAngle() + item.getSweep()) / 2); 370 } 371 invalidate(); 372 } 373 } 374 // always re-dispatch event 375 return false; 376 } 377 378 private void layoutPieView(PieView pv, int x, int y, float angle) { 379 pv.layout(x, y, onTheLeft(), angle); 380 } 381 382 /** 383 * enter a slice for a view 384 * updates model only 385 * @param item 386 */ 387 private void onEnter(PieItem item) { 388 // deselect 389 if (mCurrentItem != null) { 390 mCurrentItem.setSelected(false); 391 } 392 if (item != null) { 393 // clear up stack 394 playSoundEffect(SoundEffectConstants.CLICK); 395 item.setSelected(true); 396 mPieView = null; 397 } 398 mCurrentItem = item; 399 } 400 401 private void deselect() { 402 if (mCurrentItem != null) { 403 mCurrentItem.setSelected(false); 404 } 405 mCurrentItem = null; 406 mPieView = null; 407 } 408 409 private PointF getPolar(float x, float y) { 410 PointF res = new PointF(); 411 // get angle and radius from x/y 412 res.x = (float) Math.PI / 2; 413 x = mCenter.x - x; 414 if (mCenter.x < mSlop) { 415 x = -x; 416 } 417 y = mCenter.y - y; 418 res.y = (float) Math.sqrt(x * x + y * y); 419 if (y > 0) { 420 res.x = (float) Math.asin(x / res.y); 421 } else if (y < 0) { 422 res.x = (float) (Math.PI - Math.asin(x / res.y )); 423 } 424 return res; 425 } 426 427 /** 428 * 429 * @param polar x: angle, y: dist 430 * @return the item at angle/dist or null 431 */ 432 private PieItem findItem(PointF polar) { 433 // find the matching item: 434 for (PieItem item : mItems) { 435 if ((item.getInnerRadius() - mTouchOffset < polar.y) 436 && (item.getOuterRadius() - mTouchOffset > polar.y) 437 && (item.getStartAngle() < polar.x) 438 && (item.getStartAngle() + item.getSweep() > polar.x)) { 439 return item; 440 } 441 } 442 return null; 443 } 444 445} 446