There are no shortcuts to Android custom controls, and they require constant imitation to get started. The choice of a mock demo is crucial, and the best choice is the official control, which can be intimidating with thousands of lines of code. This article introduces how to understand and achieve the Android QQ side slide menu, 300 lines of code. First of all, the completed renderings:

Side effects


First of all,

This article won’t go into the principles of custom controls, from drawing to screen coordinates, sliding to animation, because I’m sure you already know the principles from somewhere else, whether or not you can customize controls. But for the sake of easy understanding, it will be interspersed in the implementation process.

Define goals and direction

Let’s take a look at this before we do the code. First, let’s make sure that our goal is to customize a ViewGroup, and we need to control its two child views for sliding transformation. On closer inspection, we can see that the two child views are superimposed on top of each other, so to reduce the code we can consider directly inheriting from one of the ViewGroup’s implementation classes: FrameLayout. The bottom is the menu view, superimposed on the main interface. Create a class: CoordinatorMenu and get two child Views after loading the layout

public class CoordinatorMenu extends FrameLayout {
    private View mMenuView;
    private View mMainView;

    // called after loading the layout file
    @Override
    protected void onFinishInflate(a) {
        mMenuView = getChildAt(0);// The first subview is at the bottom, as menu
        mMainView = getChildAt(1);// The second subview is at the top, as main
    }Copy the code

Prepare for the slide

There are many ways to do this, the most basic of which is to rewrite the onTouchEvent method in conjunction with Scroller, but it’s also the most complicated. Thankfully, a ViewDragHelper class is provided to help us implement it (essentially using Scroller). It is initialized in our constructor using the ViewDragHelper static method:

mViewDragHelper = ViewDragHelper.create(
    this, 
    TOUCH_SLOP_SENSITIVITY, 
    new CoordinatorCallback());Copy the code

The meanings of the three parameters:

  • The View you want to listen to. Here is the current control
  • The sensitivity of the initial touch slide, the higher the value, the more sensitive, 1.0F is the normal value
  • A Callback, the core logic of ViewDragHelper, has a custom implementation class

Then intercept the touch event and hand it over to our hero ViewDragHelper:

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    return mViewDragHelper.shouldInterceptTouchEvent(ev);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    // Pass the touch event to ViewDragHelper, which is necessary
    mViewDragHelper.processTouchEvent(event);
    return true;
}Copy the code

Handling computeScroll methods:

// called during sliding
@Override
public void computeScroll(a) {
    if (mViewDragHelper.continueSettling(true)) {
        ViewCompat.postInvalidateOnAnimation(this);// Process the refresh to achieve smooth movement}}Copy the code

Handle some Callback callbacks

// Tell ViewDragHelper which child View to drag
@Override
public boolean tryCaptureView(View child, int pointerId) {
    // The slide menu is off by default
    // The user must first touch the upper main interface
    return mMainView == child;
}

// Slide horizontally
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
    return left;Left refers to the position of the left edge of the view
}Copy the code

The main of sliding

Now we can drag the top child View– Main in the horizontal direction at will. Now we need to limit its horizontal sliding range, as shown below:

The position of Main after the menu is fully expanded


@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
    if (left < 0) {
        left = 0;// The initial position is the left edge of the screen
    } else if (left > mMenuWidth) {
        left = mMenuWidth;// The farthest distance is the width of the menu bar when it is fully expanded
    }
    return left;    
}Copy the code

Added rebound effect:

  • When the menu is closed, swipe from left to rightmainWhen, less than a certain distance to release the hand, need to let it bounce back to the left, otherwise directly open the menu
  • When the menu is fully open, swipe from right to leftmainWhen, less than a certain distance to release the hand, need to let it bounce back to the right, or directly close the menu

First judge the direction of sliding:

// Called when the view position changes, i.e. when dragging
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
    //dx represents the sliding distance after the last sliding interval
    if (dx > 0) {/ / is
        mDragOrientation = LEFT_TO_RIGHT;// From left to right
    } else if (dx < 0) {/ / negative
        mDragOrientation = RIGHT_TO_LEFT;// From right to left}}Copy the code

After letting go:

//View is called after release
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
    super.onViewReleased(releasedChild, xvel, yvel);
    if (mDragOrientation == LEFT_TO_RIGHT) {// Slide from left to right
        if (mMainView.getLeft() < mSpringBackDistance) {// Less than the set distance
            closeMenu();// Close the menu
        } else {
            openMenu();// Otherwise open the menu}}else if (mDragOrientation == RIGHT_TO_LEFT) {// Swipe from right to left
        if (mMainView.getLeft() < mMenuWidth - mSpringBackDistance){// Less than the set distance
            closeMenu();// Close the menu
        } else {
            openMenu();// Otherwise open the menu}}}public void openMenu(a) {
    mViewDragHelper.smoothSlideViewTo(mMainView, mMenuWidth, 0);
    ViewCompat.postInvalidateOnAnimation(CoordinatorMenu.this);
}

public void closeMenu(a) {
    mViewDragHelper.smoothSlideViewTo(mMainView, 0.0);
    ViewCompat.postInvalidateOnAnimation(CoordinatorMenu.this);
}Copy the code

The menu of the sliding

Once expanded, we can touch the underlying Menu view. We can’t drag menu itself, nor can we drag main, because we specified earlier that touches only apply to main. We can think for a moment that the bottom layer of QQ’s sliding menu moves with the top layer (if you are careful, you will find that it is not completely followed, there is a linear relationship between the distance changes between them, which will be discussed later). In this case, we can entrust the menu to main completely. There are two steps: 1. 2. Manage menu sliding when sliding on main. First we need to determine the initial position and size of menu, rewrite the Layout method, offset a mMenuOffset to the left

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    super.onLayout(changed, left, top, right, bottom);
    MarginLayoutParams menuParams = (MarginLayoutParams) mMenuView.getLayoutParams();
    menuParams.width = mMenuWidth;
    mMenuView.setLayoutParams(menuParams);
    mMenuView.layout(-mMenuOffset, top, mMenuWidth - mMenuOffset, bottom);
    }Copy the code

Let’s implement the first step: touch menu and hand it to Main. Before doing this, rewrite the previous callback method to make menu accept touch events

@Override
public boolean tryCaptureView(View child, int pointerId) {
    return mMainView == child || mMenuView == child;
}Copy the code

then

// Observe the view being touched
@Override
public void onViewCaptured(View capturedChild, int activePointerId) {
    if (capturedChild == mMenuView) {// When the view is touched is menu
        mViewDragHelper.captureChildView(mMainView, activePointerId);// pass to main}}Copy the code

After this step, we can drag main when our finger touches menu. This feeling is like pointing fingers, pointing to menu, scolding is main, ha ha.

Next, we realize the second step. Menu slides with main to see the following position diagram of Menu and main


From menu closed to Menu open: Menu moves its initial left offset, mMenuOffset, and main moves by exactly the width of Menu, mMenuWidth

So we can use the callback we used before: OnViewPositionChanged (View changedView, int left, int top, int dx, int dy), Then we can move the menu by dx * mMenuOffset/mMenuWidth. It looks great, but in practice it’s No! No! No! We need to rearrange the menu using the layout method to move it, and this method passes in an int value. The above formula is a float, and unfortunately this dx refers to the sliding distance after the last sliding interval. It’s a way of breaking up your whole slide into a lot of little pieces, and each piece is very short, and if you slide very slowly, then dx equals one in that very short period of time, hehe. So this calculation of the accuracy of serious loss, can not achieve the effect of synchronous movement. So we have to think differently and use another relationship between them: the distance between the left edge of menu and the left edge of main increases from mMenuOffset to mMenuWidth, when Main moves mMenuWidth. This increase can be considered linear, as shown in the figure below:



y = kx + d

mainLeft - menuLeft = (mMenuWidth - mMenuOffset) / mMenuWidth * mainLeft 
+ mMenuOffsetCopy the code

So override the onViewPositionChanged callback to make menu slide with main:

@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
    float scale = (float) (mMenuWidth - mMenuOffset) / (float) mMenuWidth;
    int menuLeft = left - ((int) (scale * left) + mMenuOffset);
    mMenuView.layout(menuLeft, mMenuView.getTop(),
            menuLeft + mMenuWidth, mMenuView.getBottom());
}Copy the code

I believe that if I do not give the above mathematical relationship solution, directly look at the code, you may be a face meng force, which is also a lot of custom control source difficult to read the reason.

Add a sliding gradient shadow to Main

After the above operation, I feel that the overall shape has been formed, but there is still one thing missing, that is, during the process of main from closed to fully opened through the menu, there will be a layer of shadow from transparent to opaque, see the following GIF to demonstrate:

The shadow changes



drawChild
menu
main
main

@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    boolean result = super.drawChild(canvas, child, drawingTime);// Finish drawing the original sub-view: menu and main

    int shadowLeft = mMainView.getLeft();// Shadow the left edge position
    final Paint shadowPaint = new Paint();// Shadow brush
    shadowPaint.setColor(Color.parseColor("#" + mShadowOpacity + "777777"));// Set the color of the opacity change for the brush
    shadowPaint.setStyle(Paint.Style.FILL);// Set the brush type fill
    canvas.drawRect(shadowLeft, 0, mScreenWidth, mScreenHeight, shadowPaint);// Draw a shadow

    return result;
}Copy the code

Where, mShadowOpacity changes with the position of main:

private String mShadowOpacity = "00"

@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
    float showing = (float) (mScreenWidth - left) / (float) mScreenWidth;
    int hex = 255 - Math.round(showing * 255);
    if (hex < 16) {
        mShadowOpacity = "0" + Integer.toHexString(hex);
    } else{ mShadowOpacity = Integer.toHexString(hex); }}Copy the code

At this point our menu is almost complete, but!

Some optimization is needed

1. If you open the menu, turn off the screen, and light up the screen again, the menu will return to the closed state. After the screen is lit up, the layout method will be called again.

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    super.onLayout(changed, left, top, right, bottom);
    MarginLayoutParams menuParams = (MarginLayoutParams) mMenuView.getLayoutParams();
    menuParams.width = mMenuWidth;
    mMenuView.setLayoutParams(menuParams);
    if (mMenuState == MENU_OPENED) {// Check the status of the menu is open
        // Keep the open position
        mMenuView.layout(0.0, mMenuWidth, bottom);
        mMainView.layout(mMenuWidth, 0, mMenuWidth + mScreenWidth, bottom);
        return;
    }
    mMenuView.layout(-mMenuOffset, top, mMenuWidth - mMenuOffset, bottom);
}

// Get the status of the menu
@Override
public void computeScroll(a) {
    if (mMainView.getLeft() == 0) {
        mMenuState = MENU_CLOSED;
    } else if(mMainView.getLeft() == mMenuWidth) { mMenuState = MENU_OPENED; }}Copy the code

2. Rotating the screen will also cause the above problems, so we need to call onSaveInstanceState and onRestoreInstanceState to save and restore the state of our menu respectively.

protected static class SavedState extends AbsSavedState {
    int menuState;// Record the value of the menu state

    SavedState(Parcel in, ClassLoader loader) {
        super(in, loader);
        menuState = in.readInt();
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        super.writeToParcel(dest, flags); dest.writeInt(menuState); }... . . }@Override
protected Parcelable onSaveInstanceState(a) {
    final Parcelable superState = super.onSaveInstanceState();
    final CoordinatorMenu.SavedState ss = new CoordinatorMenu.SavedState(superState);
    ss.menuState = mMenuState;// Save the state
    return ss;
}

@Override
protected void onRestoreInstanceState(Parcelable state) {
    if(! (stateinstanceof CoordinatorMenu.SavedState)) {
        super.onRestoreInstanceState(state);
        return;
    }

    final CoordinatorMenu.SavedState ss = (CoordinatorMenu.SavedState) state;
    super.onRestoreInstanceState(ss.getSuperState());

    if (ss.menuState == MENU_OPENED) {// The read state is open
        openMenu();// Open the menu}}Copy the code

2. Avoid overdrawing. There will be an overlap between Menu and main in the sliding process. The overlap part, namely the part covered by menu, does not need to be drawn. We only need to draw the displayed menu part, as shown in the figure:



drawChild

 @Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    final int restoreCount = canvas.save();// Save the current clipping information of the canvas

    final int height = getHeight();
    final int clipLeft = 0;
    int clipRight = mMainView.getLeft();
    if (child == mMenuView) {
        canvas.clipRect(clipLeft, 0, clipRight, height);// Crop the display area
    }

    boolean result = super.drawChild(canvas, child, drawingTime);// Draw the current view

    // Restore the clipping information saved before the canvas
    // Use the normal drawn view
    canvas.restoreToCount(restoreCount);
}Copy the code

Write in the last

At this point, our slide-out menu is both functional and optimized to handle some details. If sometimes meet don’t know how to realize the function, the solution is the best direction is to first take a look at the official ever realize such functions, search for answers to their source, for example I optimized the shadow map and excessive paint here is reference to the official controls DrawerLayout, reading the official source not only can let you realize the function, It can also motivate you and improve the quality of your code, like oh my God, the code was best written this way.

This article source address: github.com/bestTao/Coo… Please feel free to issue if you have any questions.

You can also introduce this control directly into your project:

  1. Start by adding the following code to the root directory of your projectbuild.gradle
    allprojects {
         repositories{... maven { url'https://jitpack.io'}}}Copy the code
  2. Reintroduce dependencies:
    dependencies {
             compile 'com. Making. BestTao: CoordinatorMenu: v1.0.2'
    }Copy the code

    For details and the latest version, please refer to[README.md]