blob: 4374fa624b18eb9c9d63dc7479c2c40eb2c854d7 [file] [log] [blame]
Michael Kolb8872c232013-01-29 10:33:22 -08001/*
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
Michael Kolb8872c232013-01-29 10:33:22 -080019import android.content.Context;
20import android.content.res.Resources;
21import android.graphics.Canvas;
22import android.graphics.Color;
23import android.graphics.Paint;
24import android.graphics.Path;
25import android.graphics.Point;
26import android.graphics.PointF;
27import android.graphics.RectF;
28import android.os.Handler;
29import android.os.Message;
30import android.view.MotionEvent;
31import android.view.ViewConfiguration;
32import android.view.animation.Animation;
33import android.view.animation.Animation.AnimationListener;
34import android.view.animation.LinearInterpolator;
35import android.view.animation.Transformation;
36
Michael Kolb10f4ba02013-04-10 08:50:51 -070037import com.android.camera.drawable.TextDrawable;
John Reck54987e82013-02-15 15:51:30 -080038import com.android.gallery3d.R;
Michael Kolb8872c232013-01-29 10:33:22 -080039
40import java.util.ArrayList;
41import java.util.List;
42
43public class PieRenderer extends OverlayRenderer
44 implements FocusIndicator {
45
46 private static final String TAG = "CAM Pie";
47
48 // Sometimes continuous autofocus starts and stops several times quickly.
49 // These states are used to make sure the animation is run for at least some
50 // time.
51 private volatile int mState;
52 private ScaleAnimation mAnimation = new ScaleAnimation();
53 private static final int STATE_IDLE = 0;
54 private static final int STATE_FOCUSING = 1;
55 private static final int STATE_FINISHING = 2;
56 private static final int STATE_PIE = 8;
57
Michael Kolb3bc96b22013-03-12 10:24:42 -070058 private static final float MATH_PI_2 = (float)(Math.PI / 2);
59
Michael Kolb8872c232013-01-29 10:33:22 -080060 private Runnable mDisappear = new Disappear();
61 private Animation.AnimationListener mEndAction = new EndAction();
62 private static final int SCALING_UP_TIME = 600;
63 private static final int SCALING_DOWN_TIME = 100;
64 private static final int DISAPPEAR_TIMEOUT = 200;
65 private static final int DIAL_HORIZONTAL = 157;
Michael Kolbebcf6fe2013-02-27 11:00:31 -080066 // fade out timings
67 private static final int PIE_FADE_OUT_DURATION = 600;
Michael Kolb8872c232013-01-29 10:33:22 -080068
69 private static final long PIE_FADE_IN_DURATION = 200;
70 private static final long PIE_XFADE_DURATION = 200;
71 private static final long PIE_SELECT_FADE_DURATION = 300;
Michael Kolb3bc96b22013-03-12 10:24:42 -070072 private static final long PIE_OPEN_SUB_DELAY = 400;
Michael Kolb8872c232013-01-29 10:33:22 -080073
74 private static final int MSG_OPEN = 0;
75 private static final int MSG_CLOSE = 1;
Michael Kolb3bc96b22013-03-12 10:24:42 -070076 private static final int MSG_OPENSUBMENU = 2;
Michael Kolb3daa3512013-04-02 16:03:04 -070077
78 protected static float CENTER = (float) Math.PI / 2;
79 protected static final float SWEEP_SLICE = 0.14f;
80 protected static final float SWEEP_ARC = 0.23f;
81
Michael Kolb8872c232013-01-29 10:33:22 -080082 // geometry
Michael Kolb8872c232013-01-29 10:33:22 -080083 private int mRadius;
Michael Kolb3daa3512013-04-02 16:03:04 -070084 private int mRadiusInc;
Michael Kolb8872c232013-01-29 10:33:22 -080085
86 // the detection if touch is inside a slice is offset
87 // inbounds by this amount to allow the selection to show before the
88 // finger covers it
89 private int mTouchOffset;
90
Michael Kolb3bc96b22013-03-12 10:24:42 -070091 private List<PieItem> mOpen;
Michael Kolb8872c232013-01-29 10:33:22 -080092
93 private Paint mSelectedPaint;
94 private Paint mSubPaint;
Michael Kolb3bc96b22013-03-12 10:24:42 -070095 private Paint mMenuArcPaint;
Michael Kolb8872c232013-01-29 10:33:22 -080096
97 // touch handling
98 private PieItem mCurrentItem;
99
100 private Paint mFocusPaint;
101 private int mSuccessColor;
102 private int mFailColor;
103 private int mCircleSize;
104 private int mFocusX;
105 private int mFocusY;
106 private int mCenterX;
107 private int mCenterY;
Michael Kolb3daa3512013-04-02 16:03:04 -0700108 private int mArcCenterY;
109 private int mSliceCenterY;
Michael Kolb3bc96b22013-03-12 10:24:42 -0700110 private int mPieCenterX;
111 private int mPieCenterY;
Michael Kolb3daa3512013-04-02 16:03:04 -0700112 private int mSliceRadius;
Michael Kolb3bc96b22013-03-12 10:24:42 -0700113 private int mArcRadius;
114 private int mArcOffset;
Michael Kolb8872c232013-01-29 10:33:22 -0800115
116 private int mDialAngle;
117 private RectF mCircle;
118 private RectF mDial;
119 private Point mPoint1;
120 private Point mPoint2;
121 private int mStartAnimationAngle;
122 private boolean mFocused;
123 private int mInnerOffset;
124 private int mOuterStroke;
125 private int mInnerStroke;
126 private boolean mTapMode;
127 private boolean mBlockFocus;
128 private int mTouchSlopSquared;
129 private Point mDown;
130 private boolean mOpening;
131 private LinearAnimation mXFade;
132 private LinearAnimation mFadeIn;
Michael Kolbebcf6fe2013-02-27 11:00:31 -0800133 private FadeOutAnimation mFadeOut;
Michael Kolb8872c232013-01-29 10:33:22 -0800134 private volatile boolean mFocusCancelled;
Michael Kolb3daa3512013-04-02 16:03:04 -0700135 private PointF mPolar = new PointF();
Michael Kolb10f4ba02013-04-10 08:50:51 -0700136 private TextDrawable mLabel;
Michael Kolb3daa3512013-04-02 16:03:04 -0700137
138
Michael Kolb8872c232013-01-29 10:33:22 -0800139
140 private Handler mHandler = new Handler() {
141 public void handleMessage(Message msg) {
142 switch(msg.what) {
143 case MSG_OPEN:
144 if (mListener != null) {
Michael Kolb3bc96b22013-03-12 10:24:42 -0700145 mListener.onPieOpened(mPieCenterX, mPieCenterY);
Michael Kolb8872c232013-01-29 10:33:22 -0800146 }
147 break;
148 case MSG_CLOSE:
149 if (mListener != null) {
150 mListener.onPieClosed();
151 }
152 break;
Michael Kolb3bc96b22013-03-12 10:24:42 -0700153 case MSG_OPENSUBMENU:
154 onEnterOpen();
155 break;
Michael Kolb8872c232013-01-29 10:33:22 -0800156 }
Michael Kolb3bc96b22013-03-12 10:24:42 -0700157
Michael Kolb8872c232013-01-29 10:33:22 -0800158 }
159 };
160
161 private PieListener mListener;
162
163 static public interface PieListener {
164 public void onPieOpened(int centerX, int centerY);
165 public void onPieClosed();
166 }
167
168 public void setPieListener(PieListener pl) {
169 mListener = pl;
170 }
171
172 public PieRenderer(Context context) {
173 init(context);
174 }
175
176 private void init(Context ctx) {
177 setVisible(false);
Michael Kolb3bc96b22013-03-12 10:24:42 -0700178 mOpen = new ArrayList<PieItem>();
179 mOpen.add(new PieItem(null, 0));
Michael Kolb8872c232013-01-29 10:33:22 -0800180 Resources res = ctx.getResources();
181 mRadius = (int) res.getDimensionPixelSize(R.dimen.pie_radius_start);
Michael Kolb3daa3512013-04-02 16:03:04 -0700182 mRadiusInc = (int) res.getDimensionPixelSize(R.dimen.pie_radius_increment);
Michael Kolb8872c232013-01-29 10:33:22 -0800183 mCircleSize = mRadius - res.getDimensionPixelSize(R.dimen.focus_radius_offset);
Michael Kolb8872c232013-01-29 10:33:22 -0800184 mTouchOffset = (int) res.getDimensionPixelSize(R.dimen.pie_touch_offset);
Michael Kolb8872c232013-01-29 10:33:22 -0800185 mSelectedPaint = new Paint();
186 mSelectedPaint.setColor(Color.argb(255, 51, 181, 229));
187 mSelectedPaint.setAntiAlias(true);
188 mSubPaint = new Paint();
189 mSubPaint.setAntiAlias(true);
190 mSubPaint.setColor(Color.argb(200, 250, 230, 128));
191 mFocusPaint = new Paint();
192 mFocusPaint.setAntiAlias(true);
193 mFocusPaint.setColor(Color.WHITE);
194 mFocusPaint.setStyle(Paint.Style.STROKE);
195 mSuccessColor = Color.GREEN;
196 mFailColor = Color.RED;
197 mCircle = new RectF();
198 mDial = new RectF();
199 mPoint1 = new Point();
200 mPoint2 = new Point();
201 mInnerOffset = res.getDimensionPixelSize(R.dimen.focus_inner_offset);
202 mOuterStroke = res.getDimensionPixelSize(R.dimen.focus_outer_stroke);
203 mInnerStroke = res.getDimensionPixelSize(R.dimen.focus_inner_stroke);
204 mState = STATE_IDLE;
205 mBlockFocus = false;
206 mTouchSlopSquared = ViewConfiguration.get(ctx).getScaledTouchSlop();
207 mTouchSlopSquared = mTouchSlopSquared * mTouchSlopSquared;
208 mDown = new Point();
Michael Kolb3bc96b22013-03-12 10:24:42 -0700209 mMenuArcPaint = new Paint();
210 mMenuArcPaint.setAntiAlias(true);
211 mMenuArcPaint.setColor(Color.argb(140, 255, 255, 255));
212 mMenuArcPaint.setStrokeWidth(10);
213 mMenuArcPaint.setStyle(Paint.Style.STROKE);
Michael Kolb3daa3512013-04-02 16:03:04 -0700214 mSliceRadius = res.getDimensionPixelSize(R.dimen.pie_item_radius);
Michael Kolb3bc96b22013-03-12 10:24:42 -0700215 mArcRadius = res.getDimensionPixelSize(R.dimen.pie_arc_radius);
216 mArcOffset = res.getDimensionPixelSize(R.dimen.pie_arc_offset);
Michael Kolb10f4ba02013-04-10 08:50:51 -0700217 mLabel = new TextDrawable(res);
Michael Kolb3bc96b22013-03-12 10:24:42 -0700218 }
219
220 private PieItem getRoot() {
221 return mOpen.get(0);
Michael Kolb8872c232013-01-29 10:33:22 -0800222 }
223
224 public boolean showsItems() {
225 return mTapMode;
226 }
227
228 public void addItem(PieItem item) {
229 // add the item to the pie itself
Michael Kolb3bc96b22013-03-12 10:24:42 -0700230 getRoot().addItem(item);
Michael Kolb8872c232013-01-29 10:33:22 -0800231 }
232
233 public void clearItems() {
Michael Kolb3bc96b22013-03-12 10:24:42 -0700234 getRoot().clearItems();
Michael Kolb8872c232013-01-29 10:33:22 -0800235 }
236
237 public void showInCenter() {
238 if ((mState == STATE_PIE) && isVisible()) {
239 mTapMode = false;
240 show(false);
241 } else {
242 if (mState != STATE_IDLE) {
243 cancelFocus();
244 }
245 mState = STATE_PIE;
Michael Kolb3bc96b22013-03-12 10:24:42 -0700246 resetPieCenter();
247 setCenter(mPieCenterX, mPieCenterY);
Michael Kolb8872c232013-01-29 10:33:22 -0800248 mTapMode = true;
249 show(true);
250 }
251 }
252
253 public void hide() {
254 show(false);
255 }
256
257 /**
258 * guaranteed has center set
259 * @param show
260 */
261 private void show(boolean show) {
262 if (show) {
263 mState = STATE_PIE;
264 // ensure clean state
265 mCurrentItem = null;
Michael Kolb3bc96b22013-03-12 10:24:42 -0700266 PieItem root = getRoot();
267 for (PieItem openItem : mOpen) {
268 if (openItem.hasItems()) {
269 for (PieItem item : openItem.getItems()) {
270 item.setSelected(false);
271 }
272 }
Michael Kolb8872c232013-01-29 10:33:22 -0800273 }
Michael Kolb10f4ba02013-04-10 08:50:51 -0700274 mLabel.setText("");
Michael Kolb3bc96b22013-03-12 10:24:42 -0700275 mOpen.clear();
276 mOpen.add(root);
Michael Kolb8872c232013-01-29 10:33:22 -0800277 layoutPie();
278 fadeIn();
279 } else {
280 mState = STATE_IDLE;
281 mTapMode = false;
282 if (mXFade != null) {
283 mXFade.cancel();
284 }
285 }
286 setVisible(show);
287 mHandler.sendEmptyMessage(show ? MSG_OPEN : MSG_CLOSE);
288 }
289
290 private void fadeIn() {
291 mFadeIn = new LinearAnimation(0, 1);
292 mFadeIn.setDuration(PIE_FADE_IN_DURATION);
293 mFadeIn.setAnimationListener(new AnimationListener() {
294 @Override
295 public void onAnimationStart(Animation animation) {
296 }
297
298 @Override
299 public void onAnimationEnd(Animation animation) {
300 mFadeIn = null;
301 }
302
303 @Override
304 public void onAnimationRepeat(Animation animation) {
305 }
306 });
307 mFadeIn.startNow();
308 mOverlay.startAnimation(mFadeIn);
309 }
310
311 public void setCenter(int x, int y) {
Michael Kolb3bc96b22013-03-12 10:24:42 -0700312 mPieCenterX = x;
313 mPieCenterY = y;
Michael Kolb3daa3512013-04-02 16:03:04 -0700314 mSliceCenterY = y - mArcOffset + mSliceRadius;
315 mArcCenterY = y - mArcOffset + mArcRadius;
Michael Kolb3bc96b22013-03-12 10:24:42 -0700316 }
317
318 @Override
319 public void layout(int l, int t, int r, int b) {
320 super.layout(l, t, r, b);
321 mCenterX = (r - l) / 2;
322 mCenterY = (b - t) / 2;
323
324 mFocusX = mCenterX;
325 mFocusY = mCenterY;
326 resetPieCenter();
327 setCircle(mFocusX, mFocusY);
328 if (isVisible() && mState == STATE_PIE) {
329 setCenter(mPieCenterX, mPieCenterY);
330 layoutPie();
331 }
332 }
333
334 private void resetPieCenter() {
335 mPieCenterX = mCenterX;
336 mPieCenterY = mCenterY + mCenterY / 3;
Michael Kolb8872c232013-01-29 10:33:22 -0800337 }
338
339 private void layoutPie() {
Michael Kolb3daa3512013-04-02 16:03:04 -0700340 layoutItems(0, getRoot().getItems());
Michael Kolb10f4ba02013-04-10 08:50:51 -0700341 layoutLabel(0);
342 }
343
344 private void layoutLabel(int level) {
345 int x = mPieCenterX;
346 int y = mArcCenterY - mArcRadius - (level + 2) * mRadiusInc;
347 int w = mLabel.getIntrinsicWidth();
348 int h = mLabel.getIntrinsicHeight();
349 mLabel.setBounds(x - w/2, y - h/2, x + w/2, y + h/2);
Michael Kolb8872c232013-01-29 10:33:22 -0800350 }
351
Michael Kolb3daa3512013-04-02 16:03:04 -0700352 private void layoutItems(int level, List<PieItem> items) {
353 int extend = 1;
354 Path path = makeSlice(getDegrees(0) + extend, getDegrees(SWEEP_ARC) - extend,
355 mArcRadius, mArcRadius + mRadiusInc + mRadiusInc / 4,
356 mPieCenterX, mArcCenterY - level * mRadiusInc);
Michael Kolb8872c232013-01-29 10:33:22 -0800357 for (PieItem item : items) {
358 // shared between items
359 item.setPath(path);
Michael Kolb3daa3512013-04-02 16:03:04 -0700360 float angle = getArcCenter(item);
Michael Kolb8872c232013-01-29 10:33:22 -0800361 int w = item.getIntrinsicWidth();
362 int h = item.getIntrinsicHeight();
363 // move views to outer border
Michael Kolb3daa3512013-04-02 16:03:04 -0700364 int r = mArcRadius + mRadiusInc * 2 / 3;
Michael Kolb8872c232013-01-29 10:33:22 -0800365 int x = (int) (r * Math.cos(angle));
Michael Kolb3daa3512013-04-02 16:03:04 -0700366 int y = mArcCenterY - (level * mRadiusInc) - (int) (r * Math.sin(angle)) - h / 2;
367 x = mPieCenterX + x - w / 2;
Michael Kolb8872c232013-01-29 10:33:22 -0800368 item.setBounds(x, y, x + w, y + h);
Michael Kolb3daa3512013-04-02 16:03:04 -0700369 item.setLevel(level);
Michael Kolb8872c232013-01-29 10:33:22 -0800370 if (item.hasItems()) {
Michael Kolb3daa3512013-04-02 16:03:04 -0700371 layoutItems(level + 1, item.getItems());
Michael Kolb8872c232013-01-29 10:33:22 -0800372 }
Michael Kolb8872c232013-01-29 10:33:22 -0800373 }
374 }
375
Michael Kolb3daa3512013-04-02 16:03:04 -0700376 private Path makeSlice(float start, float end, int inner, int outer, int cx, int cy) {
Michael Kolb8872c232013-01-29 10:33:22 -0800377 RectF bb =
Michael Kolb3daa3512013-04-02 16:03:04 -0700378 new RectF(cx - outer, cy - outer, cx + outer,
379 cy + outer);
Michael Kolb8872c232013-01-29 10:33:22 -0800380 RectF bbi =
Michael Kolb3daa3512013-04-02 16:03:04 -0700381 new RectF(cx - inner, cy - inner, cx + inner,
382 cy + inner);
Michael Kolb8872c232013-01-29 10:33:22 -0800383 Path path = new Path();
384 path.arcTo(bb, start, end - start, true);
385 path.arcTo(bbi, end, start - end);
386 path.close();
387 return path;
388 }
389
Michael Kolb3daa3512013-04-02 16:03:04 -0700390 private float getArcCenter(PieItem item) {
391 return getCenter(item.getPosition(), item.getCount(), SWEEP_ARC);
392 }
393
394 private float getSliceCenter(PieItem item) {
395 return getCenter(item.getPosition(), item.getCount(), SWEEP_SLICE);
396 }
397
398 private float getCenter(int pos, int count, float sweep) {
399 return CENTER + (count - 1) * sweep / 2f - pos * sweep;
400 }
401
Michael Kolb8872c232013-01-29 10:33:22 -0800402 /**
403 * converts a
404 * @param angle from 0..PI to Android degrees (clockwise starting at 3 o'clock)
405 * @return skia angle
406 */
407 private float getDegrees(double angle) {
408 return (float) (360 - 180 * angle / Math.PI);
409 }
410
Michael Kolbebcf6fe2013-02-27 11:00:31 -0800411 private void startFadeOut(final PieItem item) {
412 if (mFadeIn != null) {
413 mFadeIn.cancel();
Michael Kolb8872c232013-01-29 10:33:22 -0800414 }
Michael Kolbebcf6fe2013-02-27 11:00:31 -0800415 if (mXFade != null) {
416 mXFade.cancel();
417 }
418 mFadeOut = new FadeOutAnimation();
419 mFadeOut.setDuration(PIE_FADE_OUT_DURATION);
420 mFadeOut.setAnimationListener(new AnimationListener() {
421 @Override
422 public void onAnimationStart(Animation animation) {
423 }
424
425 @Override
426 public void onAnimationEnd(Animation animation) {
427 item.performClick();
428 mFadeOut = null;
429 deselect();
430 show(false);
431 mOverlay.setAlpha(1);
432 }
433
434 @Override
435 public void onAnimationRepeat(Animation animation) {
436 }
437 });
438 mFadeOut.startNow();
439 mOverlay.startAnimation(mFadeOut);
Michael Kolb8872c232013-01-29 10:33:22 -0800440 }
441
Michael Kolb3bc96b22013-03-12 10:24:42 -0700442 // root does not count
443 private boolean hasOpenItem() {
444 return mOpen.size() > 1;
445 }
446
447 // pop an item of the open item stack
448 private PieItem closeOpenItem() {
449 PieItem item = getOpenItem();
450 mOpen.remove(mOpen.size() -1);
451 return item;
452 }
453
454 private PieItem getOpenItem() {
455 return mOpen.get(mOpen.size() - 1);
456 }
457
458 // return the children either the root or parent of the current open item
459 private PieItem getParent() {
460 return mOpen.get(Math.max(0, mOpen.size() - 2));
461 }
462
463 private int getLevel() {
464 return mOpen.size() - 1;
465 }
466
Michael Kolb8872c232013-01-29 10:33:22 -0800467 @Override
468 public void onDraw(Canvas canvas) {
469 float alpha = 1;
470 if (mXFade != null) {
471 alpha = mXFade.getValue();
472 } else if (mFadeIn != null) {
473 alpha = mFadeIn.getValue();
Michael Kolbebcf6fe2013-02-27 11:00:31 -0800474 } else if (mFadeOut != null) {
475 alpha = mFadeOut.getValue();
Michael Kolb8872c232013-01-29 10:33:22 -0800476 }
477 int state = canvas.save();
478 if (mFadeIn != null) {
479 float sf = 0.9f + alpha * 0.1f;
Michael Kolb3bc96b22013-03-12 10:24:42 -0700480 canvas.scale(sf, sf, mPieCenterX, mPieCenterY);
Michael Kolb8872c232013-01-29 10:33:22 -0800481 }
Michael Kolb3bc96b22013-03-12 10:24:42 -0700482 if (mState != STATE_PIE) {
483 drawFocus(canvas);
484 }
Michael Kolb8872c232013-01-29 10:33:22 -0800485 if (mState == STATE_FINISHING) {
486 canvas.restoreToCount(state);
487 return;
488 }
Michael Kolb3bc96b22013-03-12 10:24:42 -0700489 if (!hasOpenItem() || (mXFade != null)) {
Michael Kolb8872c232013-01-29 10:33:22 -0800490 // draw base menu
Michael Kolb3daa3512013-04-02 16:03:04 -0700491 drawArc(canvas, getLevel(), getParent());
Michael Kolb3bc96b22013-03-12 10:24:42 -0700492 for (PieItem item : getParent().getItems()) {
493 drawItem(Math.max(0, mOpen.size() - 2), canvas, item, alpha);
Michael Kolb8872c232013-01-29 10:33:22 -0800494 }
Michael Kolb10f4ba02013-04-10 08:50:51 -0700495 mLabel.draw(canvas);
Michael Kolb8872c232013-01-29 10:33:22 -0800496 }
Michael Kolb3bc96b22013-03-12 10:24:42 -0700497 if (hasOpenItem()) {
498 int level = getLevel();
Michael Kolb3daa3512013-04-02 16:03:04 -0700499 drawArc(canvas, level, getOpenItem());
Michael Kolb3bc96b22013-03-12 10:24:42 -0700500 for (PieItem inner : getOpenItem().getItems()) {
Michael Kolbebcf6fe2013-02-27 11:00:31 -0800501 if (mFadeOut != null) {
Michael Kolb3bc96b22013-03-12 10:24:42 -0700502 drawItem(level, canvas, inner, alpha);
Michael Kolbebcf6fe2013-02-27 11:00:31 -0800503 } else {
Michael Kolb3bc96b22013-03-12 10:24:42 -0700504 drawItem(level, canvas, inner, (mXFade != null) ? (1 - 0.5f * alpha) : 1);
Michael Kolbebcf6fe2013-02-27 11:00:31 -0800505 }
Michael Kolb8872c232013-01-29 10:33:22 -0800506 }
Michael Kolb10f4ba02013-04-10 08:50:51 -0700507 mLabel.draw(canvas);
Michael Kolb8872c232013-01-29 10:33:22 -0800508 }
509 canvas.restoreToCount(state);
510 }
511
Michael Kolb3daa3512013-04-02 16:03:04 -0700512 private void drawArc(Canvas canvas, int level, PieItem item) {
Michael Kolb3bc96b22013-03-12 10:24:42 -0700513 // arc
514 if (mState == STATE_PIE) {
Michael Kolb3daa3512013-04-02 16:03:04 -0700515 int min = Integer.MAX_VALUE;
516 int max = Integer.MIN_VALUE;
517 int count = 0;
518 for (PieItem child : item.getItems()) {
519 final int p = child.getPosition();
520 count = child.getCount();
521 if (p < min) min = p;
522 if (p > max) max = p;
523 }
524 float start = CENTER + (count - 1) * SWEEP_ARC / 2f - min * SWEEP_ARC + SWEEP_ARC / 2f;
525 float end = CENTER + (count - 1) * SWEEP_ARC / 2f - max * SWEEP_ARC - SWEEP_ARC / 2f;
526 int cy = mArcCenterY - level * mRadiusInc;
527 canvas.drawArc(new RectF(mPieCenterX - mArcRadius, cy - mArcRadius,
528 mPieCenterX + mArcRadius, cy + mArcRadius),
529 getDegrees(end), getDegrees(start) - getDegrees(end), false, mMenuArcPaint);
Michael Kolb3bc96b22013-03-12 10:24:42 -0700530 }
531 }
Michael Kolb3daa3512013-04-02 16:03:04 -0700532
Michael Kolb3bc96b22013-03-12 10:24:42 -0700533 private void drawItem(int level, Canvas canvas, PieItem item, float alpha) {
Michael Kolb8872c232013-01-29 10:33:22 -0800534 if (mState == STATE_PIE) {
535 if (item.getPath() != null) {
Michael Kolb3daa3512013-04-02 16:03:04 -0700536 int y = mArcCenterY - level * mRadiusInc;
Michael Kolb8872c232013-01-29 10:33:22 -0800537 if (item.isSelected()) {
538 Paint p = mSelectedPaint;
539 int state = canvas.save();
Michael Kolb3daa3512013-04-02 16:03:04 -0700540 float angle = getArcCenter(item) - SWEEP_ARC / 2f;
541 angle = getDegrees(angle);
542 canvas.rotate(angle, mPieCenterX, y);
Michael Kolbebcf6fe2013-02-27 11:00:31 -0800543 if (mFadeOut != null) {
544 p.setAlpha((int)(255 * alpha));
545 }
Michael Kolb8872c232013-01-29 10:33:22 -0800546 canvas.drawPath(item.getPath(), p);
Michael Kolbebcf6fe2013-02-27 11:00:31 -0800547 if (mFadeOut != null) {
548 p.setAlpha(255);
549 }
Michael Kolb8872c232013-01-29 10:33:22 -0800550 canvas.restoreToCount(state);
551 }
Michael Kolbebcf6fe2013-02-27 11:00:31 -0800552 if (mFadeOut == null) {
553 alpha = alpha * (item.isEnabled() ? 1 : 0.3f);
554 // draw the item view
555 item.setAlpha(alpha);
556 }
Michael Kolb8872c232013-01-29 10:33:22 -0800557 item.draw(canvas);
558 }
559 }
560 }
561
562 @Override
563 public boolean onTouchEvent(MotionEvent evt) {
564 float x = evt.getX();
565 float y = evt.getY();
566 int action = evt.getActionMasked();
Michael Kolb3daa3512013-04-02 16:03:04 -0700567 getPolar(x, y, !mTapMode, mPolar);
Michael Kolb8872c232013-01-29 10:33:22 -0800568 if (MotionEvent.ACTION_DOWN == action) {
569 mDown.x = (int) evt.getX();
570 mDown.y = (int) evt.getY();
571 mOpening = false;
572 if (mTapMode) {
Michael Kolb3daa3512013-04-02 16:03:04 -0700573 PieItem item = findItem(mPolar);
Michael Kolb8872c232013-01-29 10:33:22 -0800574 if ((item != null) && (mCurrentItem != item)) {
575 mState = STATE_PIE;
576 onEnter(item);
577 }
578 } else {
579 setCenter((int) x, (int) y);
580 show(true);
581 }
582 return true;
583 } else if (MotionEvent.ACTION_UP == action) {
584 if (isVisible()) {
585 PieItem item = mCurrentItem;
586 if (mTapMode) {
Michael Kolb3daa3512013-04-02 16:03:04 -0700587 item = findItem(mPolar);
Michael Kolb3bc96b22013-03-12 10:24:42 -0700588 if (mOpening) {
Michael Kolb8872c232013-01-29 10:33:22 -0800589 mOpening = false;
590 return true;
591 }
592 }
593 if (item == null) {
594 mTapMode = false;
595 show(false);
Michael Kolb3bc96b22013-03-12 10:24:42 -0700596 } else if (!mOpening && !item.hasItems()) {
597 startFadeOut(item);
598 mTapMode = false;
599 } else {
600 mTapMode = true;
Michael Kolb8872c232013-01-29 10:33:22 -0800601 }
602 return true;
603 }
604 } else if (MotionEvent.ACTION_CANCEL == action) {
605 if (isVisible() || mTapMode) {
606 show(false);
607 }
608 deselect();
Michael Kolb3bc96b22013-03-12 10:24:42 -0700609 mHandler.removeMessages(MSG_OPENSUBMENU);
Michael Kolb8872c232013-01-29 10:33:22 -0800610 return false;
611 } else if (MotionEvent.ACTION_MOVE == action) {
Michael Kolb3daa3512013-04-02 16:03:04 -0700612 if (pulledToCenter(mPolar)) {
Michael Kolb3bc96b22013-03-12 10:24:42 -0700613 mHandler.removeMessages(MSG_OPENSUBMENU);
614 if (hasOpenItem()) {
615 if (mCurrentItem != null) {
616 mCurrentItem.setSelected(false);
617 }
618 closeOpenItem();
619 mCurrentItem = null;
Michael Kolb8872c232013-01-29 10:33:22 -0800620 } else {
621 deselect();
622 }
Michael Kolb10f4ba02013-04-10 08:50:51 -0700623 mLabel.setText("");
Michael Kolb8872c232013-01-29 10:33:22 -0800624 return false;
625 }
Michael Kolb3daa3512013-04-02 16:03:04 -0700626 PieItem item = findItem(mPolar);
Michael Kolb8872c232013-01-29 10:33:22 -0800627 boolean moved = hasMoved(evt);
628 if ((item != null) && (mCurrentItem != item) && (!mOpening || moved)) {
Michael Kolb3bc96b22013-03-12 10:24:42 -0700629 mHandler.removeMessages(MSG_OPENSUBMENU);
Michael Kolb8872c232013-01-29 10:33:22 -0800630 // only select if we didn't just open or have moved past slop
Michael Kolb8872c232013-01-29 10:33:22 -0800631 if (moved) {
632 // switch back to swipe mode
633 mTapMode = false;
634 }
Michael Kolb3bc96b22013-03-12 10:24:42 -0700635 onEnterSelect(item);
636 mHandler.sendEmptyMessageDelayed(MSG_OPENSUBMENU, PIE_OPEN_SUB_DELAY);
Michael Kolb8872c232013-01-29 10:33:22 -0800637 }
638 }
639 return false;
640 }
641
Michael Kolb3bc96b22013-03-12 10:24:42 -0700642 private boolean pulledToCenter(PointF polarCoords) {
Michael Kolb3daa3512013-04-02 16:03:04 -0700643 return polarCoords.y < mArcRadius - mRadiusInc;
Michael Kolb3bc96b22013-03-12 10:24:42 -0700644 }
645
Michael Kolb3daa3512013-04-02 16:03:04 -0700646 private boolean inside(PointF polar, PieItem item) {
647 float start = getSliceCenter(item) - SWEEP_SLICE / 2f;
648 boolean res = (mArcRadius < polar.y)
649 && (start < polar.x)
650 && (start + SWEEP_SLICE > polar.x)
651 && (!mTapMode || (mArcRadius + mRadiusInc > polar.y));
652 return res;
653 }
654
655 private void getPolar(float x, float y, boolean useOffset, PointF res) {
Michael Kolb3bc96b22013-03-12 10:24:42 -0700656 // get angle and radius from x/y
657 res.x = (float) Math.PI / 2;
Michael Kolb3daa3512013-04-02 16:03:04 -0700658 x = x - mPieCenterX;
659 float y1 = mSliceCenterY - getLevel() * mRadiusInc - y;
660 float y2 = mArcCenterY - getLevel() * mRadiusInc - y;
661 res.y = (float) Math.sqrt(x * x + y2 * y2);
Michael Kolb3bc96b22013-03-12 10:24:42 -0700662 if (x != 0) {
Michael Kolb3daa3512013-04-02 16:03:04 -0700663 res.x = (float) Math.atan2(y1, x);
Michael Kolb3bc96b22013-03-12 10:24:42 -0700664 if (res.x < 0) {
665 res.x = (float) (2 * Math.PI + res.x);
666 }
667 }
668 res.y = res.y + (useOffset ? mTouchOffset : 0);
Michael Kolb3bc96b22013-03-12 10:24:42 -0700669 }
670
Michael Kolb8872c232013-01-29 10:33:22 -0800671 private boolean hasMoved(MotionEvent e) {
672 return mTouchSlopSquared < (e.getX() - mDown.x) * (e.getX() - mDown.x)
673 + (e.getY() - mDown.y) * (e.getY() - mDown.y);
674 }
675
Michael Kolb3bc96b22013-03-12 10:24:42 -0700676 private void onEnterSelect(PieItem item) {
677 if (mCurrentItem != null) {
678 mCurrentItem.setSelected(false);
679 }
680 if (item != null && item.isEnabled()) {
681 item.setSelected(true);
682 mCurrentItem = item;
Michael Kolb10f4ba02013-04-10 08:50:51 -0700683 mLabel.setText(mCurrentItem.getLabel());
684 layoutLabel(getLevel());
Michael Kolb3bc96b22013-03-12 10:24:42 -0700685 } else {
686 mCurrentItem = null;
687 }
688 }
689
690 private void onEnterOpen() {
Michael Kolbdc7d1ac2013-04-12 08:45:25 -0700691 if ((mCurrentItem != null) && (mCurrentItem != getOpenItem()) && mCurrentItem.hasItems()) {
Michael Kolb3bc96b22013-03-12 10:24:42 -0700692 openCurrentItem();
693 }
694 }
695
Michael Kolb8872c232013-01-29 10:33:22 -0800696 /**
697 * enter a slice for a view
698 * updates model only
699 * @param item
700 */
701 private void onEnter(PieItem item) {
702 if (mCurrentItem != null) {
703 mCurrentItem.setSelected(false);
704 }
705 if (item != null && item.isEnabled()) {
706 item.setSelected(true);
707 mCurrentItem = item;
Michael Kolb3bc96b22013-03-12 10:24:42 -0700708 if ((mCurrentItem != getOpenItem()) && mCurrentItem.hasItems()) {
Michael Kolb8872c232013-01-29 10:33:22 -0800709 openCurrentItem();
Michael Kolb10f4ba02013-04-10 08:50:51 -0700710 layoutLabel(getLevel());
Michael Kolb8872c232013-01-29 10:33:22 -0800711 }
712 } else {
713 mCurrentItem = null;
714 }
715 }
716
717 private void deselect() {
718 if (mCurrentItem != null) {
719 mCurrentItem.setSelected(false);
720 }
Michael Kolb3bc96b22013-03-12 10:24:42 -0700721 if (hasOpenItem()) {
722 PieItem item = closeOpenItem();
723 onEnter(item);
724 } else {
725 mCurrentItem = null;
Michael Kolb8872c232013-01-29 10:33:22 -0800726 }
Michael Kolb8872c232013-01-29 10:33:22 -0800727 }
728
729 private void openCurrentItem() {
730 if ((mCurrentItem != null) && mCurrentItem.hasItems()) {
Michael Kolb3bc96b22013-03-12 10:24:42 -0700731 mOpen.add(mCurrentItem);
Michael Kolb8872c232013-01-29 10:33:22 -0800732 mOpening = true;
Michael Kolbebcf6fe2013-02-27 11:00:31 -0800733 if (mFadeIn != null) {
734 mFadeIn.cancel();
735 }
Michael Kolb8872c232013-01-29 10:33:22 -0800736 mXFade = new LinearAnimation(1, 0);
737 mXFade.setDuration(PIE_XFADE_DURATION);
Michael Kolb3bc96b22013-03-12 10:24:42 -0700738 final PieItem ci = mCurrentItem;
Michael Kolb8872c232013-01-29 10:33:22 -0800739 mXFade.setAnimationListener(new AnimationListener() {
740 @Override
741 public void onAnimationStart(Animation animation) {
742 }
743
744 @Override
745 public void onAnimationEnd(Animation animation) {
746 mXFade = null;
Michael Kolb3bc96b22013-03-12 10:24:42 -0700747 ci.setSelected(false);
748 mOpening = false;
Michael Kolb10f4ba02013-04-10 08:50:51 -0700749 mLabel.setText("");
Michael Kolb8872c232013-01-29 10:33:22 -0800750 }
751
752 @Override
753 public void onAnimationRepeat(Animation animation) {
754 }
755 });
756 mXFade.startNow();
757 mOverlay.startAnimation(mXFade);
758 }
759 }
760
Michael Kolb8872c232013-01-29 10:33:22 -0800761 /**
762 * @param polar x: angle, y: dist
763 * @return the item at angle/dist or null
764 */
765 private PieItem findItem(PointF polar) {
766 // find the matching item:
Michael Kolb3bc96b22013-03-12 10:24:42 -0700767 List<PieItem> items = getOpenItem().getItems();
Michael Kolb8872c232013-01-29 10:33:22 -0800768 for (PieItem item : items) {
769 if (inside(polar, item)) {
770 return item;
771 }
772 }
773 return null;
774 }
775
Michael Kolb8872c232013-01-29 10:33:22 -0800776
777 @Override
778 public boolean handlesTouch() {
779 return true;
780 }
781
782 // focus specific code
783
784 public void setBlockFocus(boolean blocked) {
785 mBlockFocus = blocked;
786 if (blocked) {
787 clear();
788 }
789 }
790
791 public void setFocus(int x, int y) {
792 mFocusX = x;
793 mFocusY = y;
794 setCircle(mFocusX, mFocusY);
795 }
796
797 public void alignFocus(int x, int y) {
798 mOverlay.removeCallbacks(mDisappear);
799 mAnimation.cancel();
800 mAnimation.reset();
801 mFocusX = x;
802 mFocusY = y;
803 mDialAngle = DIAL_HORIZONTAL;
804 setCircle(x, y);
805 mFocused = false;
806 }
807
808 public int getSize() {
809 return 2 * mCircleSize;
810 }
811
812 private int getRandomRange() {
813 return (int)(-60 + 120 * Math.random());
814 }
815
Michael Kolb8872c232013-01-29 10:33:22 -0800816 private void setCircle(int cx, int cy) {
817 mCircle.set(cx - mCircleSize, cy - mCircleSize,
818 cx + mCircleSize, cy + mCircleSize);
819 mDial.set(cx - mCircleSize + mInnerOffset, cy - mCircleSize + mInnerOffset,
820 cx + mCircleSize - mInnerOffset, cy + mCircleSize - mInnerOffset);
821 }
822
823 public void drawFocus(Canvas canvas) {
824 if (mBlockFocus) return;
825 mFocusPaint.setStrokeWidth(mOuterStroke);
826 canvas.drawCircle((float) mFocusX, (float) mFocusY, (float) mCircleSize, mFocusPaint);
827 if (mState == STATE_PIE) return;
828 int color = mFocusPaint.getColor();
829 if (mState == STATE_FINISHING) {
830 mFocusPaint.setColor(mFocused ? mSuccessColor : mFailColor);
831 }
832 mFocusPaint.setStrokeWidth(mInnerStroke);
833 drawLine(canvas, mDialAngle, mFocusPaint);
834 drawLine(canvas, mDialAngle + 45, mFocusPaint);
835 drawLine(canvas, mDialAngle + 180, mFocusPaint);
836 drawLine(canvas, mDialAngle + 225, mFocusPaint);
837 canvas.save();
838 // rotate the arc instead of its offset to better use framework's shape caching
839 canvas.rotate(mDialAngle, mFocusX, mFocusY);
840 canvas.drawArc(mDial, 0, 45, false, mFocusPaint);
841 canvas.drawArc(mDial, 180, 45, false, mFocusPaint);
842 canvas.restore();
843 mFocusPaint.setColor(color);
844 }
845
846 private void drawLine(Canvas canvas, int angle, Paint p) {
847 convertCart(angle, mCircleSize - mInnerOffset, mPoint1);
848 convertCart(angle, mCircleSize - mInnerOffset + mInnerOffset / 3, mPoint2);
849 canvas.drawLine(mPoint1.x + mFocusX, mPoint1.y + mFocusY,
850 mPoint2.x + mFocusX, mPoint2.y + mFocusY, p);
851 }
852
853 private static void convertCart(int angle, int radius, Point out) {
854 double a = 2 * Math.PI * (angle % 360) / 360;
855 out.x = (int) (radius * Math.cos(a) + 0.5);
856 out.y = (int) (radius * Math.sin(a) + 0.5);
857 }
858
859 @Override
860 public void showStart() {
861 if (mState == STATE_PIE) return;
862 cancelFocus();
863 mStartAnimationAngle = 67;
864 int range = getRandomRange();
865 startAnimation(SCALING_UP_TIME,
866 false, mStartAnimationAngle, mStartAnimationAngle + range);
867 mState = STATE_FOCUSING;
868 }
869
870 @Override
871 public void showSuccess(boolean timeout) {
872 if (mState == STATE_FOCUSING) {
873 startAnimation(SCALING_DOWN_TIME,
874 timeout, mStartAnimationAngle);
875 mState = STATE_FINISHING;
876 mFocused = true;
877 }
878 }
879
880 @Override
881 public void showFail(boolean timeout) {
882 if (mState == STATE_FOCUSING) {
883 startAnimation(SCALING_DOWN_TIME,
884 timeout, mStartAnimationAngle);
885 mState = STATE_FINISHING;
886 mFocused = false;
887 }
888 }
889
890 private void cancelFocus() {
891 mFocusCancelled = true;
892 mOverlay.removeCallbacks(mDisappear);
Michael Kolbe3de7222013-02-18 15:16:44 -0800893 if (mAnimation != null && !mAnimation.hasEnded()) {
Michael Kolb8872c232013-01-29 10:33:22 -0800894 mAnimation.cancel();
895 }
896 mFocusCancelled = false;
897 mFocused = false;
898 mState = STATE_IDLE;
899 }
900
901 @Override
902 public void clear() {
903 if (mState == STATE_PIE) return;
904 cancelFocus();
905 mOverlay.post(mDisappear);
906 }
907
908 private void startAnimation(long duration, boolean timeout,
909 float toScale) {
910 startAnimation(duration, timeout, mDialAngle,
911 toScale);
912 }
913
914 private void startAnimation(long duration, boolean timeout,
915 float fromScale, float toScale) {
916 setVisible(true);
917 mAnimation.reset();
918 mAnimation.setDuration(duration);
919 mAnimation.setScale(fromScale, toScale);
920 mAnimation.setAnimationListener(timeout ? mEndAction : null);
921 mOverlay.startAnimation(mAnimation);
922 update();
923 }
924
925 private class EndAction implements Animation.AnimationListener {
926 @Override
927 public void onAnimationEnd(Animation animation) {
928 // Keep the focus indicator for some time.
929 if (!mFocusCancelled) {
930 mOverlay.postDelayed(mDisappear, DISAPPEAR_TIMEOUT);
931 }
932 }
933
934 @Override
935 public void onAnimationRepeat(Animation animation) {
936 }
937
938 @Override
939 public void onAnimationStart(Animation animation) {
940 }
941 }
942
943 private class Disappear implements Runnable {
944 @Override
945 public void run() {
946 if (mState == STATE_PIE) return;
947 setVisible(false);
948 mFocusX = mCenterX;
949 mFocusY = mCenterY;
950 mState = STATE_IDLE;
951 setCircle(mFocusX, mFocusY);
952 mFocused = false;
953 }
954 }
955
Michael Kolbebcf6fe2013-02-27 11:00:31 -0800956 private class FadeOutAnimation extends Animation {
957
958 private float mAlpha;
959
960 public float getValue() {
961 return mAlpha;
962 }
963
964 @Override
965 protected void applyTransformation(float interpolatedTime, Transformation t) {
966 if (interpolatedTime < 0.2) {
967 mAlpha = 1;
968 } else if (interpolatedTime < 0.3) {
969 mAlpha = 0;
970 } else {
971 mAlpha = 1 - (interpolatedTime - 0.3f) / 0.7f;
972 }
973 }
974 }
975
Michael Kolb8872c232013-01-29 10:33:22 -0800976 private class ScaleAnimation extends Animation {
977 private float mFrom = 1f;
978 private float mTo = 1f;
979
980 public ScaleAnimation() {
981 setFillAfter(true);
982 }
983
984 public void setScale(float from, float to) {
985 mFrom = from;
986 mTo = to;
987 }
988
989 @Override
990 protected void applyTransformation(float interpolatedTime, Transformation t) {
991 mDialAngle = (int)(mFrom + (mTo - mFrom) * interpolatedTime);
992 }
993 }
994
Michael Kolb8872c232013-01-29 10:33:22 -0800995 private class LinearAnimation extends Animation {
996 private float mFrom;
997 private float mTo;
998 private float mValue;
999
1000 public LinearAnimation(float from, float to) {
1001 setFillAfter(true);
1002 setInterpolator(new LinearInterpolator());
1003 mFrom = from;
1004 mTo = to;
1005 }
1006
1007 public float getValue() {
1008 return mValue;
1009 }
1010
1011 @Override
1012 protected void applyTransformation(float interpolatedTime, Transformation t) {
1013 mValue = (mFrom + (mTo - mFrom) * interpolatedTime);
1014 }
1015 }
1016
1017}