SlidingPanelLayout
应用场景
SlidingPaneLayout 是Android在android-support-v4.jar中推出的一个支持测滑面板的布局。例如:头条详情页,知乎详情页…都是支持侧滑移除Activity的。通过抓取手机页面的xml,可以看到他们使用的技术也都是SlidingPaneLayout。如果你也想实现这么一个功能,那么请移步下面。
SlidingPaneLayout 出来那么长时间了,第一次使用它,汗颜~~~
常用API
- setSliderFadeColor :根据滑动的距离控制滑出的内容淡出的颜色亮度
- setCoveredFadeColor : 根据滑动的距离设置左侧面板的阴影的亮度
- setPanelSlideListener :设置面板的滑动监听,可以在回调中为底部面板设置效果
- mOverhangSize属性:滑动到边界时,内容面板与边界的距离。需要设置为0
使用方式
网上有一些说在XML中直接添加,我觉得这个扩展性不是太好,建议使用API,而不使用XML布局。
// 截屏: 这个方法不会截取底部导航栏,但是截取到的内容底部有一块同导航栏同高的白块。
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;
// 上一个activity的截图
public static Bitmap screenSlideBitmap;
protected void initSwipeBackFinish() {
// 1. 若底部背景不存在,则不使用左滑
if (screenBitmap == null) {
return;
}
SlidingPaneLayout paneLayout = new SlidingPaneLayout(this);
try {
// 当滑动到最后时,会预留出mOverhangSize的距离到边界,使用反射将预留距离设为0
Field overhang = SlidingPaneLayout.class.getDeclaredField("mOverhangSize");
overhang.setAccessible(true);
overhang.set(paneLayout, 0);
} catch (Exception exp) {
}
// 设置滑动监听
paneLayout.setPanelSlideListener(this);
// 根据滑动的距离控制滑出的内容淡出的颜色
paneLayout.setSliderFadeColor(Color.parseColor("#0fff0000"));
// 设置底部的背景图,可以在滑动监听中,为该view设置展示效果
slideBackImg = new ImageView(this);
slideBackImg.setImageBitmap(screenBitmap);
paneLayout.addView(slideBackImg);
ViewGroup decor = (ViewGroup) getWindow().getDecorView();
View decorChild = decor.getChildAt(0);
// 获取原decorChild距离底部的高度
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams)decorChild.getLayoutParams();
int paddingBottom = layoutParams.bottomMargin;
decor.removeView(decorChild);
decorChild.setBackgroundColor(getResources().getColor(android.R.color.white));
// 当底部的导航栏存在时:若不设置padding,会全屏展示,覆盖了底部的导航栏。
decorChild.setPadding(0,0,0,paddingBottom);
paneLayout.addView(decorChild, 1);
// 将paneLayout设置为第一个view
decor.addView(paneLayout,0);
}
@Override
public void onPanelSlide(@NonNull View panel, float slideOffset) {
// 滑动内容面板时,在这个回调这种做一些效果
changeSlideBackAlpha(slideOffset);
}
// 动态改变底部面板的亮度与大小
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();
// 内容面板完全出去后,取消activity的转场效果,否则会产生不好的交互体验
overridePendingTransition(0,0);
}
@Override
public void onPanelClosed(View panel) {
}
源码分析
// SlidingPaneLayout 是一个自定义容器,想一下:自定义ViewGroup,也许他重写了onMeasure与onLayout?
public class SlidingPaneLayout extends ViewGroup {
// BingGo,果然,你猜的没错,他重写了测量与布局。再想一下:测量大小时,他需要处理AT_MOST模式下的size吧?
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 看吧,他也处理了AT_MOST模式下的大小。
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;
}
// 确定完父布局的大小后,就要去测量子View想要的大小了。
final int childCount = getChildCount();
// 注意他建议使用两个子View,即上下两个面板。
if (childCount > 2) {
Log.e(TAG, "onMeasure: More than two child views are not supported.");
}
// 每次都计算得到可以滑动的View
mSlideableView = null;
boolean canSlide = false;
final int widthAvailable = widthSize - getPaddingLeft() - getPaddingRight();
int widthRemaining = widthAvailable;
// 测量子View的大小以及确定可滑动的view
for(int i = 0; i < childCount; ++i){
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// 相信你,如何测量子View的代码已经不用写了~~~(允许我偷懒一下)
child.measure(childWidthSpec, childHeightSpec);
// 想不想知道为什么是内容面板可以滑动,下面你会得到答案
// 是的,他每次返回的都是index=1的面板即内容面板,且做了可滑宽度的优化。
widthRemaining -= childWidth;
canSlide |= lp.slideable = widthRemaining < 0;
if (lp.slideable) {
mSlideableView = child;
}
}
// 测量滑动时上下面板的view
if(canSlide || weightSum > 0){
// 知道为什么使用时如果不将 mOverhangSize设为0,会距离边界有mOverhangSize的距离了吧
// 固定内容面板宽度限制值
final int fixedPanelWidthLimit = widthAvailable - mOverhangSize;
for (int i = 0; i < childCount; i++) {
// 跳过测量view宽度的标志
final boolean skippedFirstPass = lp.width == 0 && lp.weight > 0;
final int measuredWidth = skippedFirstPass ? 0 : child.getMeasuredWidth();
if(canSlide && child != mSlideableView){ // 处理底部面板
if(lp.width < 0 && (measuredWidth > fixedPanelWidthLimit || lp.height > 0)){
// 和我们看到的效果是一样的,底部面板的大小一直是那些没有变
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) { // 处理内容面板
// 想一下内容面板的效果,宽度一直在变,而高度不变。是不是有思路了那
final childHeightSpec = MeasureSpec.makeMeasureSpec(
child.getMeasuredHeight(), MeasureSpec.EXACTLY);
if(canSlide){
final int horizontalMargin = lp.leftMargin + lp.rightMargin;
// 由于widthAvailabley一直在变,因此newWidth也在变
final int newWidth = widthAvailable - horizontalMargin;
final int childWidthSpec = MeasureSpec.makeMeasureSpec(
newWidth, MeasureSpec.EXACTLY);
if (measuredWidth != newWidth) {
child.measure(childWidthSpec, childHeightSpec);
}
}
}
}
// 测量的最后
final int measureWidth = widthSize;
final int measureHeight = layoutHeight + getpaddingTop() + getPaddingBottom();
setMeasuredDimension(measuredWidth, measuredHeight);
// 至此,大小测量就完成了?....仔细想一下,是不是缺点啥?是的,还有滑动没处理
mCanSlide = canSlide;
if(mDragHelper.getViewDragState != ViewDragHelper.STATE_IDLE && !canSlide){
mDragHelper.abort();
}
// 至此,测量过程就完成了。去看下布局过程吧!
}
}
// 布局,顾名思义,他控制了View的显示位置,因此计算坐标是必须的
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 根据从左向右还是从右向左,获取子view距离边界的值
final int width = r - l; // 父布局的宽度
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;
// 滑动的偏移量
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){
// 获取子view与父布局的margin和
final int margin = lp.leftMargin + lp.rightMargin;
// 确定滑动了的区间(即内容面板距离开始边界的范围)
final int range = Math.min(nextStart , width - paddingEnd - mOverhangSize) - xStart - margin;
mSlideRange = range;
// 子view距离父布局的margin
final int lpMargin = isLayoutRtl?lp.rightMargin:lp.leftMargin;
// 判断可滑动区域是否超出了width
lp.dimWhenOffset = xStart + lpMargin + range + childWidth / 2 > width - paddingEnd;
// 计算每次移动的x坐标
final int pos = (int)(range * mSlideOffset)
// 计算内容布局下一次开始滑动的x坐标
xStart += pos + lpMargin;
// 计算下一次的偏移量
mSlideOffset = (float) pos / mSlideRange;
} else if (mCanSlide && mParallaxBy != 0) {
offset = (int) ((1 - mSlideOffset) * mParallaxBy);
xStart = nextXStart;
} else { // 底部面板的计算
xStart = nextXStart;
}
// 进行布局
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;
// 好了,布局也分析完了。可是既然他是支持滑动的自定义view,那么事件机制肯定要重写。那我们去看下事件吧
}
// 事件拦截只拦截down和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);
// 获取触摸启动的最小距离
final int slop = mDragHelper.getTouchSlop();
// 如果是垂直方向,则不拦截
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);
}
// 将事件交由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: {
// 抬起时,处理是否隐藏内容面板
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;
}
// 有没有一种恍然大明白的感觉,别慌,咱们还有一个难啃的骨头在后面,滑动时候是如何将偏移量传递到之前注册的监听上的?仔细想一下,咱们的事件是谁处理的?没错,是DragHelper!!!
public SlidingPaneLayout(){
// 在构造方法中,创建一个ViewDragHelper,并将该view与ViewDragHelper绑定。
mDragHelper = ViewDragHelper.create(this, 0.5f, new DragHelperCallback());
mDragHelper.setMinVelocity((400 * context.getResources().getDisplayMetrics().density));
}
// 看没看到Callback,没错他也是通过回调,来让view处理响应的。
private class DragHelperCallback extends ViewDragHelper.Callback{
// 当状态改变时调用
@Override
public void onViewDragStateChanged(int state) {
// 只处理IDLE状态
if (mDragHelper.getViewDragState() == ViewDragHelper.STATE_IDLE) {
// 调用OnPanelOpened
dispatchOnPanelOpened(mSlideableView);
}
}
// 当拖拽view时触发
@Override
public void onViewCaptured(View capturedChild, int activePointerId) {
// 设置上下面板可见
setAllChildrenVisible();
}
// 当每个view的位置发生改变时调用
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
// 根据距离边界的大小计算偏移量,并调用OnPanelSlide
onPanelDragged(left);
invalidate();
}
***
}
// 好了,整个的流程大致走了一遍,希望有所帮助。
}
总结
- 建议使用API的形式使用SlidingPaneLayout;
- 注意将内容面板滑动到边界时的距离(mOverhangSize)设置为0;
- 注意处理底部导航栏的问题;(将paneLayout设置为decorView的第一个View)
- 注意内容全屏显示的问题。(设置paddingBottom解决)
来源:CSDN
作者:子木-沐阳
链接:https://blog.csdn.net/VollyRequest/article/details/104009397