Android 侧滑布局逻辑解析
一、前言
测滑布局应用非常广泛,HorizontalScrollView 本身实现的滑动效果让实现变得很简单,实际上有很多种方式实现,有很多现有的方法可以直接调用。
二、逻辑实现
在Android 中,滑动分为2类,一类以ScrollView为代表布局,通过子View实现布局超出视区(ViewPort)之后,进行Scroll操作的,另一类事以修改Offset为代表的Recycler类,前者实时保持最大高度。形像的理解为前者是“齿轮传动派”,后者是“滑板派”,两派都有过出分头的时候,即便是个派弟子如NestedScrollView和RecyclerView争的你死我活,不过总体上齿轮传动派占在弱势地位。不过android的改版,让他们做了很多和平相处的事情,不如NestedScrolling机制的支持,让他们想传动就传动,想滑翔就滑翔。
齿轮传动派看家本领
- scrollX,ScrollY,scrollTo等方法
- 一个长得很长的独生子
滑板派的看家本领
- offsetXXX方法
- 被魔改的ScrollXXX
- 一群会滑板的孩子
- layout 方法也是他们的榜首
前者为了实现的简单的滑动,后者空间可以无限大,期间还可自由换孩子。
三、代码实现
有很多现成的example都是基于齿轮传动派的,但是如果使用,你得记住,齿轮传动派会的滑板派一定会,反过来就不一样了。
这里我们使用layout方法实现,核心代码
View leftView = mWrapperView.getChildAt(0);
leftView.layout(l,t,l+leftView.getWidth(),t+leftView.getHeight());
maskAlpha = leftView.getLeft()*1.0f/leftView.getWidth();
之所以使用layout的原因是很多人都不记得ListView可以使用该方法实现吸顶效果,而RecyclerView因为为了兼容更多类型,导致他使用这个很难实现吸顶,但是没关系,child.layout和child.measure方法可以在类的任何地方调用,这个是必须要掌握的。
3.1 布局初始化
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if(isFirstLayout && getRealChildCount()==2){
View leftView = mWrapperView.getChildAt(0);
scrollTo(leftView.getWidth(),0); //初始化状态让右侧View展示处理
}
isFirstLayout = true;
}
3.2 相对运动
滑动时让左侧View保持同样的滑动距离和方向
View leftView = mWrapperView.getChildAt(0);
leftView.layout(l,t,l+leftView.getWidth(),t+leftView.getHeight());
maskAlpha = leftView.getLeft()*1.0f/leftView.getWidth();
3.3 全部代码
public class SlidingFoldLayout extends HorizontalScrollView {
private TextPaint mPaint = null;
private LinearLayout mWrapperView = null;
private boolean isFirstLayout = true;
private float maskAlpha = 1.0f;
public SlidingFoldLayout(Context context) {
this(context, null);
}
public SlidingFoldLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SlidingFoldLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
LinearLayout linearLayout = getWrapperLayout(context);
setOverScrollMode(View.OVER_SCROLL_NEVER);
setWillNotDraw(false);
mPaint = createPaint();
addViewInLayout(linearLayout, 0, linearLayout.getLayoutParams(), true);
mWrapperView = linearLayout;
}
public LinearLayout getWrapperLayout(Context context) {
LinearLayout linearLayout = new LinearLayout(context);
HorizontalScrollView.LayoutParams lp = generateDefaultLayoutParams();
lp.width = LayoutParams.WRAP_CONTENT;
linearLayout.setLayoutParams(lp);
linearLayout.setOrientation(LinearLayout.HORIZONTAL);
linearLayout.setPadding(0, 0, 0, 0);
return linearLayout;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int childCount = mWrapperView.getChildCount();
if (childCount == 0) {
return;
}
int leftMenuWidth = mWrapperView.getChildAt(0).getMeasuredWidth();
ViewGr0up.LayoutParams lp = (ViewGr0up.LayoutParams) getLayoutParams();
int width = getMeasuredWidth() - getPaddingRight() - getPaddingLeft();
if (lp instanceof ViewGr0up.MarginLayoutParams) {
width = width - ((MarginLayoutParams) lp).leftMargin - ((MarginLayoutParams) lp).rightMargin;
}
if (width <= leftMenuWidth) {
mWrapperView.getChildAt(0).getLayoutParams().width = (int) (width - dp2px(50));
measureChild(mWrapperView, widthMeasureSpec, heightMeasureSpec);
}
if (childCount != 2) {
return;
}
View rightView = mWrapperView.getChildAt(1);
int rightMenuWidth = rightView.getMeasuredWidth();
if (width != rightMenuWidth) {
rightView.getLayoutParams().width = width;
measureChild(mWrapperView, widthMeasureSpec, heightMeasureSpec);
rightView.bringToFront();
}
}
private float dp2px(int dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
}
@Override
public void addView(View child) {
int childCount = mWrapperView.getChildCount();
if (childCount > 2) {
throw new IllegalStateException("SlidingFoldLayout should host only two child");
}
ViewGr0up.LayoutParams lp = child.getLayoutParams();
if (lp != null && lp instanceof LinearLayout.LayoutParams) {
lp = new LinearLayout.LayoutParams(lp);
child.setLayoutParams(lp);
}
mWrapperView.addView(child);
}
public int getRealChildCount() {
if (mWrapperView == null) {
return 0;
}
return mWrapperView.getChildCount();
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if(isFirstLayout && getRealChildCount()==2){
View leftView = mWrapperView.getChildAt(0);
scrollTo(leftView.getWidth(),0);
}
isFirstLayout = true;
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
int realCount = getRealChildCount();
if(realCount!=2) return;
View leftView = mWrapperView.getChildAt(0);
leftView.layout(l,t,l+leftView.getWidth(),t+leftView.getHeight());
maskAlpha = leftView.getLeft()*1.0f/leftView.getWidth();
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action){
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_OUTSIDE:
super.onTouchEvent(ev);
scrollToTraget();
break;
}
return super.onTouchEvent(ev);
}
private void scrollToTraget() {
int count = getRealChildCount();
if(count!=2) return;
int with = getWidth();
if(with==0) return;
View leftView = mWrapperView.getChildAt(0);
float x = leftView.getLeft()*1.0f/leftView.getWidth();
if(x > 0.5f){
smoothScrollTo(leftView.getWidth(),0);
}else{
smoothScrollTo(0,0);
}
}
@Override
public void addView(View child, int index) {
int childCount = mWrapperView.getChildCount();
if (childCount > 2) {
throw new IllegalStateException("SlidingFoldLayout should host only two child");
}
ViewGr0up.LayoutParams lp = child.getLayoutParams();
if (lp != null && lp instanceof LinearLayout.LayoutParams) {
lp = new LinearLayout.LayoutParams(lp);
child.setLayoutParams(lp);
}
mWrapperView.addView(child, index);
}
@Override
public void addView(View child, ViewGr0up.LayoutParams params) {
int childCount = mWrapperView.getChildCount();
if (childCount > 2) {
throw new IllegalStateException("SlidingFoldLayout should host only two child");
}
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(params);
child.setLayoutParams(lp);
mWrapperView.addView(child, lp);
}
@Override
public void addView(View child, int index, ViewGr0up.LayoutParams params) {
int childCount = mWrapperView.getChildCount();
if (childCount > 2) {
throw new IllegalStateException("SlidingFoldLayout should host only two child");
}
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(params);
child.setLayoutParams(lp);
mWrapperView.addView(child, index);
}
private TextPaint createPaint() {
// 实例化画笔并打开抗锯齿
TextPaint paint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
paint.setAntiAlias(true);
return paint;
}
RectF rectF = new RectF();
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
int realCount = getRealChildCount();
if(realCount!=2) return;
View leftView = mWrapperView.getChildAt(0);
View rightView = mWrapperView.getChildAt(1);
rectF.top = leftView.getTop();
rectF.bottom = leftView.getBottom();
rectF.left = leftView.getLeft();
rectF.right = rightView.getLeft();
int alpha = (int) (153*maskAlpha);
mPaint.setColor(argb(alpha,0x00,0x00,0x00));
int saveId = canvas.save();
canvas.drawRect(rectF,mPaint);
canvas.restoreToCount(saveId);
}
public static int argb(
int alpha,
int red,
int green,
int blue) {
return (alpha << 24) | (red << 16) | (green << 8) | blue;
}
}
三、使用方式
使用方式简单清晰,没有看到ScrollView的独生子,原因是我们把他写到了类里面
<com.cn.scrolllayout.view.SlidingFoldLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="300dp"
android:layout_height="match_parent"
android:gravity="center"
>
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@mipmap/img_sample_text"
/>
</LinearLayout>
<LinearLayout
android:layout_width="500dp"
android:layout_height="match_parent"
android:background="@color/colorAccent"
>
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitCenter"
android:src="@mipmap/img_sample_panda"
/>
</LinearLayout>
</com.cn.scrolllayout.view.SlidingFoldLayout>
四、总结
掌握ScrollX和OffsetX两种的滑动很重要,但是不能忘记layout的作用,本质上他属于一种OffsetX上层的封装。
来源:juejin.cn/post/7307989656288034851