今天分享一下PieMenu的实现, 可以理解为在一个Framelayout上绘制一个PieMenu: 代码在PieMenu.java中
其添加到窗口的代码在 PieControlBase中
protected void attachToContainer(FrameLayout container) { if (mPie == null) { mPie = new PieMenu(mActivity); LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); mPie.setLayoutParams(lp); populateMenu();//添加pieitem mPie.setController(this); } container.addView(mPie); }
我们知道这个PieMenu是当用户按住屏幕边缘的时候出现的, 也就是是说应该在Touch的时候show了PieMenu:
看他的Action_down的时候:
if (MotionEvent.ACTION_DOWN == action) {//用户按下屏幕 if ((x > getWidth() - mSlop) || (x < mSlop)) {//判断发现需要显示PieMenu, 于是执行show setCenter((int) x, (int) y); //设置pie的位置 show(true); return true;//返回true表示将来的move 和up事件都用这个view来处理不用dispatch和intercept了. } }
通过show函数就可以把整个Menu显示出来了:
/** * guaranteed has center set * @param show * 显示piemenu */ private void show(boolean show) { mOpen = show; if (mOpen) { if (mController != null) { boolean changed = mController.onOpen(); } layoutPie(); } if (!show) { mCurrentItem = null; mPieView = null; } invalidate(); }
layoutPie会遍历每个item把他绘制到指定的位置, 可以看到核心是使用 path把每个扇形绘制出来的.
private void layoutPie() {//绘制每个pieitem的位置 float emptyangle = (float) Math.PI / 16;//空白间隙 int rgap = 2; int inner = mRadius + rgap; int outer = mRadius + mRadiusInc - rgap; int radius = mRadius; int gap = 1; for (int i = 0; i < mLevels; i++) { int level = i + 1; float sweep = (float) (Math.PI - 2 * emptyangle) / mCounts[level];//每个pieitem占用的度数 ,总的度数是pi/2 float angle = emptyangle + sweep / 2;//为了使 piememu上下对称 for (PieItem item : mItems) { if (item.getLevel() == level) { View view = item.getView(); view.measure(view.getLayoutParams().width, view.getLayoutParams().height); int w = view.getMeasuredWidth();//计算其宽度和高度 int h = view.getMeasuredHeight(); int r = inner + (outer - inner) * 2 / 3;//扇形的半径 int x = (int) (r * Math.sin(angle));//使用三角函数 计算其x y的位置 int y = mCenter.y - (int) (r * Math.cos(angle)) - h / 2; if (onTheLeft()) {//如果是在左边 x = mCenter.x + x - w / 2; } else { x = mCenter.x - x - w / 2; } view.layout(x, y, x + w, y + h); //设置区位置 float itemstart = angle - sweep / 2; Path slice = makeSlice(getDegrees(itemstart) - gap, getDegrees(itemstart + sweep) + gap, outer, inner, mCenter); //绘制扇形 item.setGeometry(itemstart, sweep, inner, outer, slice); angle += sweep; //下一个扇形在这个扇形的下一个位置 } } inner += mRadiusInc; outer += mRadiusInc; } }
然后就是onDraw进行绘制每个扇形了:
@Override protected void onDraw(Canvas canvas) { if (mOpen) { int state; if (mUseBackground) { int w = mBackground.getIntrinsicWidth(); int h = mBackground.getIntrinsicHeight(); int left = mCenter.x - w; int top = mCenter.y - h / 2; mBackground.setBounds(left, top, left + w, top + h); state = canvas.save(); if (onTheLeft()) { canvas.scale(-1, 1);//翻转 } mBackground.draw(canvas); canvas.restoreToCount(state); } for (PieItem item : mItems) {//遍历每一个item将其绘制 Paint p = item.isSelected() ? mSelectedPaint : mNormalPaint; //选择选择的画笔还是normal画笔? state = canvas.save(); if (onTheLeft()) {//如果点击的在左边 canvas.scale(-1, 1); } drawPath(canvas, item.getPath(), p); canvas.restoreToCount(state);//绘制完了回到以前的状态绘制item drawItem(canvas, item); } if (mPieView != null) {//绘制piemenu的sbumenu 比如tab的缩略图 mPieView.draw(canvas); } } }
然后是Move事件了, 在Movel事件的时候会判断用户手指落在哪里了,然后设置那个扇形为选中, 绘制不再赘述
else if (MotionEvent.ACTION_MOVE == action) { boolean handled = false; PointF polar = getPolar(x, y);//根据xy 求出点击的那个扇面. int maxr = mRadius + mLevels * mRadiusInc + 50; if (mPieView != null) { handled = mPieView.onTouchEvent(evt); } if (handled) { invalidate(); return false; } if (polar.y > maxr) {//可能没有选中, 就恢复以前的状态 deselect(); show(false); evt.setAction(MotionEvent.ACTION_DOWN); if (getParent() != null) { ((ViewGroup) getParent()).dispatchTouchEvent(evt); } return false; } PieItem item = findItem(polar);//根据给定的点 求出点击的那个扇面. if (mCurrentItem != item) { onEnter(item); if ((item != null) && item.isPieView()) {//类型tab, 含有二级的pieview, 就展现那个listview或者其他 int cx = item.getView().getLeft() + (onTheLeft() ? item.getView().getWidth() : 0); int cy = item.getView().getTop(); mPieView = item.getPieView(); layoutPieView(mPieView, cx, cy, (item.getStartAngle() + item.getSweep()) / 2); } invalidate(); } }
在用户放开手的时候也就是Up的时候会通知controller 选中了哪个扇形: , 绘制不再赘述
else if (MotionEvent.ACTION_UP == action) { if (mOpen) {//在手放开的时候如果piemenu在显示那么就开始执行相应的点击操作, 并恢复touch前的状态 boolean handled = false; if (mPieView != null) { handled = mPieView.onTouchEvent(evt); } PieItem item = mCurrentItem; deselect();//取消选中 show(false);//关闭menu if (!handled && (item != null)) { item.getView().performClick();//相应相关item的点击事件 } return true; } }
完全自绘的viewgroup, 对我们学习还是帮助很大的.Framelayout只是提供了一个"舞台"而已, 所有的绘制和事件处理都是我们自己实现的, 对于一些复杂的需求, 我们可以这样实现.