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
- setSliderFadeColor: controls the fading color brightness of the sliding content according to the sliding distance
- setCoveredFadeColor: sets the brightness of the shadow on the left panel based on the sliding distance
- setPanelSlideListener: set the sliding monitor of the panel. You can set the effect for the bottom panel in the callback
- 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)