From 376b54116e38b3b94c4d64663d1bff38352b0e59 Mon Sep 17 00:00:00 2001 From: Michael Kolb Date: Wed, 15 Dec 2010 11:52:57 -0800 Subject: Add quick controls Bug: http://b/issue?id=3277888 Added Quick Controls Lab setting Implemented Quick Controls UI Change-Id: I72011daf9140aa5d15c8b785126867c10bbc5501 --- src/com/android/browser/view/PieMenu.java | 463 ++++++++++++++++++++++++++++++ 1 file changed, 463 insertions(+) create mode 100644 src/com/android/browser/view/PieMenu.java (limited to 'src/com/android/browser/view') diff --git a/src/com/android/browser/view/PieMenu.java b/src/com/android/browser/view/PieMenu.java new file mode 100644 index 0000000..d838a34 --- /dev/null +++ b/src/com/android/browser/view/PieMenu.java @@ -0,0 +1,463 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.browser.view; + +import com.android.browser.R; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.widget.FrameLayout; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class PieMenu extends FrameLayout { + + private static final int RADIUS_GAP = 10; + + public interface PieController { + /** + * called before menu opens to customize menu + * returns if pie state has been changed + */ + public boolean onOpen(); + } + private Point mCenter; + private int mRadius; + private int mRadiusInc; + private int mSlop; + + private boolean mOpen; + private Paint mPaint; + private Paint mSelectedPaint; + private PieController mController; + + private Map> mMenu; + private List mStack; + + private boolean mDirty; + + /** + * @param context + * @param attrs + * @param defStyle + */ + public PieMenu(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context); + } + + /** + * @param context + * @param attrs + */ + public PieMenu(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + /** + * @param context + */ + public PieMenu(Context context) { + super(context); + init(context); + } + + private void init(Context ctx) { + this.setTag(new MenuTag(0)); + mStack = new ArrayList(); + mStack.add(this); + Resources res = ctx.getResources(); + mRadius = (int) res.getDimension(R.dimen.qc_radius); + mRadiusInc = (int) res.getDimension(R.dimen.qc_radius_inc); + mSlop = (int) res.getDimension(R.dimen.qc_slop); + mPaint = new Paint(); + mPaint.setAntiAlias(true); + mPaint.setColor(res.getColor(R.color.qc_slice_normal)); + mSelectedPaint = new Paint(); + mSelectedPaint.setAntiAlias(true); + mSelectedPaint.setColor(res.getColor(R.color.qc_slice_active)); + mOpen = false; + mMenu = new HashMap>(); + setWillNotDraw(false); + setDrawingCacheEnabled(false); + mCenter = new Point(0,0); + mDirty = true; + } + + public void setController(PieController ctl) { + mController = ctl; + } + + public void setRadius(int r) { + mRadius = r; + requestLayout(); + } + + public void setRadiusIncrement(int ri) { + mRadiusInc = ri; + requestLayout(); + } + + /** + * add a menu item to another item as a submenu + * @param item + * @param parent + */ + public void addItem(View item, View parent) { + List subs = mMenu.get(parent); + if (subs == null) { + subs = new ArrayList(); + mMenu.put(parent, subs); + } + subs.add(item); + MenuTag tag = new MenuTag(((MenuTag) parent.getTag()).level + 1); + item.setTag(tag); + } + + public void addItem(View view) { + // add the item to the pie itself + addItem(view, this); + } + + public void removeItem(View view) { + List subs = mMenu.get(view); + mMenu.remove(view); + for (View p : mMenu.keySet()) { + List sl = mMenu.get(p); + if (sl != null) { + sl.remove(view); + } + } + } + + public void clearItems(View parent) { + List subs = mMenu.remove(parent); + if (subs != null) { + for (View sub: subs) { + clearItems(sub); + } + } + } + + public void clearItems() { + mMenu.clear(); + } + + + public void show(boolean show) { + mOpen = show; + if (mOpen) { + if (mController != null) { + boolean changed = mController.onOpen(); + } + mDirty = true; + } + if (!show) { + // hide sub items + mStack.clear(); + mStack.add(this); + } + invalidate(); + } + + private void setCenter(int x, int y) { + if (x < mSlop) { + mCenter.x = 0; + } else { + mCenter.x = getWidth(); + } + mCenter.y = y; + } + + private boolean onTheLeft() { + return mCenter.x < mSlop; + } + + @Override + protected void onDraw(Canvas canvas) { + if (mOpen) { + int radius = mRadius; + // start in the center for 0 level menu + float anchor = (float) Math.PI / 2; + PointF angles = new PointF(); + int state = canvas.save(); + if (onTheLeft()) { + // left handed + canvas.scale(-1, 1); + } + for (View parent : mStack) { + List subs = mMenu.get(parent); + if (subs != null) { + setGeometry(anchor, subs.size(), angles); + } + anchor = drawSlices(canvas, subs, radius, angles.x, angles.y); + radius += mRadiusInc + RADIUS_GAP; + } + canvas.restoreToCount(state); + mDirty = false; + } + } + + /** + * draw the set of slices + * @param canvas + * @param items + * @param radius + * @param start + * @param sweep + * @return the angle of the selected slice + */ + private float drawSlices(Canvas canvas, List items, int radius, + float start, float sweep) { + float angle = start + sweep / 2; + // gap between slices in degrees + float gap = 1f; + float newanchor = 0f; + for (View item : items) { + if (mDirty) { + item.measure(item.getLayoutParams().width, + item.getLayoutParams().height); + int w = item.getMeasuredWidth(); + int h = item.getMeasuredHeight(); + int x = (int) (radius * Math.sin(angle)); + int y = mCenter.y - (int) (radius * Math.cos(angle)) - h / 2; + if (onTheLeft()) { + x = mCenter.x + x - w / 2; + } else { + x = mCenter.x - x - w / 2; + } + item.layout(x, y, x + w, y + h); + } + float itemstart = angle - sweep / 2; + int inner = radius - mRadiusInc / 2; + int outer = radius + mRadiusInc / 2; + Path slice = makeSlice(getDegrees(itemstart) - gap, + getDegrees(itemstart + sweep) + gap, + outer, inner, mCenter); + MenuTag tag = (MenuTag) item.getTag(); + tag.start = itemstart; + tag.sweep = sweep; + tag.inner = inner; + tag.outer = outer; + + Paint p = item.isPressed() ? mSelectedPaint : mPaint; + canvas.drawPath(slice, p); + int state = canvas.save(); + if (onTheLeft()) { + canvas.scale(-1, 1); + } + canvas.translate(item.getX(), item.getY()); + item.draw(canvas); + canvas.restoreToCount(state); + if (mStack.contains(item)) { + // item is anchor for sub menu + newanchor = angle; + } + angle += sweep; + } + return newanchor; + } + + /** + * converts a + * @param angle from 0..PI to Android degrees (clockwise starting at 3 o'clock) + * @return skia angle + */ + private float getDegrees(double angle) { + return (float) (270 - 180 * angle / Math.PI); + } + + private Path makeSlice(float startangle, float endangle, int outerradius, + int innerradius, Point center) { + RectF bb = new RectF(center.x - outerradius, center.y - outerradius, + center.x + outerradius, center.y + outerradius); + RectF bbi = new RectF(center.x - innerradius, center.y - innerradius, + center.x + innerradius, center.y + innerradius); + Path path = new Path(); + path.arcTo(bb, startangle, endangle - startangle, true); + path.arcTo(bbi, endangle, startangle - endangle); + path.close(); + return path; + } + + /** + * all angles are 0 .. MATH.PI where 0 points up, and rotate counterclockwise + * set the startangle and slice sweep in result + * @param anchorangle : angle at which the menu is anchored + * @param nslices + * @param result : x : start, y : sweep + */ + private void setGeometry(float anchorangle, int nslices, PointF result) { + float span = (float) Math.min(anchorangle, Math.PI - anchorangle); + float sweep = 2 * span / (nslices + 1); + result.x = anchorangle - span + sweep / 2; + result.y = sweep; + } + + // touch handling for pie + + View mCurrentView; + Rect mHitRect = new Rect(); + + @Override + public boolean onTouchEvent(MotionEvent evt) { + float x = evt.getX(); + float y = evt.getY(); + int action = evt.getActionMasked(); + int edges = evt.getEdgeFlags(); + if (MotionEvent.ACTION_DOWN == action) { + if ((x > getWidth() - mSlop) || (x < mSlop)) { + setCenter((int) x, (int) y); + show(true); + return true; + } + } else if (MotionEvent.ACTION_UP == action) { + if (mOpen) { + View v = mCurrentView; + deselect(); + if (v != null) { + v.performClick(); + } + show(false); + return true; + } + } else if (MotionEvent.ACTION_CANCEL == action) { + if (mOpen) { + show(false); + } + deselect(); + return false; + } else if (MotionEvent.ACTION_MOVE == action) { + View v = findView((int) x, (int) y); + if (mCurrentView != v) { + onEnter(v); + invalidate(); + } + } + // always re-dispatch event + return false; + } + + /** + * enter a slice for a view + * updates model only + * @param view + */ + private void onEnter(View view) { + // deselect + if (mCurrentView != null) { + if (getLevel(mCurrentView) >= getLevel(view)) { + mCurrentView.setPressed(false); + } + } + if (view != null) { + // clear up stack + MenuTag tag = (MenuTag) view.getTag(); + int i = mStack.size() - 1; + while (i > 0) { + View v = mStack.get(i); + if (((MenuTag) v.getTag()).level >= tag.level) { + v.setPressed(false); + mStack.remove(i); + } else { + break; + } + i--; + } + List items = mMenu.get(view); + if (items != null) { + mStack.add(view); + mDirty = true; + } + view.setPressed(true); + } + mCurrentView = view; + } + + private void deselect() { + if (mCurrentView != null) { + mCurrentView.setPressed(false); + } + mCurrentView = null; + } + + private int getLevel(View v) { + if (v == null) return -1; + return ((MenuTag) v.getTag()).level; + } + + private View findView(int x, int y) { + // get angle and radius from x/y + float angle = (float) Math.PI / 2; + x = mCenter.x - x; + if (mCenter.x < mSlop) { + x = -x; + } + y = mCenter.y - y; + float dist = (float) Math.sqrt(x * x + y * y); + if (y > 0) { + angle = (float) Math.asin(x / dist); + } else if (y < 0) { + angle = (float) (Math.PI - Math.asin(x / dist )); + } + // find the matching item: + for (View parent : mStack) { + List subs = mMenu.get(parent); + if (subs != null) { + for (View item : subs) { + MenuTag tag = (MenuTag) item.getTag(); + if ((tag.inner < dist) + && (tag.outer > dist) + && (tag.start < angle) + && (tag.start + tag.sweep > angle)) { + return item; + } + } + } + } + return null; + } + + class MenuTag { + + int level; + float start; + float sweep; + int inner; + int outer; + + public MenuTag(int l) { + level = l; + } + + } + +} -- cgit v1.1