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