NestedScrolling机制(四)——最后一个例子

系列文章的最后,让我们来实现最初见到的那个饿了么店铺详情页效果吧。成品效果及对比如下:

算是低仿吧,主要是也没想要仿的一模一样(因为懒)。内容部分有两个列表和只有一个列表其实是一样的道理(NestedScrollingParent 1vs2 无压力);至于两个列表之间的连动,只要监听列表内容的位置然后自己处理一下就好。闲话不多说,下面进入正题。

1 activity_main.xml

根布局就是我们后面将要介绍的ElemeDetailView,这是一个自定义LinearLayout,实现了NestedScrollingParent接口。

对于edv_title、edv_header、edv_content这3个id需要注意一下,它们分别对应效果图中的标题栏(“魔兽世界”)、标题栏和内容之间的部分、内容(包含“拍卖行”和下面的列表),在ElemeDetailView中会通过这三个id来找到这三部分内容。

简便起见,这里的NestedScrollingChild使用了系统已经实现好的RecyclerView。

<?xml version="1.0" encoding="utf-8"?>
<cn.szx.elemedetailview.ElemeDetailView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/edv"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="cn.szx.elemedetailview.MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:background="@drawable/bg"
        android:orientation="vertical">

        <TextView
            android:id="@+id/edv_title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="#0066cc"
            android:padding="10dp"
            android:text="魔兽世界"
            android:textColor="#ffffff"
            android:textSize="20dp" />

        <TextView
            android:id="@+id/edv_header"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </LinearLayout>

    <LinearLayout
        android:id="@+id/edv_content"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="#0066cc"
            android:padding="10dp"
            android:text="拍卖行"
            android:textColor="#ffffff"
            android:textSize="18dp" />

        <android.support.v7.widget.RecyclerView
            android:id="@+id/rv"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#ffffff" />
    </LinearLayout>
</cn.szx.elemedetailview.ElemeDetailView>

2 ElemeDetailView.java

代码其实挺少的,注意几个点:

  1. 在onMeasure方法中需要重新测量自身的高度,在原本高度的基础上加上edv_header的高度,这样我们在向上滑的时候才不会滑出一片空白来。
  2. 在onNestedPreScroll方法中通过ViewCompat.offsetTopAndBottom()来移动edv_content。这只是移动方法之一,还有其他方法,比如通过设置layoutParams。
  3. 通过ViewTreeObserver的OnPreDrawListener来获取edv_content位置改变的信息(因为位置改变就必然涉及到重绘)。定义一个Listener接口并对外提供setListener方法,方便ElemeDetailView的使用者监听edv_content的位置改变。

这其实已经有点CoordinatorLayout的意思了,因为CoordinatorLayout的基本思想其实也就是监控一个子view的状态,并据此去改变另一个子view的状态。CoordinatorLayout的身份是一个协调者或者说是中介,目的是让它的多个子view之间能够相互配合动作————这也正是Coordinator(协调者)这个名字的由来。再来看看我们的项目,相互配合动作的子view有两组:一是通过NestedScrolling机制监听列表的滚动状态并据此改变edv_content的位置;二是通过viewTreeOberserver监听edv_content的位置并据此改变edv_titile的透明度(改变透明度的代码在MainActivity中,ElemeDetailView只是对外提供了监听接口)。

public class ElemeDetailView extends LinearLayout implements NestedScrollingParent {
    View edv_title, edv_header, edv_content;
    int titleHeight, headerHeight;
    NestedScrollingParentHelper helper = new NestedScrollingParentHelper(this);
    Listener listener;

    public ElemeDetailView(Context context) {
        super(context);
    }

    public ElemeDetailView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onFinishInflate() {
        edv_title = findViewById(R.id.edv_title);
        edv_header = findViewById(R.id.edv_header);
        edv_content = findViewById(R.id.edv_content);

        //监听edv_content的位置改变
        //edv_content的移动范围为titleHeight~titleHeight+headerHeight
        //据此算出一个百分比
        edv_content.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
            @Override
            public boolean onPreDraw() {
                if (listener != null) {
                    float fraction = (edv_content.getY() - titleHeight) / headerHeight;
                    listener.onContentPostionChanged(fraction);
                }
                return true;
            }
        });
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        titleHeight = edv_title.getMeasuredHeight();
        headerHeight = edv_header.getMeasuredHeight();
        //重新测量高度
        int newHeightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight() + headerHeight, MeasureSpec.EXACTLY);
        super.onMeasure(widthMeasureSpec, newHeightMeasureSpec);
    }

    public void setListener(Listener listener) {
        this.listener = listener;
    }

    interface Listener {
        void onContentPostionChanged(float fraction);
    }

//以下:NestedScrollingParent接口------------------------------

    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        return true;
    }

    //移动edv_content
    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        float supposeY = edv_content.getY() - dy;//希望edv_content移动到的位置

        //往上滑,y的边界为titleHeight
        if (dy > 0) {
            if (supposeY >= titleHeight) {
                offset(dy, consumed);
            } else {
                offset((int) (edv_content.getY() - titleHeight), consumed);
            }
        }

        //往下滑,y的边界为titleHeight + headerHeight
        if (dy < 0) {
            if (!ViewCompat.canScrollVertically(target, dy)) {//当target不能向下滑时
                if (supposeY <= titleHeight + headerHeight) {
                    offset(dy, consumed);
                } else {
                    offset((int) (edv_content.getY() - headerHeight - titleHeight), consumed);
                }
            }
        }
    }

    private void offset(int dy, int[] consumed) {
        //第二个参数为正代表向下,为负代表向上
        ViewCompat.offsetTopAndBottom(edv_content, -dy);
        consumed[0] = 0;
        consumed[1] = dy;
    }

    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {

    }

//以下:NestedScrollingParent接口的其他方法

    @Override
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
        helper.onNestedScrollAccepted(child, target, nestedScrollAxes);
    }

    @Override
    public void onStopNestedScroll(View target) {
        helper.onStopNestedScroll(target);
    }

    @Override
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
        return false;
    }

    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        return false;
    }

    @Override
    public int getNestedScrollAxes() {
        return helper.getNestedScrollAxes();
    }
}

3 MainActivity.java

MainActivity的工作也很简单,就是监听edv_content的位置改变,并实时改变edv_titile的透明度即可。

public class MainActivity extends AppCompatActivity {
    RecyclerView rv;
    ElemeDetailView edv;
    View edv_title;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initView();
    }

    private void initView() {
        rv = (RecyclerView) findViewById(R.id.rv);
        rv.setLayoutManager(new LinearLayoutManager(this));
        rv.setAdapter(new MyAdapter());

        //监听edv_content的位置改变,并改变edv_title的透明度
        edv = (ElemeDetailView) findViewById(R.id.edv);
        edv_title = findViewById(R.id.edv_title);
        edv.setListener(new ElemeDetailView.Listener() {
            @Override
            public void onContentPostionChanged(float fraction) {
                edv_title.setAlpha(1 - fraction);
            }
        });
    }

    class MyAdapter extends RecyclerView.Adapter {
		...
    }
}

以上就是关于NestedScrolling机制,我想说的全部内容了。NestedScrolling机制很强大,CoordinatorLayout+Behavior能实现的效果,我们把NestedScrolling拿来使劲捣腾一下也基本都能实现。

前面也说过,CoordinatorLayout+Behavior其实也是建立在NestedScrolling基础之上的。使用CoordinatorLayout+Behavior的优势在于实现常见的效果代码量更少,而直接使用NestedScrolling的优势则在于更接近底层,没有那些条条框框的限制,可以更加“为所欲为”。

后面有空的话,我也会把自己的CoordinatorLayout和Behavior的笔记整理分享出来,不过就只有使用方法了,源码印象中有好几千行,聊起来太累了。

项目代码下载

发布了46 篇原创文章 · 获赞 38 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/al4fun/article/details/53889144
今日推荐