当前位置: 代码迷 >> Android >> ViewDragHelper实践之仿Android官方侧滑菜单NavigationDrawer成效
  详细解决方案

ViewDragHelper实践之仿Android官方侧滑菜单NavigationDrawer成效

热度:70   发布时间:2016-04-28 01:05:23.0
ViewDragHelper实践之仿Android官方侧滑菜单NavigationDrawer效果

相信经常使用移动应用的用户都很熟悉侧滑菜单栏, 下拉, 下弹, 上弹等应用场景, 几乎主流的移动应用无论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,这篇文章就写到这了,感谢你的阅读.^^



  相关解决方案