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,这篇文章就写到这了,感谢你的阅读.^^



郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。