相信经常使用移动应用的用户都很熟悉侧滑菜单栏, 下拉, 下弹, 上弹等应用场景, 几乎主流的移动应用无论IOS 还是Android都能看到. 2.3以前的时候, 很多第三方比如SlidingMenu, MenuDrawer, ActionbarSherlock等等都很大程度的丰富和深化了这种交互理念.能让小小的屏幕, 容纳更多的交互接口. 也是这种趋势, Android官方在v4终于推出了DrawerLayout. 表示对侧滑的重视与肯定.
唠叨到这了. 去看了DrawerLayout的源码和官方示例. 官方提供的DrawerLayout已经封装的很好,可拿来即用.其实现原理, 就是使用上篇提及的ViewDragHelper去实现.而ViewDragHelper又借助View和Scroller,去实现真正的拖曳移动效果.为了加深对ViewDragHelper的认识, 这次我也来仿照官方的NavigationDrawer示例效果,做一下.做得不好的地方,请提出.谢谢哈.
因为是仿, 所以原理是跟NavigationDrawer一样的, 图片也大部分借了官方例子的图片. NavigationDrawer的实现关键是DrawerLayout, 它利用了ViewDragHelper. 当然,不只是ViewDragHelper, 还有其他辅助类.因为NavigationDrawer是结合ActionBar去做的, 所以也使用了ActionBarDrawerToggle作为切换侧滑菜单的开关. 但其实实现类似官方的效果, 只用ViewDragHelper也是够的. 因为我们的目的就是学ViewDragHelper. 如果对ViewDragHelper不了解,可以去上篇文章或者官网去看文档先了解下相关信息.
先上个效果图(额, 手机录屏后变横屏效果了 =,=)
下面上代码:
布局文件:
activity_main.xm
<com.alextam.simpleslidingmenu.SlidingMenu xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/drawer_layout" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" > <FrameLayout android:id="@+id/content_frame" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@drawable/bg" /> <LinearLayout android:id="@+id/ly_main_a" android:layout_width="240dp" android:layout_height="match_parent" android:orientation="vertical" android:background="#888888" android:alpha="0.0" android:layout_gravity="start" android:layout_marginLeft="-220dp" android:padding="20dp" > <ListView android:id="@+id/listview_main" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout></com.alextam.simpleslidingmenu.SlidingMenu>
看了官方示例的源码, 就不难理解为何这么布局, 因为要将侧边菜单栏的Layout隐藏在屏幕以外.我也借鉴了这种思路.
DrawerLayout是官方已经封装好的View类.于是我也使用ViewDragHelper封装了类似的View, SlidingMenu.
public class SlidingMenu extends FrameLayout { private static final String TAG = "SlidingMenu"; private ViewDragHelper mDragHelper; private int minValue = 20; //dp //边缘可触临界值 private int leftEdgeMinSize = minValue; //子view左侧(LEFT)值 private int leftValue; private View childViewA;// private View childViewBG; private boolean slidingMenuOpenSate = false; //注意的几个点, //如果mDragHelper.settleCapturedViewAt(left, top);方法去移动View,必须使用invalidate()刷新View才有效果. public SlidingMenu(Context context) { this(context,null); } public SlidingMenu(Context context, AttributeSet attrs) { this(context,attrs,0); } public SlidingMenu(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { //为了提高兼容性,new ViewDragHelper()这个创建方法是私有的,只能通过Create()这个工厂方法去创建对象 mDragHelper = ViewDragHelper.create(this, 1.0f, new sCallBack()); int eValue = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, minValue , getResources().getDisplayMetrics()); leftEdgeMinSize = eValue > minValue ? eValue : minValue; } @Override protected void onFinishInflate() { childViewA = findViewById(R.id.ly_main_a);// childViewBG = findViewById(R.id.content_frame); } /** * 拖曳监听接口,要使用ViewDragHelper,必须实现该接口类 */ private class sCallBack extends ViewDragHelper.Callback { //该方法必须实现 @Override public boolean tryCaptureView(View child, int pointerId) { return childViewA == child; } @Override public void onViewDragStateChanged(int state) { if(state == ViewDragHelper.STATE_IDLE) { //IDLE if(childViewA.getLeft() >= 0) { slidingMenuOpenSate = true; } } else if(state == ViewDragHelper.STATE_DRAGGING) { //Drag } else if(state == ViewDragHelper.STATE_SETTLING) { //Settle } } @Override public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { if(changedView != null) { float alp = (float)(1 + (float)Math.abs(left)/leftValue); if(left <= leftValue) { changedView.setAlpha(0.0f); } else { changedView.setAlpha(alp); } } } @Override public void onViewCaptured(View capturedChild, int activePointerId) {} //手势释放子view时会回调该方法 @Override public void onViewReleased(View releasedChild, float xvel, float yvel) { if(xvel < leftValue/3) { closeMenu(); } else if((xvel + leftValue/3) > 0) { openMenu(); } else { if(releasedChild.getLeft() > (leftValue - leftValue/3)) { openMenu(); } else { closeMenu(); } } } @Override public void onEdgeTouched(int edgeFlags, int pointerId) {} @Override public boolean onEdgeLock(int edgeFlags) { return false; } @Override public void onEdgeDragStarted(int edgeFlags, int pointerId) {} @Override public int getOrderedChildIndex(int index) { return index; } @Override public int getViewHorizontalDragRange(View child) { return 0; } @Override public int getViewVerticalDragRange(View child) { return leftValue; } //实现水平拖曳的重要方法,返回的值是实现子view被水平拖曳移动的值 @Override public int clampViewPositionHorizontal(View child, int left, int dx) { final int paddingLeft = getPaddingLeft(); //限制子view的拖曳不超出父view的左右边缘 //如果直接return left; 也是可以的.但子view的拖曳就可以滑出父view以外位置了// final int resultLeft = Math.min(Math.max(paddingLeft,left),// getWidth() - getChildAt(1).getWidth());// return resultLeft; final int resultLeft = Math.max(leftValue , Math.min(left,0)); return resultLeft; } //实现垂直拖曳的重要方法 @Override public int clampViewPositionVertical(View child, int top, int dy) { final int paddingTop = getPaddingTop(); final int resultTop = Math.min(Math.max(paddingTop,top), getHeight() - childViewA.getHeight()); return resultTop; } } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { final int action = MotionEventCompat.getActionMasked(ev); if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { if(mDragHelper != null) //取消或手指放开,都应当cancel() mDragHelper.cancel(); return false; } return mDragHelper.shouldInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent ev) { if(ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_CANCEL ) { if(childViewA != null) { if(slidingMenuOpenSate && ev.getX() > childViewA.getWidth()) { closeMenu(); } } } if(mDragHelper != null) { mDragHelper.processTouchEvent(ev); return true; } return false; } @Override public void computeScroll() { if (mDragHelper.continueSettling(true)) { ViewCompat.postInvalidateOnAnimation(this); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed,l,t,r,b); leftValue = leftEdgeMinSize - childViewA.getWidth(); } protected void openMenu() { if(mDragHelper != null) { if(mDragHelper.smoothSlideViewTo(childViewA,0,0)) { ViewCompat.postInvalidateOnAnimation(this); slidingMenuOpenSate = true; } } } protected void closeMenu() { if(mDragHelper != null) { if(mDragHelper.smoothSlideViewTo(childViewA,leftValue,0)) { ViewCompat.postInvalidateOnAnimation(this); slidingMenuOpenSate = false; } } } //获取侧滑菜单栏展开状态 public boolean getSlidingMenuOpenSate() { return slidingMenuOpenSate; }}
整个过程的关键有3个地方, 一个就是之前说过的ViewDragHelper里面所提供的CallBack接口类要实现, CallBack接口提供了整个View拖曳或settle过程的监听方法,非常有效. 第二是要处理好手势触摸的onTouch事件. 第三个, 根据需要设置可拖曳View的边缘大小.这个值直接决定了view能否快速有效地定位到手势.
目的是为了熟悉ViewDragHelper这个类以及实现侧滑的效果, 所以SimpleSlidingMenu这个类并没有封装做的很复杂,思路还是能看清的吧.对于不熟悉的东西, 我原则是一般先做出来再说, 至于做得好不好,怎么优化等等,都是等先有个哪怕是粗糙的成品出来了再说. 不然胡想一大堆, 而且什么也没做成,效率太低.当然,主要的思路还是要有的嘛.
strings.xml中的引用数组:
<string-array name="planets_array"> <item>Mercury</item> <item>Venus</item> <item>Earth</item> <item>Mars</item> <item>Jupiter</item> <item>Saturn</item> <item>Uranus</item> <item>Neptune</item> </string-array>
然后是MainAcitvity
public class MainActivity extends Activity { private LinearLayout linearLayout; private ListView listView; private SlidingMenu drawerLayout; private String[] mPlanetTitles; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); drawerLayout = (SlidingMenu)findViewById(R.id.drawer_layout); linearLayout = (LinearLayout)findViewById(R.id.ly_main_a); mPlanetTitles = getResources().getStringArray(R.array.planets_array); listView = (ListView)findViewById(R.id.listview_main); listView.setAdapter(new ArrayAdapter<String>(this,R.layout.drawer_list_item,mPlanetTitles)); listView.setOnItemClickListener(new DrawerItemClickListener()); } private class DrawerItemClickListener implements ListView.OnItemClickListener { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { listView.setSelection(position); selectItem(position); } } private void selectItem(int position) { // update the main content by replacing fragments Fragment fragment = new PlanetFragment(); Bundle args = new Bundle(); args.putInt(PlanetFragment.ARG_PLANET_NUMBER, position); fragment.setArguments(args); FragmentManager fragmentManager = getFragmentManager(); fragmentManager.beginTransaction().replace(R.id.content_frame, fragment).commit(); // update selected item and title, then close the drawer listView.setItemChecked(position, true); //折合侧滑菜单 drawerLayout.closeMenu(); } public static class PlanetFragment extends Fragment { public static final String ARG_PLANET_NUMBER = "planet_number"; public PlanetFragment() { // Empty constructor required for fragment subclasses } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_planet, container, false); int i = getArguments().getInt(ARG_PLANET_NUMBER); String planet = getResources().getStringArray(R.array.planets_array)[i]; int imageId = getResources().getIdentifier(planet.toLowerCase(Locale.getDefault()), "drawable", getActivity().getPackageName()); ((ImageView) rootView.findViewById(R.id.image)).setImageResource(imageId); getActivity().setTitle(planet); return rootView; } }}
MainActivity的实现过程,跟NavigationDrawer类似, 都是利用Fragment替换某个ID的Layout. 这里要说明下, MainActivity中创建和替换PlanetFragment的相关方法源码是用了示例的源码. 毕竟不想重复造轮子嘛. 不同的是,示例直接用ListView作为侧滑的菜单, 但这样有点局限了. 于是改了用Layout,里面装ListView, 这样ListView同样也能被引用. 当然Layout里面装其他View或者Layout也是可以的.
最后回到侧滑这件事上,侧滑是一个不错的交互选择.官方也针对于此给出引导, 将应用的菜单导航放在左侧, 将功能放在右侧菜单.而侧滑流行了相当长一段时间, 现在已经有很多成熟的第三方提供. ViewDragHelper只是在自己实现的时候提供了一种选择. 至于是不是好的选择,跟具体的需求和实现有关.当然ViewDragHelper也不仅仅局限于简单的拖曳某个View什么的. 将它和ViewGroup,或者一些Layout类结合起来,能生产出有丰富拖曳滑动效果的容器.Then,这篇文章就写到这了,感谢你的阅读.^^