PieRenderer.java revision c605826581f2ef1640828af82dbf26a70d4c7c78
1/* 2 * Copyright (C) 2012 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.camera.ui; 18 19import android.annotation.TargetApi; 20import android.content.Context; 21import android.content.res.Resources; 22import android.graphics.Canvas; 23import android.graphics.Color; 24import android.graphics.Paint; 25import android.graphics.Path; 26import android.graphics.Point; 27import android.graphics.PointF; 28import android.graphics.RectF; 29import android.os.Handler; 30import android.os.Message; 31import android.util.Log; 32import android.view.MotionEvent; 33import android.view.View; 34import android.view.animation.Animation; 35import android.view.animation.Animation.AnimationListener; 36import android.view.animation.Transformation; 37 38import com.android.camera.R; 39import com.android.gallery3d.common.ApiHelper; 40 41import java.util.ArrayList; 42import java.util.List; 43 44public class PieRenderer extends OverlayRenderer { 45 46 private static final String TAG = "CAM Pie"; 47 48 private static final long PIE_OPEN_DELAY = 200; 49 50 private static final int MSG_OPEN = 2; 51 private static final int MSG_CLOSE = 3; 52 private static final int MSG_SUBMENU = 4; 53 private static final float PIE_SWEEP = (float)(Math.PI * 2 / 3); 54 // geometry 55 private Point mCenter; 56 private int mRadius; 57 private int mRadiusInc; 58 private int mSlop; 59 // the detection if touch is inside a slice is offset 60 // inbounds by this amount to allow the selection to show before the 61 // finger covers it 62 private int mTouchOffset; 63 64 private List<PieItem> mItems; 65 66 private PieItem mOpenItem; 67 68 private Paint mNormalPaint; 69 private Paint mSelectedPaint; 70 private Paint mSubPaint; 71 72 // touch handling 73 private PieItem mCurrentItem; 74 75 private boolean mAnimating; 76 private float mAlpha; 77 78 private Handler mHandler = new Handler() { 79 public void handleMessage(Message msg) { 80 switch(msg.what) { 81 case MSG_OPEN: 82 if (mListener != null && !mAnimating) { 83 mListener.onPieOpened(mCenter.x, mCenter.y); 84 } 85 break; 86 case MSG_CLOSE: 87 if (mListener != null && !mAnimating) { 88 mListener.onPieClosed(); 89 } 90 break; 91 case MSG_SUBMENU: 92 openCurrentItem(); 93 break; 94 } 95 } 96 }; 97 98 private PieListener mListener; 99 100 static public interface PieListener { 101 public void onPieOpened(int centerX, int centerY); 102 public void onPieClosed(); 103 } 104 105 public void setPieListener(PieListener pl) { 106 mListener = pl; 107 } 108 109 public PieRenderer(Context context) { 110 init(context); 111 } 112 private void init(Context ctx) { 113 setVisible(false); 114 mItems = new ArrayList<PieItem>(); 115 Resources res = ctx.getResources(); 116 mRadius = (int) res.getDimensionPixelSize(R.dimen.pie_radius_start); 117 mRadiusInc = (int) res.getDimensionPixelSize(R.dimen.pie_radius_increment); 118 mSlop = (int) res.getDimensionPixelSize(R.dimen.pie_touch_slop); 119 mTouchOffset = (int) res.getDimensionPixelSize(R.dimen.pie_touch_offset); 120 mCenter = new Point(0,0); 121 mNormalPaint = new Paint(); 122 mNormalPaint.setColor(Color.argb(0, 0, 0, 0)); 123 mNormalPaint.setAntiAlias(true); 124 mSelectedPaint = new Paint(); 125 mSelectedPaint.setColor(Color.argb(128, 0, 0, 0)); //res.getColor(R.color.qc_selected)); 126 mSelectedPaint.setAntiAlias(true); 127 mSubPaint = new Paint(); 128 mSubPaint.setAntiAlias(true); 129 mSubPaint.setColor(Color.argb(200, 250, 230, 128)); //res.getColor(R.color.qc_sub)); 130 } 131 132 public void addItem(PieItem item) { 133 // add the item to the pie itself 134 mItems.add(item); 135 } 136 137 public void removeItem(PieItem item) { 138 mItems.remove(item); 139 } 140 141 public void clearItems() { 142 mItems.clear(); 143 } 144 145 public void fade() { 146 Animation anim = new AlphaAnimation(); 147 anim.setFillAfter(true); 148 anim.setAnimationListener(new AnimationListener() { 149 @Override 150 public void onAnimationStart(Animation animation) { 151 mAnimating = true; 152 update(); 153 } 154 @Override 155 public void onAnimationEnd(Animation animation) { 156 show(false); 157 mAlpha = 0f; 158 mAnimating = false; 159 setViewAlpha(mOverlay, 1); 160 } 161 @Override 162 public void onAnimationRepeat(Animation animation) { 163 } 164 }); 165 anim.reset(); 166 anim.setDuration(500); 167 show(true); 168 mOverlay.startAnimation(anim); 169 } 170 171 /** 172 * guaranteed has center set 173 * @param show 174 */ 175 private void show(boolean show) { 176 if (show) { 177 // ensure clean state 178 mAnimating = false; 179 mCurrentItem = null; 180 mOpenItem = null; 181 for (PieItem item : mItems) { 182 item.setSelected(false); 183 } 184 layoutPie(); 185 } 186 setVisible(show); 187 mHandler.sendEmptyMessage(show ? MSG_OPEN : MSG_CLOSE); 188 } 189 190 public void setCenter(int x, int y) { 191 mCenter.x = x; 192 mCenter.y = y; 193 } 194 195 private void layoutPie() { 196 int rgap = 2; 197 int inner = mRadius + rgap; 198 int outer = mRadius + mRadiusInc - rgap; 199 int gap = 1; 200 layoutItems(mItems, (float) (Math.PI / 2), inner, outer, gap); 201 } 202 203 private void layoutItems(List<PieItem> items, float centerAngle, int inner, 204 int outer, int gap) { 205 float emptyangle = PIE_SWEEP / 16; 206 float sweep = (float) (PIE_SWEEP - 2 * emptyangle) / items.size(); 207 float angle = centerAngle - PIE_SWEEP / 2 + emptyangle + sweep / 2; 208 // check if we have custom geometry 209 // first item we find triggers custom sweep for all 210 // this allows us to re-use the path 211 for (PieItem item : items) { 212 if (item.getCenter() >= 0) { 213 sweep = item.getSweep(); 214 break; 215 } 216 } 217 Path path = makeSlice(getDegrees(0) - gap, getDegrees(sweep) + gap, 218 outer, inner, mCenter); 219 for (PieItem item : items) { 220 // shared between items 221 item.setPath(path); 222 View view = item.getView(); 223 if (item.getCenter() >= 0) { 224 angle = item.getCenter(); 225 } 226 if (view != null) { 227 view.measure(view.getLayoutParams().width, 228 view.getLayoutParams().height); 229 int w = view.getMeasuredWidth(); 230 int h = view.getMeasuredHeight(); 231 // move views to outer border 232 int r = inner + (outer - inner) * 2 / 3; 233 int x = (int) (r * Math.cos(angle)); 234 int y = mCenter.y - (int) (r * Math.sin(angle)) - h / 2; 235 x = mCenter.x + x - w / 2; 236 view.layout(x, y, x + w, y + h); 237 } 238 float itemstart = angle - sweep / 2; 239 item.setGeometry(itemstart, sweep, inner, outer); 240 if (item.hasItems()) { 241 layoutItems(item.getItems(), angle, inner, 242 outer + mRadiusInc / 2, gap); 243 } 244 angle += sweep; 245 } 246 } 247 248 private Path makeSlice(float start, float end, int outer, int inner, Point center) { 249 outer = inner + (outer - inner) * 2 / 3; 250 RectF bb = 251 new RectF(center.x - outer, center.y - outer, center.x + outer, 252 center.y + outer); 253 RectF bbi = 254 new RectF(center.x - inner, center.y - inner, center.x + inner, 255 center.y + inner); 256 Path path = new Path(); 257 path.arcTo(bb, start, end - start, true); 258 path.arcTo(bbi, end, start - end); 259 path.close(); 260 return path; 261 } 262 263 /** 264 * converts a 265 * @param angle from 0..PI to Android degrees (clockwise starting at 3 o'clock) 266 * @return skia angle 267 */ 268 private float getDegrees(double angle) { 269 return (float) (360 - 180 * angle / Math.PI); 270 } 271 272 @Override 273 public void onDraw(Canvas canvas) { 274 if (mAnimating) { 275 setViewAlpha(mOverlay, mAlpha); 276 } 277 if (mOpenItem == null) { 278 // draw base menu 279 for (PieItem item : mItems) { 280 drawItem(canvas, item); 281 } 282 } else { 283 for (PieItem inner : mOpenItem.getItems()) { 284 drawItem(canvas, inner); 285 } 286 } 287 } 288 289 private void drawItem(Canvas canvas, PieItem item) { 290 if (item.getView() != null) { 291 Paint p = item.isSelected() ? mSelectedPaint : mNormalPaint; 292 int state = canvas.save(); 293 float r = getDegrees(item.getStartAngle()); 294 canvas.rotate(r, mCenter.x, mCenter.y); 295 canvas.drawPath(item.getPath(), p); 296 canvas.restoreToCount(state); 297 // draw the item view 298 View view = item.getView(); 299 state = canvas.save(); 300 canvas.translate(view.getX(), view.getY()); 301 view.draw(canvas); 302 canvas.restoreToCount(state); 303 } 304 } 305 306 @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) 307 private void setViewAlpha(View v, float alpha) { 308 if (ApiHelper.HAS_VIEW_TRANSFORM_PROPERTIES) { 309 v.setAlpha(alpha); 310 } 311 } 312 313 // touch handling for pie 314 315 @Override 316 public boolean onTouchEvent(MotionEvent evt) { 317 float x = evt.getX(); 318 float y = evt.getY(); 319 int action = evt.getActionMasked(); 320 if (MotionEvent.ACTION_DOWN == action) { 321 setCenter((int) x, (int) y); 322 show(true); 323 return true; 324 } else if (MotionEvent.ACTION_UP == action) { 325 if (isVisible()) { 326 PieItem item = mCurrentItem; 327 if (!mAnimating) { 328 deselect(); 329 } 330 show(false); 331 if ((item != null) && (item.getView() != null)) { 332 if ((item == mOpenItem) || !mAnimating) { 333 item.getView().performClick(); 334 } 335 } 336 return true; 337 } 338 } else if (MotionEvent.ACTION_CANCEL == action) { 339 if (isVisible()) { 340 show(false); 341 } 342 if (!mAnimating) { 343 deselect(); 344 } 345 return false; 346 } else if (MotionEvent.ACTION_MOVE == action) { 347 if (mAnimating) return false; 348 PointF polar = getPolar(x, y); 349 int maxr = mRadius + mRadiusInc + 50; 350 if (polar.y < mRadius) { 351 if (mOpenItem != null) { 352 mOpenItem = null; 353 } else if (!mAnimating) { 354 deselect(); 355 } 356 return false; 357 } 358 if (polar.y > maxr) { 359 deselect(); 360 show(false); 361 evt.setAction(MotionEvent.ACTION_DOWN); 362 return false; 363 } 364 PieItem item = findItem(polar); 365 if (item == null) { 366 } else if (mCurrentItem != item) { 367 onEnter(item); 368 } 369 } 370 return false; 371 } 372 373 /** 374 * enter a slice for a view 375 * updates model only 376 * @param item 377 */ 378 private void onEnter(PieItem item) { 379 if (mCurrentItem != null) { 380 mCurrentItem.setSelected(false); 381 } 382 if (item != null && item.isEnabled()) { 383 item.setSelected(true); 384 mCurrentItem = item; 385 if ((mCurrentItem != mOpenItem) && mCurrentItem.hasItems()) { 386 mHandler.sendEmptyMessageDelayed(MSG_SUBMENU, PIE_OPEN_DELAY); 387 } 388 } else { 389 mCurrentItem = null; 390 } 391 } 392 393 private void deselect() { 394 if (mCurrentItem != null) { 395 mCurrentItem.setSelected(false); 396 mHandler.removeMessages(MSG_SUBMENU); 397 } 398 if (mOpenItem != null) { 399 mOpenItem = null; 400 } 401 mCurrentItem = null; 402 } 403 404 private void openCurrentItem() { 405 if ((mCurrentItem != null) && mCurrentItem.hasItems()) { 406 mOpenItem = mCurrentItem; 407 } 408 } 409 410 private PointF getPolar(float x, float y) { 411 PointF res = new PointF(); 412 // get angle and radius from x/y 413 res.x = (float) Math.PI / 2; 414 x = x - mCenter.x; 415 y = mCenter.y - y; 416 res.y = (float) Math.sqrt(x * x + y * y); 417 if (x != 0) { 418 res.x = (float) Math.atan2(y, x); 419 if (res.x < 0) { 420 res.x = (float) (2 * Math.PI + res.x); 421 } 422 } 423 res.y = res.y + mTouchOffset; 424 return res; 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 List<PieItem> items = (mOpenItem != null) ? mOpenItem.getItems() : mItems; 434 for (PieItem item : items) { 435 if (inside(polar, item)) { 436 return item; 437 } 438 } 439 return null; 440 } 441 442 private boolean inside(PointF polar, PieItem item) { 443 return (item.getInnerRadius() < polar.y) 444 && (item.getOuterRadius() > polar.y) 445 && (item.getStartAngle() < polar.x) 446 && (item.getStartAngle() + item.getSweep() > polar.x); 447 } 448 449 @Override 450 public boolean handlesTouch() { 451 return true; 452 } 453 454 @Override 455 public void layout(int l, int t, int r, int b) { 456 super.layout(l, t, r, b); 457 } 458 459 private class AlphaAnimation extends Animation { 460 @Override 461 protected void applyTransformation(float interpolatedTime, Transformation t) { 462 mAlpha = 1 - interpolatedTime; 463 } 464 } 465 466} 467