前面说的这些东西其实都是重写view实现view的自由绘制, 但是有些时候, 可能我们还需要这样一种自定义的view, 他们其实
不能称得上是view, 而只是一些对视图的描述, 这很像我们使用animator的时候, 也会定义一些shape, 然后随着时间控制shape的变化, 然后把这些shape draw到画布上.呈现给用户.其实一定程度是实现了一个自己的"view系统".
同样, 在android浏览器的Piemenu中也有这样的类, 这就是PieView, 我感觉更应该叫 SubPieView.
他在Phone上的展现如下:
也就是选中一个扇形之后, 显示下一级菜单. 当然你也可以自定义成那种围绕内层圆弧再次展开的外层的menu.
我们看一下人是如何实现的.首先是其接口:
public interface PieView { public interface OnLayoutListener { public void onLayout(int ax, int ay, boolean left); } //同意设置布局监听, 在布局的时候做一些事情, 这里主要是刷新tab public void setLayoutListener(OnLayoutListener l); //父亲只管layout, 至于以何种方式展现, 孩子来决定 public void layout(int anchorX, int anchorY, boolean onleft, float angle); //父亲只管draw, 至于以何种方式展现, 孩子来决定 public void draw(Canvas c); //父亲只管发出onTouchEvent, 至于响应什么事件, 孩子来决定 public boolean onTouchEvent(MotionEvent evt); }
设计图如下,在不同的设备(手机和平板)上显示的是不同的,.
首先看一下setAdapter也就是view的组装:
这个调用是在PieControlPhone::populateMenu()中实现调用的:
stack.setAdapter(mTabAdapter);
mTabAdapter还是一个BaseAdapter, 其实这个东西的实现可以认识是一个mini版的listview
//类似于listview的setAdapter public void setAdapter(Adapter adapter) { mAdapter = adapter; if (adapter == null) { if (mAdapter != null) { mAdapter.unregisterDataSetObserver(mObserver); } mViews = null; mCurrent = -1; } else { //注册监听, 通知 从adapter中拿到各个view进行拼接 mObserver = new DataSetObserver() { @Override public void onChanged() { buildViews(); } @Override public void onInvalidated() { mViews.clear(); } }; //监听更新的通知, 对应adapter的 notifyDataSetChanged mAdapter.registerDataSetObserver(mObserver); setCurrent(0); } }
我们也可以看看我们熟悉的 adapter.getView的原理:
//从adapter中拿到view进行组装 protected void buildViews() { if (mAdapter != null) { final int n = mAdapter.getCount(); if (mViews == null) { mViews = new ArrayList(n); } else { mViews.clear(); } mChildWidth = 0; mChildHeight = 0; for (int i = 0; i < n; i++) { View view = mAdapter.getView(i, null, null);//展示了adapter也可以在其他情况中使用 (非listview), 可以用来把数据转换为view显示 view.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); mChildWidth = Math.max(mChildWidth, view.getMeasuredWidth()); mChildHeight = Math.max(mChildHeight, view.getMeasuredHeight()); mViews.add(view); } } }
通过这两步就可以组装成一个view列表了,当然listview还有view的复用等, 这个view没有实现这些功能.
为了方便学习, 把整个BasePieView贴上, 代码也不多:
public abstract class BasePieView implements PieMenu.PieView { protected Adapter mAdapter; private DataSetObserver mObserver; protected ArrayListmViews; protected OnLayoutListener mListener; protected int mCurrent; protected int mChildWidth; protected int mChildHeight; protected int mWidth; protected int mHeight; protected int mLeft; protected int mTop; public BasePieView() { } public void setLayoutListener(OnLayoutListener l) { mListener = l; } //类似于listview的setAdapter public void setAdapter(Adapter adapter) { mAdapter = adapter; if (adapter == null) { if (mAdapter != null) { mAdapter.unregisterDataSetObserver(mObserver); } mViews = null; mCurrent = -1; } else { //注册监听, 通知 从adapter中拿到各个view进行拼接 mObserver = new DataSetObserver() { @Override public void onChanged() { buildViews(); } @Override public void onInvalidated() { mViews.clear(); } }; //监听更新的通知, 对应adapter的 notifyDataSetChanged mAdapter.registerDataSetObserver(mObserver); setCurrent(0); } } public void setCurrent(int ix) { mCurrent = ix; } public Adapter getAdapter() { return mAdapter; } //从adapter中拿到view进行组装 protected void buildViews() { if (mAdapter != null) { final int n = mAdapter.getCount(); if (mViews == null) { mViews = new ArrayList (n); } else { mViews.clear(); } mChildWidth = 0; mChildHeight = 0; for (int i = 0; i < n; i++) { View view = mAdapter.getView(i, null, null);//展示了adapter也可以在其他情况中使用 (非listview), 可以用来把数据转换为view显示 view.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); mChildWidth = Math.max(mChildWidth, view.getMeasuredWidth()); mChildHeight = Math.max(mChildHeight, view.getMeasuredHeight()); mViews.add(view); } } } /** * this will be called before the first draw call * needs to set top, left, width, height * 被父亲控制, 开始布局 */ @Override public void layout(int anchorX, int anchorY, boolean left, float angle) { if (mListener != null) { mListener.onLayout(anchorX, anchorY, left); } } //让孩子们去画着玩吧! @Override public abstract void draw(Canvas canvas); protected void drawView(View view, Canvas canvas) { final int state = canvas.save(); canvas.translate(view.getLeft(), view.getTop()); view.draw(canvas); canvas.restoreToCount(state); } //有孩子来决定 根据y判断当前选中view的策略 protected abstract int findChildAt(int y); @Override public boolean onTouchEvent(MotionEvent evt) { int action = evt.getActionMasked(); int evtx = (int) evt.getX(); int evty = (int) evt.getY(); if ((evtx < mLeft) || (evtx >= mLeft + mWidth) || (evty < mTop) || (evty >= mTop + mHeight)) { return false; } switch (action) { case MotionEvent.ACTION_MOVE: View v = mViews.get(mCurrent); setCurrent(Math.max(0, Math.min(mViews.size() -1,//设置显示那个tab findChildAt(evty))));//更加y来判断 指向了哪个tab View v1 = mViews.get(mCurrent); if (v != v1) { v.setPressed(false); v1.setPressed(true); } break; case MotionEvent.ACTION_UP://在手机抬起的时候 通知相应点击事件 mViews.get(mCurrent).performClick(); mViews.get(mCurrent).setPressed(false); break; default: break; } return true; }}
然后是一种孩子的实现, 也很简单:
/** * shows views in a stack * 这是一个显示层叠tab 视图的view适用于phone */public class PieStackView extends BasePieView { private static final int SLOP = 5; private OnCurrentListener mCurrentListener; private int mMinHeight; public interface OnCurrentListener { public void onSetCurrent(int index); } public PieStackView(Context ctx) { mMinHeight = (int) ctx.getResources() .getDimension(R.dimen.qc_tab_title_height); } public void setOnCurrentListener(OnCurrentListener l) { mCurrentListener = l; } //当前选中哪个? @Override public void setCurrent(int ix) { super.setCurrent(ix); if (mCurrentListener != null) { mCurrentListener.onSetCurrent(ix); buildViews();//从adapter中拿到view layoutChildrenLinear(); } } /** * this will be called before the first draw call * 在绘制之前会被调用进行layout */ @Override public void layout(int anchorX, int anchorY, boolean left, float angle) { super.layout(anchorX, anchorY, left, angle); buildViews(); mWidth = mChildWidth; mHeight = mChildHeight + (mViews.size() - 1) * mMinHeight; mLeft = anchorX + (left ? SLOP : -(SLOP + mChildWidth)); mTop = anchorY - mHeight / 2; if (mViews != null) { layoutChildrenLinear(); } } //布局其中的child private void layoutChildrenLinear() { final int n = mViews.size(); int top = mTop; int dy = (n == 1) ? 0 : (mHeight - mChildHeight) / (n - 1); for (View view : mViews) { int x = mLeft; view.layout(x, top, x + mChildWidth, top + mChildHeight);//重叠的tab缩略图进行布局 top += dy; } } //真正的绘制view, 谁想显示这个view就把他的canvas传给他就可以绘制出来了. @Override public void draw(Canvas canvas) { if (mViews != null) { final int n = mViews.size(); for (int i = 0; i < mCurrent; i++) { drawView(mViews.get(i), canvas); } for (int i = n - 1; i > mCurrent; i--) { drawView(mViews.get(i), canvas); } //先绘制其他的tab 最后绘制当前的tab这样就可以把当前的tab整个显示出来,其他tab被他遮挡一部分了 drawView(mViews.get(mCurrent), canvas); } } //被父亲调用, 用来更加用户的y查看选中的是哪个tab @Override protected int findChildAt(int y) { final int ix = (y - mTop) * mViews.size() / mHeight; return ix; }}
这样其实我们学习到了另一个自定义view的方式, 完全不继承view, 谁想显示这个view把canvas给我们的view就可以显示相应的展现
也对listview和adapter的原理有了一些了解~