PieMenu.java revision acb126d7fc636e403756e3828765d23bc81d4ac6
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 if (MotionEvent.ACTION_DOWN == action) { 314 if ((x > getWidth() - mSlop) || (x < mSlop)) { 315 setCenter((int) x, (int) y); 316 show(true); 317 return true; 318 } 319 } else if (MotionEvent.ACTION_UP == action) { 320 if (mOpen) { 321 boolean handled = false; 322 if (mPieView != null) { 323 handled = mPieView.onTouchEvent(evt); 324 } 325 PieItem item = mCurrentItem; 326 deselect(); 327 show(false); 328 if (!handled && (item != null)) { 329 item.getView().performClick(); 330 } 331 return true; 332 } 333 } else if (MotionEvent.ACTION_CANCEL == action) { 334 if (mOpen) { 335 show(false); 336 } 337 deselect(); 338 return false; 339 } else if (MotionEvent.ACTION_MOVE == action) { 340 boolean handled = false; 341 PointF polar = getPolar(x, y); 342 int maxr = mRadius + mLevels * mRadiusInc + 50; 343 if (mPieView != null) { 344 handled = mPieView.onTouchEvent(evt); 345 } 346 if (handled) { 347 invalidate(); 348 return false; 349 } 350 if (polar.y > maxr) { 351 deselect(); 352 show(false); 353 evt.setAction(MotionEvent.ACTION_DOWN); 354 if (getParent() != null) { 355 ((ViewGroup) getParent()).dispatchTouchEvent(evt); 356 } 357 return false; 358 } 359 PieItem item = findItem(polar); 360 if (mCurrentItem != item) { 361 onEnter(item); 362 if ((item != null) && item.isPieView()) { 363 int cx = item.getView().getLeft() + (onTheLeft() 364 ? item.getView().getWidth() : 0); 365 int cy = item.getView().getTop(); 366 mPieView = item.getPieView(); 367 layoutPieView(mPieView, cx, cy, 368 (item.getStartAngle() + item.getSweep()) / 2); 369 } 370 invalidate(); 371 } 372 } 373 // always re-dispatch event 374 return false; 375 } 376 377 private void layoutPieView(PieView pv, int x, int y, float angle) { 378 pv.layout(x, y, onTheLeft(), angle); 379 } 380 381 /** 382 * enter a slice for a view 383 * updates model only 384 * @param item 385 */ 386 private void onEnter(PieItem item) { 387 // deselect 388 if (mCurrentItem != null) { 389 mCurrentItem.setSelected(false); 390 } 391 if (item != null) { 392 // clear up stack 393 playSoundEffect(SoundEffectConstants.CLICK); 394 item.setSelected(true); 395 mPieView = null; 396 } 397 mCurrentItem = item; 398 } 399 400 private void deselect() { 401 if (mCurrentItem != null) { 402 mCurrentItem.setSelected(false); 403 } 404 mCurrentItem = null; 405 mPieView = null; 406 } 407 408 private PointF getPolar(float x, float y) { 409 PointF res = new PointF(); 410 // get angle and radius from x/y 411 res.x = (float) Math.PI / 2; 412 x = mCenter.x - x; 413 if (mCenter.x < mSlop) { 414 x = -x; 415 } 416 y = mCenter.y - y; 417 res.y = (float) Math.sqrt(x * x + y * y); 418 if (y > 0) { 419 res.x = (float) Math.asin(x / res.y); 420 } else if (y < 0) { 421 res.x = (float) (Math.PI - Math.asin(x / res.y )); 422 } 423 return res; 424 } 425 426 /** 427 * 428 * @param polar x: angle, y: dist 429 * @return the item at angle/dist or null 430 */ 431 private PieItem findItem(PointF polar) { 432 // find the matching item: 433 for (PieItem item : mItems) { 434 if ((item.getInnerRadius() - mTouchOffset < polar.y) 435 && (item.getOuterRadius() - mTouchOffset > polar.y) 436 && (item.getStartAngle() < polar.x) 437 && (item.getStartAngle() + item.getSweep() > polar.x)) { 438 return item; 439 } 440 } 441 return null; 442 } 443 444} 445