Learning and using SlidingPanelLayout

Keywords: Android xml Mobile

Article directory

SlidingPanelLayout

Application scenario

SlidingPaneLayout is a layout that Android launched in android-support-v4.jar to support sliding test panel. For example: headline details page, know details page It supports side sliding to remove activities. By grabbing the xml of mobile page, we can see that the technologies they use are all SlidingPaneLayout. If you want to implement such a function, please move to the next step.

It's been a long time since SlidingPaneLayout came out. The first time I use it, I'm ashamed~~~

Commonly used API

  1. setSliderFadeColor: controls the fading color brightness of the sliding content according to the sliding distance
  2. setCoveredFadeColor: sets the brightness of the shadow on the left panel based on the sliding distance
  3. setPanelSlideListener: set the sliding monitor of the panel. You can set the effect for the bottom panel in the callback
  4. Meverhangsize property: the distance between the content panel and the boundary when sliding to the boundary. Need to be set to 0

Usage mode

There are some on the Internet that say to add directly to XML. I don't think this extensibility is very good. It is recommended to use API instead of XML layout.

 // Screen capture: this method does not capture the bottom navigation bar, but there is a white block at the bottom of the captured content with the same height as the navigation bar.
private static Bitmap captureScreen(Activity activity){
        View cv = activity.getWindow().getDecorView();
        cv.setDrawingCacheEnabled(true);
        cv.buildDrawingCache();
        Bitmap drawingCache = Bitmap.createBitmap(cv.getDrawingCache());
        if(drawingCache != null){
            drawingCache.setHasAlpha(false);
            drawingCache.prepareToDraw();
        }
        return drawingCache;
    }
private ImageView slideBackImg;
// Screenshot of the previous activity
public static Bitmap screenSlideBitmap;
protected void initSwipeBackFinish() {
        // 1. If the bottom background does not exist, the left slide is not used
        if (screenBitmap == null) {
            return;
        }
        SlidingPaneLayout paneLayout = new SlidingPaneLayout(this);
        try {
            // When sliding to the end, a distance of meverhangsize to the boundary is reserved, and the reserved distance is set to 0 using reflection
            Field overhang = SlidingPaneLayout.class.getDeclaredField("mOverhangSize");
            overhang.setAccessible(true);
            overhang.set(paneLayout, 0);
        } catch (Exception exp) {

        }
        // Set sliding monitor
        paneLayout.setPanelSlideListener(this);
        // Control the fading color of the content according to the sliding distance
        paneLayout.setSliderFadeColor(Color.parseColor("#0fff0000"));
        
        // Set the background image at the bottom. You can set the display effect for this view in sliding monitoring
        slideBackImg = new ImageView(this);
        slideBackImg.setImageBitmap(screenBitmap);
        paneLayout.addView(slideBackImg);

        ViewGroup decor = (ViewGroup) getWindow().getDecorView();
        View decorChild =  decor.getChildAt(0);
        // Get the height of the original decorChild from the bottom
        ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams)decorChild.getLayoutParams();
        int paddingBottom = layoutParams.bottomMargin;
        decor.removeView(decorChild);
        decorChild.setBackgroundColor(getResources().getColor(android.R.color.white));
        // When the bottom navigation bar exists: if padding is not set, it will be displayed in full screen, covering the bottom navigation bar.
        decorChild.setPadding(0,0,0,paddingBottom);
        paneLayout.addView(decorChild, 1);
        // Set paneLayout to the first view
        decor.addView(paneLayout,0);
    }
    
    @Override
    public void onPanelSlide(@NonNull View panel, float slideOffset) {
        // When sliding the content panel, you can make some effect in this callback
        changeSlideBackAlpha(slideOffset);
    }
    
    // Dynamically change the brightness and size of the bottom panel
    private void changeSlideBackAlpha(float factor) {
        slideBackImg.setScaleX(0.9f + factor * 0.1f);
        slideBackImg.setScaleY(0.9f + factor * 0.1f);
        if (factor < 0.3F) {
            factor = 0.3f;
        }
        slideBackImg.setAlpha(factor);
    }

    @Override
    public void onPanelOpened(@NonNull View panel) {
        finish();
        // After the content panel goes out completely, cancel the transition effect of activity, otherwise bad interaction experience will be generated
        overridePendingTransition(0,0);
    }
    
    @Override
    public void onPanelClosed(View panel) {
    }

Source code analysis

// SlidingPaneLayout is a custom container. Think about it: Custom ViewGroup, maybe it rewrites onMeasure and onLayout?
public class SlidingPaneLayout extends ViewGroup {
// BingGo, sure enough, you guessed right. He rewrites the measurement and layout. Think again: when measuring the size, does he need to deal with the size in at? Most mode?
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // See, he also deals with the size in at? Most mode.
        if(widthMode != MeasureSpec,EXACTLY){
            if (widthMode == MeasureSpec.AT_MOST) {
                    widthMode = MeasureSpec.EXACTLY;}
        }
        switch (heightMode) {
            case MeasureSpec.EXACTLY:
                layoutHeight = maxLayoutHeight = heightSize - getPaddingTop() - getPaddingBottom();
                break;
            case MeasureSpec.AT_MOST:
                maxLayoutHeight = heightSize - getPaddingTop() - getPaddingBottom();
                break;
        }
        // After determining the size of the parent layout, you need to measure the size of the child View.
        final int childCount = getChildCount();
        // Note that he recommends using two sub views, the upper and lower panels.
        if (childCount > 2) {
            Log.e(TAG, "onMeasure: More than two child views are not supported.");
        }
        // Every time, we get a sliding View
        mSlideableView = null;
        boolean canSlide = false;
        final int widthAvailable = widthSize - getPaddingLeft() - getPaddingRight();
        int widthRemaining = widthAvailable;
         // Measure the size of sub view and determine the sliding view
        for(int i = 0; i < childCount; ++i){
           
            final View child = getChildAt(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            // Trust you, there's no need to write the code to measure the subview
            child.measure(childWidthSpec, childHeightSpec);
            // If you want to know why the content panel can slide, you will get the answer below
            // Yes, every time he returns the panel with index=1, that is, the content panel, and he optimizes the sliding width.
            widthRemaining -= childWidth;
            canSlide |= lp.slideable = widthRemaining < 0;
            if (lp.slideable) {
                mSlideableView = child;
            }
        }
        // view of upper and lower panel when measuring sliding
        if(canSlide || weightSum > 0){
            // You know why if you don't set meverhangsize to 0, you will have a meverhangsize distance from the boundary
            // Fixed content panel width limit
            final int fixedPanelWidthLimit = widthAvailable - mOverhangSize;
            
            for (int i = 0; i < childCount; i++) {
                // Skip flag to measure view width
                final boolean skippedFirstPass = lp.width == 0 && lp.weight > 0;
                final int measuredWidth = skippedFirstPass ? 0 : child.getMeasuredWidth();
                if(canSlide && child != mSlideableView){ // Working with the bottom panel
                    if(lp.width < 0 && (measuredWidth > fixedPanelWidthLimit || lp.height > 0)){
                    // As we can see, the size of the bottom panel is always the same
                        final childHeightSpec = MeasureSpec.makeMeasureSpec(
                                    child.getMeasuredHeight(), MeasureSpec.EXACTLY);
                        final int childWidthSpec = MeasureSpec.makeMeasureSpec(
                                fixedPanelWidthLimit, MeasureSpec.EXACTLY);
                        child.measure(childWidthSpec, childHeightSpec);
                    }
                }else if (lp.weight > 0) { // Process content panel
                    // Think about the effect of the content panel. The width changes all the time, but the height does not change. Is there a way of thinking
                    final childHeightSpec = MeasureSpec.makeMeasureSpec(
                                child.getMeasuredHeight(), MeasureSpec.EXACTLY);
                    if(canSlide){
                        final int horizontalMargin = lp.leftMargin + lp.rightMargin;
                        // Because widthavailableley has been changing, so has newWidth
                        final int newWidth = widthAvailable - horizontalMargin;
                        final int childWidthSpec = MeasureSpec.makeMeasureSpec(
                                newWidth, MeasureSpec.EXACTLY);
                        if (measuredWidth != newWidth) {
                            child.measure(childWidthSpec, childHeightSpec);
                        }        
                    }
                }
            }
            // At the end of the measurement
            final int measureWidth = widthSize;
            final int measureHeight = layoutHeight + getpaddingTop() + getPaddingBottom();
            setMeasuredDimension(measuredWidth, measuredHeight);
            // At this point, the size measurement is completed? ... think about it carefully. Are there any shortcomings? Yes, there's still sliding
            mCanSlide = canSlide;
            if(mDragHelper.getViewDragState != ViewDragHelper.STATE_IDLE && !canSlide){
                mDragHelper.abort();
            }
            // At this point, the measurement process is completed. Take a look at the layout process!
        }
        
    }
    // Layout, as the name implies, controls the display position of View, so it is necessary to calculate coordinates
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        // Gets the value of the distance boundary of the child view according to whether it is left to right or right to left
        final int width = r - l; // Width of parent layout
        final int paddingStart = isLayoutRtl ? getPaddingRight() : getPaddingLeft();
        final int paddingEnd = isLayoutRtl ? getPaddingLeft() : getPaddingRight();
        final int paddingTop = getPaddingTop();

        final int childCount = getChildCount();
        int xStart = paddingStart;
        int nextXStart = xStart;
        
        // Offset of slide
        if (mFirstLayout) {
            mSlideOffset = mCanSlide && mPreservedOpenState ? 1.f : 0.f;
        }
        
        for (int i = 0; i < childCount; i++) {
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            if(lp.slideable){
                // Get the margin sum of the child view and the parent layout
                final int margin = lp.leftMargin + lp.rightMargin;
                // Determine the sliding range (i.e. the range between the content panel and the start boundary)
                final int range = Math.min(nextStart , width - paddingEnd - mOverhangSize) - xStart - margin;
                mSlideRange = range;
                // The distance between child view and parent layout
                final int lpMargin = isLayoutRtl?lp.rightMargin:lp.leftMargin;
                // Determine whether the sliding area exceeds the width
                lp.dimWhenOffset = xStart + lpMargin + range + childWidth / 2 > width - paddingEnd;
                // Calculate the x-coordinate of each move
                final int pos = (int)(range * mSlideOffset)
                // Calculate the x-coordinate of the next slide of the content layout
                xStart += pos + lpMargin;
                // Calculate the next offset
                mSlideOffset = (float) pos / mSlideRange;
            } else if (mCanSlide && mParallaxBy != 0) {
                offset = (int) ((1 - mSlideOffset) * mParallaxBy);
                xStart = nextXStart;
            } else { // Calculation of bottom panel
                xStart = nextXStart;
            }
            // Layout
            final int childLeft = xStart - offset;
            final int childRight = childLeft + childWidth;
            final int childTop = paddingTop;
            final int childBottom = childTop + child.getMeasuredHeight();
            child.layout(childLeft, paddingTop, childRight, childBottom);
            
            nextXStart += child.getWidth();nextXStart += child.getWidth();
        }
        **** 
        mFirstLayout = false;
        // OK, the layout is also analyzed. But since it supports sliding custom view, the event mechanism must be rewritten. Let's go and see the event
    }
    // Event interception only intercepts down and move
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        final int action = ev.getMaskAction();
        ****
        boolean interceptTap = false;
        final float x = ev.getX();
        final float y = ev.getY();
        switch(action){
            case MotionEvent.ACTION_DOWN: {
                mIsUnableToDrag = false;
                mInitialMotionX = x;
                mInitialMotionY = y;

                if (mDragHelper.isViewUnder(mSlideableView, (int) x, (int) y)
                        && isDimmed(mSlideableView)) {
                    interceptTap = true;
                }
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                final float adx = Math.abs(x - mInitialMotionX);
                final float ady = Math.abs(y - mInitialMotionY);
                // Get the minimum distance of touch start
                final int slop = mDragHelper.getTouchSlop();
                // If vertical, no interception
                if (adx > slop && ady > adx) {
                    mDragHelper.cancel();
                    mIsUnableToDrag = true;
                    return false;
                }
            }
        }
        final boolean interceptForDrag = mDragHelper.shouldInterceptTouchEvent(ev);
        return interceptForDrag || interceptTap;
    }
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if(!mCanSlide){
            return super.onTouchEvent(ev);
        }
        // Submit the event to DragHelper
        mDragHelper.processTouchEvent(ev);
        boolean wantTouchEvents = true;
        int action = ev.getMaskAction();
        final float x = ev.getX();
        final float y = ev.getY();
        switch(action){
            case MotionEvent.ACTION_DOWN: {
                mInitialMotionX = x;
                mInitialMotionY = y;
                break;
            }

            case MotionEvent.ACTION_UP: {
            // Handle whether to hide the content panel when lifting
                if (isDimmed(mSlideableView)) {
                    final float dx = x - mInitialMotionX;
                    final float dy = y - mInitialMotionY;
                    final int slop = mDragHelper.getTouchSlop();
                    if (dx * dx + dy * dy < slop * slop
                            && mDragHelper.isViewUnder(mSlideableView, (int) x, (int) y)) {
                        // Taps close a dimmed open pane.
                        closePane(mSlideableView, 0);
                        break;
                    }
                }
                break;
            }
        }
        return wantTouchEvents;
    }
    // Is there a sense of suddenly understanding? Don't panic. We have a hard bone in the back. How to transfer the offset to the previously registered monitor when sliding? Think about it carefully. Who dealt with our affairs? Yes, it's DragHelper!!!
    public SlidingPaneLayout(){
        // In the construction method, create a ViewDragHelper and bind the view to the ViewDragHelper.
        mDragHelper = ViewDragHelper.create(this, 0.5f, new DragHelperCallback());
        mDragHelper.setMinVelocity((400 * context.getResources().getDisplayMetrics().density));
    }
    // Do you see the Callback? Yes, it is through Callback to let view handle the response.
    private class DragHelperCallback extends ViewDragHelper.Callback{
        // Called when the state changes
        @Override
         public void onViewDragStateChanged(int state) {
         // Handle only IDLE status
             if (mDragHelper.getViewDragState() == ViewDragHelper.STATE_IDLE) {
                // Call onpaneloped
                 dispatchOnPanelOpened(mSlideableView);
             }
         }
         // Triggered when view is dragged
         @Override
        public void onViewCaptured(View capturedChild, int activePointerId) {
            // Set the upper and lower panels visible
            setAllChildrenVisible();
        }
        // Called when the location of each view changes
        @Override
         public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            // Calculate the offset based on the size of the distance boundary and call OnPanelSlide
            onPanelDragged(left);
            invalidate();   
         }
         ***
    }
    // OK, I've walked through the whole process. I hope it helps.
}

summary

  • It is recommended to use SlidingPaneLayout in the form of API;
  • Note that the distance (meverhangsize) to slide the content panel to the boundary is set to 0;
  • Pay attention to the problem of dealing with the bottom navigation bar; (set paneLayout to the first view of decorView)
  • Pay attention to the problem of full screen content display. (set paddingBottom to solve)
Published 7 original articles, won praise 1, visited 37
Private letter follow

Posted by 7724 on Thu, 16 Jan 2020 03:19:43 -0800