preface

ViewDragHelper is a magic tool. It requires very little code to implement drag and move. The code related to gestures is wrapped inside ViewDragHelper. This article uses ViewDragHelper again to realize QQ’s side slide menu.

Speaking of this sidebar, when I first learned Android, I was confused. I always wanted to implement it, but I didn’t have the ability to implement it due to my poor level at that time. Now I have been working on Android for a few years, and I finally implemented this function with ViewDragHelper. Think about it or some excitement ~

Of course, this code can only achieve the basic left slide menu, and there may be some gesture conflicts are not dealt with, but the main line is still left slide, the other processing is icing on the cake ~ if the actual development of the use, it is best to use already built wheels. (The wheel is good, but it is not your own. Only when you are free can you understand the essence.)

The finished product to show

Analysis of the

Layout analysis

The sideslip menu is divided into three parts:

  • The whole page, including the menu part and the content part

  • The menu section, at the same level as the content layout, is wrapped in the SlidingMenu control.

  • The content section, again, is at the same level as the menu section and is wrapped in the SlidingMenu control. It is not hard to notice that in our effect, when opening the menu, the content area will have a black mask, from transparent to translucent according to the sliding distance, so the content part also has a mask.

XML layout

  • The total layout
<? xml version="1.0" encoding="utf-8"? > <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.zh.android.slidingmenu.sample.SlidingMenu
        android:id="@+id/sliding_menu"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <include layout="@layout/layout_menu" />

        <include layout="@layout/layout_content" />
    </com.zh.android.slidingmenu.sample.SlidingMenu>
</FrameLayout>
Copy the code
  • The menu layout
<? xml version="1.0" encoding="utf-8"? > <androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/menu_list"
    android:layout_width="280dp"
    android:layout_height="match_parent" />
Copy the code
  • Content layout
<? xml version="1.0" encoding="utf-8"? > <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/tool_bar"
            android:layout_width="match_parent"
            android:layout_height="? actionBarSize"
            android:background="@color/colorPrimary"
            app:title="@string/app_name"
            app:titleTextColor="@android:color/white" />

        <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/content_list"
                android:layout_width="match_parent"
                android:layout_height="match_parent" />
    </LinearLayout>

    <View
        android:id="@+id/content_bg"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:alpha="0"
        android:background="#8C000000"
        tools:alpha="1" />
</FrameLayout>
Copy the code

Behavior analysis

  • Open the menu

    1. The menu pulls out from left to right, and the menu moves to the right along with the content.
    2. Content can’t be pulled from the left until it’s completely present, and content can’t be pulled anymore.
  • Close the menu

    1. Content is pulled back from right to left, and the menu moves to the left along with the content.
    2. Content moves from right to left and cannot be pulled further to the left when it is completely removed from the screen.
  • Other details

    1. Drag and drop menus and content can pull the overall menu movement.
    2. The mask forms a ratio according to the position of the drag support, so that the transparency of the mask changes.
      • The ratio is 1 when the menu is open and 0 when the menu is closed.
      • From closed to open, the ratio starts at 0 and gradually approaches 1.

Gradually achieve

  • Create SlidingMenu layout

    1. Overwrite onFinishInflate() to get the child control and the layout child control, which must have only the menu and content child controls.
    2. Duplicate onLayout(), layout menu and content.
      • Menu layout, left value is negative menu width, right value is the parent layout left, so the menu is hidden by default on the left side of the screen, is not visible.
      • Content area, all values are the same as the parent layout, that is, the parent layout.
Public class SlidingMenu extends FrameLayout {/** * private View vMenuView; /** * private View vContentView; public SlidingMenu(@NonNull Context context) { this(context, null); } public SlidingMenu(@NonNull Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public SlidingMenu(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs, defStyleAttr); } private void init(@nonnull Context Context, @nullable AttributeSet attrs, int defStyleAttr) {// Initialize... } @Override protected voidonFinishInflate() { super.onFinishInflate(); Int childCount = getChildCount();if(childCount ! = 2) { throw new IllegalStateException("There can only be 2 sub-views in the sideslip menu, menu and content."); } vMenuView = getChildAt(0); vContentView = getChildAt(1); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); And content / / layout of the menu, the menu in the left, is invisible to the ordinary state vMenuView. The layout (- vMenuView. GetMeasuredWidth (), top, left, bottom); Vcontentview. layout(left, top, right, bottom); }}Copy the code
  • Create a menu status callback interface that provides the following callback methods:

    • OnMenuOpen (), callback when menu is opened.
    • OnSliding (float fraction), callback when sliding, returns the fraction value, is sliding ratio.
    • OnMenuClose (), which is called when the menu is closed.
/ * * * menu state changes to monitor * / private OnMenuStateChangeListener mMenuStateChangeListener; / * * * menu state changes to monitor * / public interface OnMenuStateChangeListener {/ * * * when the menu opens the callback * / void onMenuOpen (); /** * @param fraction sliding percentage */ void onSliding(floatfraction); /** * callback when menu is closed */ void onMenuClose(); } /** * set state change listener ** @param menuStateChangeListener listener */ public voidsetOnMenuStateChangeListener(OnMenuStateChangeListener menuStateChangeListener) {
    mMenuStateChangeListener = menuStateChangeListener;
}
Copy the code
  • Create a ViewDragHelper and delegate the event to the ViewDragHelper
    • Clone onInterceptTouchEvent(), which delegates to ViewDragHelper.
    • Overwrite onTouchEvent(), which delegates to ViewDragHelper.
    • Overwrite computeScroll(), since ViewDragHelper’s scrolling depends on Scroller, it needs to hand over Scroller related processing to ViewDragHelper.
/** * private ViewDragHelper mViewDragHelper; /** * private void init(@nonnull Context Context, @nullable AttributeSet attrs, Int defStyleAttr) {mViewDragHelper = viewDragHelper. create(this, 1.0f, new ViewDragHelper.Callback() {/ /... Override public Boolean onInterceptTouchEvent(MotionEvent ev) {// Delegate onInterceptTouchEvent to ViewDragHelperreturnmViewDragHelper.shouldInterceptTouchEvent(ev); } Override public Boolean onTouchEvent(MotionEvent event) {// Delegate onTouchEvent to ViewDragHelper mViewDragHelper.processTouchEvent(event);return true;
}

@Override
public void computeScroll() { super.computeScroll(); // Check if it reaches the end, and continue until it reaches the endif(mViewDragHelper ! = null) {if (mViewDragHelper.continueSettling(true)) { invalidate(); }}}Copy the code
  • Duplicate ViewDragHelper to handle lateral slippage

    1. Duplicate tryCaptureView() to make sure menus and contents can be dragged
    2. Autotype getViewHorizontalDragRange (), return ye scope, returns non-zero value can, in some cases can need this value to determine whether to drag on.
    3. Autotype clampViewPositionHorizontal (), processing horizontal drag, because the menu and content can be pulled to drag this 2 part will callback, but method is introduced to the child for the menu or the content. So you need to judge the control to handle.
      • Drag is the menu, processing as follows:

        • The left margin left cannot exceed the width of the menu. Since the coordinate system, the left side of the screen is negative, so it cannot be less than the negative width of the menu.
        • The left margin left cannot exceed 0, because you cannot continue to drag to the right after the menu is fully displayed.
        • Otherwise, it is allowed and simply returns the left value passed.
      • Drag is the content, processing is as follows:

        • The left margin left cannot exceed 0 because content cannot be dragged out of the left side of the screen.
        • The left margin left cannot exceed the width of the menu because content cannot be dragged to the right after the menu is fully displayed.
        • Otherwise, it is allowed and simply returns the left value passed.
/ /... /** * Private void init(@nonnull Context Context, @nullable AttributeSet attrs, Int defStyleAttr) {mViewDragHelper = viewDragHelper. create(this, 1.0f, new ViewDragHelper.Callback() {@override public Boolean tryCaptureView(@nonnull View child, int pointerId)returnchild == vMenuView || child == vContentView; } @ Override public int getViewHorizontalDragRange (@ NonNull View child) {/ / drag scope, returns non-zero value can, in some cases can need this value to determine whether to drag onreturnvContentView.getWidth(); } @ Override public int clampViewPositionHorizontal (@ NonNull View child, int left, int dx) {/ / processing horizontal drag, Int menuWidth = vmenuView.getwidth (); // There are two cases we need to deal with because of the difference between the drag menu and the object that the content passes inif(child == vMenuView) {// Drag the menuif(left < -menuWidth) {// The left distance can only be completely hidden on the left side of the screenreturn -menuWidth;
                } else if(left > 0) {// Left distance, at most can appear completely in the screenreturn 0;
                } else {
                    returnleft; }}else if(Child == vContentView) {// Drag the content area, cannot move beyond the leftmost screenif (left < 0) {
                    return 0;
                } else if(left > menuWidth) {// Cannot exceed the width of the menureturn menuWidth;
                } else {
                    returnleft; }}return0; }}); } / /... Omit other codeCopy the code
  • Handle drag support, linkage processing

If only autotype clampViewPositionHorizontal (), only to drag the menu or the content of single movement, they are not linked, we need to drag the menu, content with the mobile together. Accordingly, when you drag and drop content, the menu moves along with it.

  1. Overwrite onViewPositionChanged() to call back to the location of a menu or content when dragging it.
  2. You also need to determine whether the Child object is a menu or content ().
    • When the drag object is a menu, manually call the layout() method of the content View to move the content View.
    • When the drag object is content, manually call the layout() method of the menu View to make the content View move.
  3. Handles on and off status handling and callback listening.
  4. Define menu opening and closing methods
    • OpenMenu () opens the menu.
    • CloseMenu (), close the menu.
/ /... /** * Private void init(@nonnull Context Context, @nullable AttributeSet attrs, Int defStyleAttr) {mViewDragHelper = viewDragHelper. create(this, 1.0f, new ViewDragHelper.Callback() { @Override public void onViewPositionChanged(@NonNull View changedView, int left, int top, int dx, int dy) { super.onViewPositionChanged(changedView, left, top, dx, dy); // Handle linkage, drag and drop menu layout, make content layout followif (changedView == vMenuView) {
                    int newLeft = vContentView.getLeft() + dx;
                    int right = newLeft + vContentView.getWidth();
                    vContentView.layout(newLeft, top, right, getBottom());
                } else if(changedView == vContentView) {newLeft = vmenuView.getLeft () + dx; vMenuView.layout(newLeft, top, left, getBottom()); } // Handle the callbacks in the slide to calculate the slide ratioif(mMenuStateChangeListener ! = null) {floatfraction = (vContentView.getLeft() * 1f) / vMenuView.getWidth(); mMenuStateChangeListener.onSliding(fraction); } // Handle the on/off state. Since this method is called back repeatedly, we need to add the status value to ensure that the listener is called back only onceif((vmenuView.getLeft () == -vmenuView.getwidth ()) &&isOpenMenu) {// Close isOpenMenu =false;
                    if (mMenuStateChangeListener != null) {
                        mMenuStateChangeListener.onMenuClose();
                    }
                } else if(vMenuView.getLeft() == 0 && ! IsOpenMenu) {// open isOpenMenu =true;
                    if(mMenuStateChangeListener ! = null) { mMenuStateChangeListener.onMenuOpen(); }}}}); /** * open menu */ public voidopenMenu() { mViewDragHelper.smoothSlideViewTo(vMenuView, 0, vMenuView.getTop()); ViewCompat.postInvalidateOnAnimation(SlidingMenu.this); } /** * close menu */ public voidcloseMenu() { mViewDragHelper.smoothSlideViewTo(vMenuView, -vMenuView.getWidth(), vMenuView.getTop()); ViewCompat.postInvalidateOnAnimation(SlidingMenu.this); }} / /... Omit other codeCopy the code
  • Handles the release rebound and fling operations

    • First, work with the Fling operation and close the menu when the XVEL value is less than 0 for a quick inertial slide to the left. If it is to the right, the xvel value is greater than 300 and the menu is opened.
    • If the menu is not fling, it is closed if the left is less than half the width of the menu.
/ /... /** * Private void init(@nonnull Context Context, @nullable AttributeSet attrs, Int defStyleAttr) {mViewDragHelper = viewDragHelper. create(this, 1.0f, new ViewDragHelper.Callback() {
            @Override
            public void onViewReleased(@NonNull View releasedChild, float xvel, floatyvel) { super.onViewReleased(releasedChild, xvel, yvel); / / fling operationif(xvel < 0) {// closeMenu();return;
                } else if(xvel > 300) {// openMenu();return; } // Release and bounce backfloathalfMenuWidth = vMenuView.getWidth() / 2f; // If the range of the menu is less than half, it is offif(vmenuView.getLeft () < -halfmenuWidth) {// Close closeMenu(); }else{/ / openMenu (); }}}); } / /... Omit other codeCopy the code
  • mask

As mentioned above, there is a mask in the content area. When the menu goes from closed to open, it goes from light to black. It is actually a translucent mask, and its transparency goes from 0 to 1. We can take the left margin of the content area as the starting point and make a ratio of it to the width of the menu View. This ratio is passed to the callback. When the callback is received, the opacity of the mask is set.

@Override public void onViewPositionChanged(@NonNull View changedView, int left, int top, int dx, int dy) { super.onViewPositionChanged(changedView, left, top, dx, dy); . // Handle the callback in the slide, calculate the slide ratioif(mMenuStateChangeListener ! = null) {floatfraction = (vContentView.getLeft() * 1f) / vMenuView.getWidth(); mMenuStateChangeListener.onSliding(fraction); }... }Copy the code

Set the callback to handle mask transparency

Public class MainActivity extends appactivity {/** * sidelights menu */ private SlidingMenu vSlidingMenu; /** * private FloatEvaluator; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);
        findView();
        bindView();
    }

    private void findView() {... Omit other controls vSlidingMenu = findViewById(R.id.sliding_menu); vContentBg = findViewById(R.id.content_bg); . Omit other controls} private voidbindView() {// create evaluator = new FloatEvaluator(); / / -- -- -- -- -- -- -- -- -- -- -- - important: set menu of sideslip state switch to monitor -- -- -- -- -- -- -- -- -- -- -- -- vSlidingMenu. SetOnMenuStateChangeListener (new SlidingMenu.OnMenuStateChangeListener() {
            @Override
            public void onMenuOpen() {
                Log.d(TAG, "Menu open"); // Make black mask, disable touch vContentbg.setClickable (true);
            }

            @Override
            public void onSliding(float fraction) {
                Log.d(TAG, "Menu drag, percentage:"+ fraction); // Set minimum and maximum transparency valuesfloat startValue = 0;
                floatEndValue = 0.55 f; // Evaluate the current transparency value and set Float value = malphaevaluator. evaluate(fraction, startValue, endValue); vContentBg.setAlpha(value); } @Override public voidonMenuClose() {
                Log.d(TAG, "Menu closed"); // Let the black mask resume touch vContentbg.setClickable (false); }}); //------------ Key: set the status switch of the slide menu listening ------------}}Copy the code

The complete code

Public class SlidingMenu extends FrameLayout {/** * private View vMenuView; /** * private View vContentView; /** * private ViewDragHelper mViewDragHelper; / * * * menu state changes to monitor * / private OnMenuStateChangeListener mMenuStateChangeListener; */ private Boolean isOpenMenu; public SlidingMenu(@NonNull Context context) { this(context, null); } public SlidingMenu(@NonNull Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public SlidingMenu(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs, defStyleAttr); } /** * private void init(@nonnull Context Context, @nullable AttributeSet attrs, Int defStyleAttr) {mViewDragHelper = viewDragHelper. create(this, 1.0f, new ViewDragHelper.Callback() {@override public Boolean tryCaptureView(@nonnull View child, int pointerId)returnchild == vMenuView || child == vContentView; } @ Override public int getViewHorizontalDragRange (@ NonNull View child) {/ / drag scope, returns non-zero value can, in some cases can need this value to determine whether to drag onreturnvContentView.getWidth(); } @ Override public int clampViewPositionHorizontal (@ NonNull View child, int left, int dx) {/ / processing horizontal drag, Int menuWidth = vmenuView.getwidth (); // There are two cases we need to deal with because of the difference between the drag menu and the object that the content passes inif(child == vMenuView) {// Drag the menuif(left < -menuWidth) {// The left distance can only be completely hidden on the left side of the screenreturn -menuWidth;
                    } else if(left > 0) {// Left distance, at most can appear completely in the screenreturn 0;
                    } else {
                        returnleft; }}else if(Child == vContentView) {// Drag the content area, cannot move beyond the leftmost screenif (left < 0) {
                        return 0;
                    } else if(left > menuWidth) {// Cannot exceed the width of the menureturn menuWidth;
                    } else {
                        returnleft; }}return0; } @Override public void onViewPositionChanged(@NonNull View changedView, int left, int top, int dx, int dy) { super.onViewPositionChanged(changedView, left, top, dx, dy); // Handle linkage, drag and drop menu layout, make content layout followif (changedView == vMenuView) {
                    int newLeft = vContentView.getLeft() + dx;
                    int right = newLeft + vContentView.getWidth();
                    vContentView.layout(newLeft, top, right, getBottom());
                } else if(changedView == vContentView) {newLeft = vmenuView.getLeft () + dx; vMenuView.layout(newLeft, top, left, getBottom()); } // Handle the callbacks in the slide to calculate the slide ratioif(mMenuStateChangeListener ! = null) {floatfraction = (vContentView.getLeft() * 1f) / vMenuView.getWidth(); mMenuStateChangeListener.onSliding(fraction); } // Handle the on/off state. Since this method is called back repeatedly, we need to add the status value to ensure that the listener is called back only onceif((vmenuView.getLeft () == -vmenuView.getwidth ()) &&isOpenMenu) {// Close isOpenMenu =false;
                    if (mMenuStateChangeListener != null) {
                        mMenuStateChangeListener.onMenuClose();
                    }
                } else if(vMenuView.getLeft() == 0 && ! IsOpenMenu) {// open isOpenMenu =true;
                    if(mMenuStateChangeListener ! = null) { mMenuStateChangeListener.onMenuOpen(); } } } @Override public void onViewReleased(@NonNull View releasedChild,float xvel, floatyvel) { super.onViewReleased(releasedChild, xvel, yvel); / / fling operationif(xvel < 0) {// closeMenu();return;
                } else if(xvel > 300) {// openMenu();return; } // Release and bounce backfloathalfMenuWidth = vMenuView.getWidth() / 2f; // If the range of the menu is less than half, it is offif(vmenuView.getLeft () < -halfmenuWidth) {// Close closeMenu(); }else{/ / openMenu (); }}}); } @Override protected voidonFinishInflate() { super.onFinishInflate(); Int childCount = getChildCount();if(childCount ! = 2) { throw new IllegalStateException("There can only be 2 sub-views in the sideslip menu, menu and content."); } vMenuView = getChildAt(0); vContentView = getChildAt(1); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); And content / / layout of the menu, the menu in the left, is invisible to the ordinary state vMenuView. The layout (- vMenuView. GetMeasuredWidth (), top, left, bottom); Vcontentview. layout(left, top, right, bottom); } @override public Boolean onInterceptTouchEvent(MotionEvent ev) {// Delegate onInterceptTouchEvent to ViewDragHelperreturnmViewDragHelper.shouldInterceptTouchEvent(ev); } Override public Boolean onTouchEvent(MotionEvent event) {// Delegate onTouchEvent to ViewDragHelper mViewDragHelper.processTouchEvent(event);return true;
    }

    @Override
    public void computeScroll() { super.computeScroll(); // Check if it reaches the end, and continue until it reaches the endif(mViewDragHelper ! = null) {if (mViewDragHelper.continueSettling(true)) { invalidate(); }}} /** * Open menu */ public voidopenMenu() { mViewDragHelper.smoothSlideViewTo(vMenuView, 0, vMenuView.getTop()); ViewCompat.postInvalidateOnAnimation(SlidingMenu.this); } /** * close menu */ public voidcloseMenu() { mViewDragHelper.smoothSlideViewTo(vMenuView, -vMenuView.getWidth(), vMenuView.getTop()); ViewCompat.postInvalidateOnAnimation(SlidingMenu.this); } / menu state changes to monitor * * * * / public interface OnMenuStateChangeListener {/ * * * when the menu opens the callback * / void onMenuOpen (); /** * @param fraction sliding ratio */ void onSliding(floatfraction); /** * callback when menu is closed */ void onMenuClose(); } /** * set state change listener ** @param menuStateChangeListener listener */ public voidsetOnMenuStateChangeListener(OnMenuStateChangeListener menuStateChangeListener) { mMenuStateChangeListener = menuStateChangeListener; }}Copy the code

conclusion

Project code, I submitted to Github, students who need to clone: Github address

Using ViewDragHelper is a magic tool, it is very convenient to do drag and move. The difficulty of this article actually is the coordinate calculation, especially in the onViewPositionChanged() method drag linkage menu and content of the two parts, the calculation of the four points of the two controls will be a bit of a mind, but other calculations are ok.