在上一篇文章中,我使用NestedScrollingParent
和NestedScrollingChild
并结合辅助类NestedScrollingParentHelper
和NestedScrollingChildHelper
实现了以下效果:
UI界面由三个部分组成,分别是图片、标题、主体,但是一般在现实需求中,标题默认是最上面的,那么,有没有办法让界面默认显示标题和主体,当下滑时才会显示图片,图片作为头部,顺便添加下尾部。
答案是必须有,下面开始分步骤说明。
首先看一下布局实现
<?xml version="1.0" encoding="utf-8"?>
<com.zyc.hezuo.aaademo.MyCustomNestedScrollingParent
android:id="@+id/nestedparent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
android:layout_width="match_parent"
android:layout_height="50dp"
android:textSize="20sp"
android:gravity="center"
android:background="#E9A8E9"
android:text="我是标题"/>
<com.zyc.hezuo.aaademo.MyCustomNestedScrollingChild
android:id="@+id/nestedchild"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFFFFF"
android:text="意内容我是任容我是任意内容我是任意内容我是任意内容我是任意内容我是任意内容我是任意内容我是任意内容我是任意内容意内容我是任容我是任意内容我是任意内容我是任意内容我是任意内容我是任意内容我是任意内容我是任意内容我是任意内容"
android:textSize="26sp"/>
</com.zyc.hezuo.aaademo.MyCustomNestedScrollingChild>
</com.zyc.hezuo.aaademo.MyCustomNestedScrollingParent>
使用自定义NestedScrollingParent和自定义NestedScrollingChild完成嵌套布局,这两个类的完整代码稍后贴出。
该布局的效果如下:
图片.png然后贴出自定义NestedScrollingChild的实现代码
public class MyCustomNestedScrollingChild extends LinearLayout implements NestedScrollingChild {
private NestedScrollingChildHelper mNestedScrollingChildHelper;
private final int[] offset = new int[2]; //偏移量
private final int[] consumed = new int[2]; //消费
private int lastY;
public MyCustomNestedScrollingChild(Context context) {
super(context);
}
public MyCustomNestedScrollingChild(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public MyCustomNestedScrollingChild(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//记录触摸时的Y轴方向
lastY = (int) event.getRawY();
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
break;
case MotionEvent.ACTION_MOVE:
int y = (int) (event.getRawY());
int dy = y - lastY;//dy为屏幕上滑动的偏移量
lastY = y;
dispatchNestedPreScroll(0, dy, consumed, offset);
break;
}
return true;
}
//初始化helper对象
private NestedScrollingChildHelper getScrollingChildHelper() {
if (mNestedScrollingChildHelper == null) {
mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
mNestedScrollingChildHelper.setNestedScrollingEnabled(true);
}
return mNestedScrollingChildHelper;
}
@Override
public void setNestedScrollingEnabled(boolean enabled) { //设置滚动事件可用性
getScrollingChildHelper().setNestedScrollingEnabled(enabled);
}
@Override
public boolean isNestedScrollingEnabled() {//是否可以滚动
return getScrollingChildHelper().isNestedScrollingEnabled();
}
@Override
public boolean startNestedScroll(int axes) {//开始滚动
Log.d("yunchong", "child ---- startNestedScroll");
return getScrollingChildHelper().startNestedScroll(axes);
}
@Override
public void stopNestedScroll() {//停止滚动,清空滚动状态
getScrollingChildHelper().stopNestedScroll();
}
@Override
public boolean hasNestedScrollingParent() {//判断是否含有对应的NestedScrollingParent
return getScrollingChildHelper().hasNestedScrollingParent();
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
//在子view进行滚动之后调用此方法,询问父view是否还要进行余下(unconsumed)的滚动。
//前四个参数为输入参数,用于告诉父view已经消费和尚未消费的距离,最后一个参数为输出参数,用于子view获取父view位置的偏移量。
//如果父view接收了它的滚动参数,进行了部分消费,则这个函数返回true,否则为false
return getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
//在子view自己进行滚动之前调用此方法,询问父view是否要在子view之前进行滚动。
//此方法的前两个参数用于告诉父View此次要滚动的距离;而第三第四个参数用于子view获取父view消费掉的距离和父view位置的偏移量。
return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return getScrollingChildHelper().dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return getScrollingChildHelper().dispatchNestedPreFling(velocityX, velocityY);
}
}
这段代码的核心都在onTouchEvent这个方法里,dispatchNestedPreScroll
方法将滑动偏移量传递给父view,startNestedScroll
确定父view是否会配合子view实现滑动。
最后,开始来自定义NestedScrollingParent,代码如下:
public class MyCustomNestedScrollingParent extends LinearLayout implements NestedScrollingParent {
//头部图片最大高度
private static final int MAX_HEIGHT = 500;
//头
private ImageView headerView;
//尾
private View footerView;
//目标view
private View childView;
public MyCustomNestedScrollingParent(Context context) {
this(context, null);
}
public MyCustomNestedScrollingParent(Context context, AttributeSet attrs) {
super(context, attrs);
//默认方向为垂直
setOrientation(LinearLayout.VERTICAL);
//头
headerView = new ImageView(context);
headerView.setScaleType(ImageView.ScaleType.CENTER_CROP);
headerView.setImageDrawable(getResources().getDrawable(R.mipmap.demo));
//尾
footerView = LayoutInflater.from(getContext()).inflate(R.layout.footer_layout, null, false);
}
//当前视图完全加载完毕,显示在屏幕上后执行
@Override
protected void onFinishInflate() {
super.onFinishInflate();
//找出目标view
for(int index = 0;index < getChildCount();index ++){
if(getChildAt(index) instanceof MyCustomNestedScrollingChild){
childView = getChildAt(index);
}
}
LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, MAX_HEIGHT);
//添加头
addView(headerView, 0, layoutParams);
//添加尾
addView(footerView, getChildCount());
// 上移,即隐藏header
scrollBy(0, MAX_HEIGHT);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
ViewGroup.LayoutParams params = childView.getLayoutParams();
//标题高度
int titleHeight = (int) (50 * getContext().getResources().getDisplayMetrics().density + 0.5);
params.height = getMeasuredHeight() - titleHeight;
}
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
//如果当前触摸的view是MyCustomNestedScrollingChild则返回true
if (target instanceof MyCustomNestedScrollingChild) {
return true;
}
return false;
}
@Override
public void onStopNestedScroll(View target) {
}
//先于child滚动
//前3个为输入参数,最后一个是输出参数
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
// 如果在自定义ViewGroup之上还有父View交给我来处理
getParent().requestDisallowInterceptTouchEvent(true);
//滑动父view
scrollBy(0, -dy);
consumed[1] = dy;
}
//后于child滚动
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
}
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
//是否消费了手指滑动事件
return false;
}
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
//是否消费了手指滑动事件
return false;
}
/**
* 限制滑动 移动y轴不能超出最大范围
*/
@Override
public void scrollTo(int x, int y) {
footerView.getLayoutParams().height = (int) (100 * getContext().getResources().getDisplayMetrics().density + 0.5);
if (y < 0) {
y = 0;
} else if (y > MAX_HEIGHT + footerView.getHeight()) {
y = MAX_HEIGHT + footerView.getHeight();
}
super.scrollTo(x, y);
}
}
最终效果如下:
50.gifMyCustomNestedScrollingParent的实现我大致说明一下:
(1)当屏幕中的MyCustomNestedScrollingParent加载完成时(包括所有的子视图),会执行onFinishInflate方法,所以,在这个方法里动态添加头和尾最为合适,计算头布局的高度(假设为y),最后将MyCustomNestedScrollingParent向上滑动距离为y的偏移量。代码如下:
//当前视图完全加载完毕,显示在屏幕上后执行
@Override
protected void onFinishInflate() {
super.onFinishInflate();
//找出目标view
for(int index = 0;index < getChildCount();index ++){
if(getChildAt(index) instanceof MyCustomNestedScrollingChild){
childView = getChildAt(index);
}
}
LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, MAX_HEIGHT);
//添加头
addView(headerView, 0, layoutParams);
//添加尾
addView(footerView, getChildCount());
// 上移,即隐藏header
scrollBy(0, MAX_HEIGHT);
}
但是,scrollBy之后,主题布局的高度不会变化,所以,必须要在scrollBy之前将提前修改主体布局的高度。在添加头和尾时,使用了addView方法,查看源码之后发现,一旦执行这个方法之后布局就会重新测量,执行onMeasure
方法,所以,完全可以在这个方法里调整主体布局的高度,如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
ViewGroup.LayoutParams params = childView.getLayoutParams();
//标题高度
int titleHeight = (int) (50 * getContext().getResources().getDisplayMetrics().density + 0.5);
params.height = getMeasuredHeight() - titleHeight;
}
在onStartNestedScroll方法中给出返回值,只有当目标view是MyCustomNestedScrollingChild才允许被滑动,代码如下:
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
//如果当前触摸的view是MyCustomNestedScrollingChild则返回true
if (target instanceof MyCustomNestedScrollingChild) {
return true;
}
return false;
}
当手指滑动主题布局时,父view也会跟着一起滑动,此时,自定义MyCustomNestedScrollingParent的onNestedPreScroll方法会不断被执行,代码如下:
//前3个为输入参数,最后一个是输出参数
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
// 如果在自定义ViewGroup之上还有父View交给我来处理
getParent().requestDisallowInterceptTouchEvent(true);
//滑动父view
scrollBy(0, -dy);
consumed[1] = dy;
}
最后,为了控制滑动范围,需要重写scrollTo方法,具体实现如下:
/**
* 限制滑动 移动y轴不能超出最大范围
*/
@Override
public void scrollTo(int x, int y) {
footerView.getLayoutParams().height = (int) (100 * getContext().getResources().getDisplayMetrics().density + 0.5);
if (y < 0) {
y = 0;
} else if (y > MAX_HEIGHT + footerView.getHeight()) {
y = MAX_HEIGHT + footerView.getHeight();
}
super.scrollTo(x, y);
}
下篇预知
以上案例中并没有发生Fling事件,因为在子view中并没有将Fling的速度值传递给父view,下一篇将实现Fling事件。
[本章完...]
网友评论