PieMenu.java revision 0860d99a463f7645bcc9aaa246fd8852e90dbb5d
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.Point; 25import android.graphics.PointF; 26import android.graphics.drawable.Drawable; 27import android.util.AttributeSet; 28import android.view.MotionEvent; 29import android.view.SoundEffectConstants; 30import android.view.View; 31import android.view.ViewGroup; 32import android.widget.FrameLayout; 33 34import java.util.ArrayList; 35import java.util.List; 36 37public class PieMenu extends FrameLayout { 38 39 private static final int MAX_LEVELS = 5; 40 41 public interface PieController { 42 /** 43 * called before menu opens to customize menu 44 * returns if pie state has been changed 45 */ 46 public boolean onOpen(); 47 } 48 49 private Point mCenter; 50 private int mRadius; 51 private int mRadiusInc; 52 private int mSlop; 53 54 private boolean mOpen; 55 private PieController mController; 56 57 private List<PieItem> mItems; 58 private int mLevels; 59 private int[] mCounts; 60 61 private Drawable mBackground; 62 63 // touch handling 64 PieItem mCurrentItem; 65 66 /** 67 * @param context 68 * @param attrs 69 * @param defStyle 70 */ 71 public PieMenu(Context context, AttributeSet attrs, int defStyle) { 72 super(context, attrs, defStyle); 73 init(context); 74 } 75 76 /** 77 * @param context 78 * @param attrs 79 */ 80 public PieMenu(Context context, AttributeSet attrs) { 81 super(context, attrs); 82 init(context); 83 } 84 85 /** 86 * @param context 87 */ 88 public PieMenu(Context context) { 89 super(context); 90 init(context); 91 } 92 93 private void init(Context ctx) { 94 mItems = new ArrayList<PieItem>(); 95 mLevels = 0; 96 mCounts = new int[MAX_LEVELS]; 97 Resources res = ctx.getResources(); 98 mRadius = (int) res.getDimension(R.dimen.qc_radius_start); 99 mRadiusInc = (int) res.getDimension(R.dimen.qc_radius_increment); 100 mSlop = (int) res.getDimension(R.dimen.qc_slop); 101 mOpen = false; 102 setWillNotDraw(false); 103 setDrawingCacheEnabled(false); 104 mCenter = new Point(0,0); 105 mBackground = res.getDrawable(R.drawable.qc_background_normal); 106 } 107 108 public void setController(PieController ctl) { 109 mController = ctl; 110 } 111 112 public void addItem(PieItem item) { 113 // add the item to the pie itself 114 mItems.add(item); 115 int l = item.getLevel(); 116 mLevels = Math.max(mLevels, l); 117 mCounts[l]++; 118 } 119 120 public void removeItem(PieItem item) { 121 mItems.remove(item); 122 } 123 124 public void clearItems() { 125 mItems.clear(); 126 } 127 128 private boolean onTheLeft() { 129 return mCenter.x < mSlop; 130 } 131 132 /** 133 * guaranteed has center set 134 * @param show 135 */ 136 private void show(boolean show) { 137 mOpen = show; 138 if (mOpen) { 139 if (mController != null) { 140 boolean changed = mController.onOpen(); 141 } 142 layoutPie(); 143 } 144 if (!show) { 145 mCurrentItem = null; 146 } 147 invalidate(); 148 } 149 150 private void setCenter(int x, int y) { 151 if (x < mSlop) { 152 mCenter.x = 0; 153 } else { 154 mCenter.x = getWidth(); 155 } 156 mCenter.y = y; 157 } 158 159 private void layoutPie() { 160 int inner = mRadius; 161 int outer = mRadius + mRadiusInc; 162 for (int i = 0; i < mLevels; i++) { 163 int level = i + 1; 164 float sweep = (float) Math.PI / (mCounts[level] + 1); 165 float angle = sweep; 166 for (PieItem item : mItems) { 167 if (item.getLevel() == level) { 168 View view = item.getView(); 169 view.measure(view.getLayoutParams().width, 170 view.getLayoutParams().height); 171 int w = view.getMeasuredWidth(); 172 int h = view.getMeasuredHeight(); 173 int x = (int) (outer * Math.sin(angle)); 174 int y = mCenter.y - (int) (outer * Math.cos(angle)) - h / 2; 175 if (onTheLeft()) { 176 x = mCenter.x + x - w; 177 } else { 178 x = mCenter.x - x; 179 } 180 view.layout(x, y, x + w, y + h); 181 float itemstart = angle - sweep / 2; 182 item.setGeometry(itemstart, sweep, inner, outer); 183 angle += sweep; 184 } 185 } 186 inner += mRadiusInc; 187 outer += mRadiusInc; 188 } 189 } 190 191 @Override 192 protected void onDraw(Canvas canvas) { 193 if (mOpen) { 194 int w = mBackground.getIntrinsicWidth(); 195 int h = mBackground.getIntrinsicHeight(); 196 int left = mCenter.x - w; 197 int top = mCenter.y - h / 2; 198 mBackground.setBounds(left, top, left + w, top + h); 199 int state = canvas.save(); 200 if (onTheLeft()) { 201 canvas.scale(-1, 1); 202 } 203 mBackground.draw(canvas); 204 canvas.restoreToCount(state); 205 for (PieItem item : mItems) { 206 drawItem(canvas, item); 207 } 208 } 209 } 210 211 private void drawItem(Canvas canvas, PieItem item) { 212 int outer = item.getOuterRadius(); 213 int left = mCenter.x - outer; 214 int top = mCenter.y - outer; 215 // draw the item view 216 View view = item.getView(); 217 int state = canvas.save(); 218 canvas.translate(view.getX(), view.getY()); 219 view.draw(canvas); 220 canvas.restoreToCount(state); 221 } 222 223 // touch handling for pie 224 225 @Override 226 public boolean onTouchEvent(MotionEvent evt) { 227 float x = evt.getX(); 228 float y = evt.getY(); 229 int action = evt.getActionMasked(); 230 int edges = evt.getEdgeFlags(); 231 if (MotionEvent.ACTION_DOWN == action) { 232 if ((x > getWidth() - mSlop) || (x < mSlop)) { 233 setCenter((int) x, (int) y); 234 show(true); 235 return true; 236 } 237 } else if (MotionEvent.ACTION_UP == action) { 238 if (mOpen) { 239 PieItem item = mCurrentItem; 240 deselect(); 241 show(false); 242 if (item != null) { 243 item.getView().performClick(); 244 } 245 return true; 246 } 247 } else if (MotionEvent.ACTION_CANCEL == action) { 248 if (mOpen) { 249 show(false); 250 } 251 deselect(); 252 return false; 253 } else if (MotionEvent.ACTION_MOVE == action) { 254 boolean handled = false; 255 PointF polar = getPolar(x, y); 256 int maxr = mRadius + mLevels * mRadiusInc + 50; 257 if (polar.y > maxr) { 258 deselect(); 259 show(false); 260 evt.setAction(MotionEvent.ACTION_DOWN); 261 if (getParent() != null) { 262 ((ViewGroup) getParent()).dispatchTouchEvent(evt); 263 } 264 return false; 265 } 266 PieItem item = findItem(polar); 267 if (mCurrentItem != item) { 268 onEnter(item); 269 invalidate(); 270 } 271 } 272 // always re-dispatch event 273 return false; 274 } 275 276 /** 277 * enter a slice for a view 278 * updates model only 279 * @param item 280 */ 281 private void onEnter(PieItem item) { 282 // deselect 283 if (mCurrentItem != null) { 284 mCurrentItem.setSelected(false); 285 } 286 if (item != null) { 287 // clear up stack 288 playSoundEffect(SoundEffectConstants.CLICK); 289 item.setSelected(true); 290 } 291 mCurrentItem = item; 292 } 293 294 private void deselect() { 295 if (mCurrentItem != null) { 296 mCurrentItem.setSelected(false); 297 } 298 mCurrentItem = null; 299 } 300 301 private PointF getPolar(float x, float y) { 302 PointF res = new PointF(); 303 // get angle and radius from x/y 304 res.x = (float) Math.PI / 2; 305 x = mCenter.x - x; 306 if (mCenter.x < mSlop) { 307 x = -x; 308 } 309 y = mCenter.y - y; 310 res.y = (float) Math.sqrt(x * x + y * y); 311 if (y > 0) { 312 res.x = (float) Math.asin(x / res.y); 313 } else if (y < 0) { 314 res.x = (float) (Math.PI - Math.asin(x / res.y )); 315 } 316 return res; 317 } 318 319 /** 320 * 321 * @param polar x: angle, y: dist 322 * @return the item at angle/dist or null 323 */ 324 private PieItem findItem(PointF polar) { 325 // find the matching item: 326 for (PieItem item : mItems) { 327 if ((item.getInnerRadius() < polar.y) 328 && (item.getOuterRadius() > polar.y) 329 && (item.getStartAngle() < polar.x) 330 && (item.getStartAngle() + item.getSweep() > polar.x)) { 331 return item; 332 } 333 } 334 return null; 335 } 336 337} 338