1-Android开发知识

CoordinatorLayout遇到问题及个人总结

2018-10-09  本文已影响180人  mandypig

重点:CoordinatorLayout中对于子view滑动时的处理思考,子view控件高度改变逻辑处理

最近花了一些时间去看了CoordinatorLayout的源码,感觉google在5.0之后推出的众多新控件中,最难搞定的有两个,一个是recyclerview另一个就是CoordinatorLayout了。recyclerview大家都会使用,但是具体涉及到的东西还是蛮多了,光看源码近两万行也就知道这不是一个简单的控件,有时间需要深入去看看关于recyclerview优化的一些文章。另一个头大的控件就是CoordinatorLayout,CoordinatorLayout和appbarlayout,recyclerview,nestedscrollview,CollapsingToolbarLayout等配合使用外加不同的xml属性设置往往能实现很多不同的效果,要想真正掌握该控件说实话还真必须去看源码,不然behavior里面的众多方法都能把你看晕,本来想写一篇文章分析下CoordinatorLayout的源码,但发现网上有不少文章,也写不出什么新意就不写了,这里就简单说下对于CoordinatorLayout源码的理解以及CoordinatorLayout中子view滑动处理中一个很细节的东西

behavior

CoordinatorLayout中相当重要的一个类,相当于代理的作用,CoordinatorLayout中的onmeasure和onlayout可以通过各个子view绑定的behavior来实现measure和layout,这里说一下我当初多个疑惑点,CoordinatorLayout最简单的一个使用场景就是内嵌一个appbarlayout和nestedscrollview。如下

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="300dp">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#00ffff"
            android:gravity="center"
            android:text="这是一个appbar"
            android:textSize="20dp"
            app:layout_scrollFlags="scroll" />
    </android.support.design.widget.AppBarLayout>

    <android.support.v4.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <TextView
                android:layout_width="match_parent"
                android:layout_height="150dp"
                android:background="#ff00ff"
                android:gravity="center"
                android:text="scrollview第1个item"
                android:textSize="15dp" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="150dp"
                android:background="#00ff00"
                android:gravity="center"
                android:text="scrollview第2个item"
                android:textSize="15dp" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="150dp"
                android:background="#0000ff"
                android:gravity="center"
                android:text="scrollview第3个item"
                android:textSize="15dp" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="150dp"
                android:background="#ffff00"
                android:gravity="center"
                android:text="scrollview第4个item"
                android:textSize="15dp" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="150dp"
                android:background="#339df5"
                android:gravity="center"
                android:text="scrollview第5个item"
                android:textSize="15dp" />
        </LinearLayout>
    </android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>
最终效果如图 QQ截图20181008194038.png

疑惑一:nestedscrollview的高度如何计算

关键代码就在HeaderScrollingViewBehavior的onMeasureChild中

final int height = availableHeight - header.getMeasuredHeight() + getScrollRange(header);

availableHeight就是CoordinatorLayout的高度,- header.getMeasuredHeight() + getScrollRange(header);就是不可滚动高度,和minheight属性有关。最终会将height设置给nestedscrollview。

疑惑二:展示出来的页面中为什么nestedscrollview会被放到appbarlayout的下面

答案就和ScrollingViewBehavior有关,CoordinatorLayout的onlayout最终会调用到ScrollingViewBehavior的layoutChild方法,该方法内部在排布nestedscrollview时会依据appbarlayout的位置,将nestedscrollview放到appbarlayout的下面。

疑惑三:onNestedPreScroll,onNestedScroll区别

nestedscrollview滑动的偏移量会首先调用到onNestedPreScroll,如果onNestedPreScroll没有消耗或者说没有消耗全部,这时会调用nestedscrollview自身的scroll方法进行消耗,处理完毕之后如果偏移量还没有消耗完毕这时又会调用onNestedScroll方法,这就是他们的区别,一个先于nestedscrollview消耗,一个后于nestedscrollview。就是这么简单

疑惑四:手指按在nestedscrollview中进行滚动时,为什么appbarlayout和nestedscrollview都会进行滚动

这里面的主要原理就是网上常说的nestscrolling机制了,这个说法我最早是在鸿洋大神的博客看到的,Android NestedScrolling机制完全解析 带你玩转嵌套滑动,在鸿洋文章中他自己实现了一个简易的类似CoordinatorLayout的StickyNavLayout,当手指在recyclerview上滑动的时候通过在StickyNavLayout中处理dy偏移量最终调用scrollby方法实现整体移动的效果。但是CoordinatorLayout并不是通过scrollby方法实现整体的移动的,它的实现原理是对appbarlayout进行偏移,因为ScrollingViewBehavior是依赖于appbarlayout的,所以最终会调用到onDependentViewChanged方法,该方法内部就会根据最新的appbarlayout位置来重新计算nestedscrollview的位置最终实现整体偏移的效果,这一点上和鸿洋的实现有一些不同,但最终展示出来的效果是一样的。

理解了以上几点那么再去看CoordinatorLayout就会更有目的性,当然上述都是自己个人看法,可能有些地方理解还不到位,有点扯远了,看文章标题也知道,上述都不是这篇文章的重点,现在来说下我真正想写这篇文章的意图,接下来分享下自己在看nestedscrollview源码中遇到的一个处理滑动的理解。先来说下处理滑动的逻辑

常规逻辑

思路类似,在down中保存lasty,在move中进行逻辑判断是否是滑动状态,如果dy超过了slot的阈值就开始滑动,并且把lasty设置为move中获取到的最新值。

固定view上手指滑动和CoordinatorLayout中子view上手指滑动区别

结合文章开头的布局想一下,当手指按在nestedscrollview向上滑动时并且appbarlayout还处于未完全消失状态,此时nestedscrollview的ontouchevent中event.gety获取到的值会是一个怎样的变化,如果你觉得当手指向上移动event.gety是一个逐渐递减的过程,那么你就理解错了,这个值实际的变化是在一个小范围内来回震动的过程,如果不相信的话可以自己写个demo测试下就知道结果了。其实要理解这个结果也很简单,因为nestedscrollview的位置是会根据手指移动而移动的,gety的含义就是手指离接受ontouchevent事件的view顶部的距离。当nestedscrollview随着手指一起移动时严格来说gety的值应该是一直不变才对,这就是CoordinatorLayout中子view上手指滑动和固定view上手指滑动最大的一个区别

CoordinatorLayout中各个view是如何一起滑动的

这个问题似乎和疑惑四是一样的,但如果好好想下上面固定view滑动和CoordinatorLayout中子view上手指滑动,就会产生一个问题,在appbarlayout未完全消失的情况下,nestedscrollview每次获取到的gety都是一样的话为什么CoordinatorLayout中各个view会产生滑动效果,其实nestedscrollview源码中已经给出了答案并且非常简单就两句代码的事情

final int y = (int) ev.getY(activePointerIndex);
int deltaY = mLastMotionY - y;

mLastMotionY就是在down中所获取到的坐标值,deltaY就是每次在move的时候获取到的偏移量,其实这里就是当初困惑我的一个地方,照理来说mLastMotionY和y每次都应该是相同的值才对,也就是说deltaY每次都是0不会有任何偏移,但事实上每次获取到的deltaY一般情况下都不为0,其实要理解这个原因必须知道一个比较关键的点就是动画是一帧一帧绘制出来的,之所以手指移动view会随着移动是因为手指移动到一个新的坐标时实际上该view的所有状态还都是上一帧的数据,在触摸事件中,ev都是从父view传递进来的,父view在传递ev的时候已经将ev进行过偏移处理,所以这也就是为什么gety获取到的总是距离该view顶部的值而不是距离父view顶部的值的原因,当手指向上移动后,gety获取值时参照的view中的数据实际还是上一帧的数据,所以最终获取到的y值总是会比mLastMotionY小一点点,那么deltaY就总会产生一个不为0的值从而引起view的滑动。

代码就这么两句,但是要明白为什么这两句代码能起作用却需要花一点时间去好好思考下,之所以会去纠结这个问题是因为之前开发中遇到过类似的问题,当时的处理方式是不使用gety而是改用getrawy去处理,但实际上就像源码的处理方式使用gety也是可以解决该问题的,并且使用gety去解决需要你对其中的原理有一定理解。

当然我上面说的都是建立在一个假设的前提之上,那就是appbarlayout还未完全消失,实际情况就是appbarlayout消失和不消失时他们的处理方式是不一样的。appbarlayout不消失时view产生的滚动是appbarlayout偏移带动nestedscrollview偏移,而appbarlayout消失后产生的滚动是纯粹的nestedscrollview的滚动,纯粹的nestedscrollview产生的滚动就比较好处理了,就像我上面所说的常规滑动处理方式就能解决,实际nestedscrollview中也是这么做的。

综上再去看nestedscrollview的滑动处理,它实际上是分两部分的,一部分就是假滚动的处理,另一部分就是真滚动的处理。这是我在看nestedscrollview滑动处理时获取到的有价值的处理技巧

子view控件高度改变逻辑处理

关于这个点的思考是看到了网上一篇不错的文章,Android开发之CoordinatorLayout打造滑动越界弹性放大图片效果通过自定义behavior来实现图片放大的效果,感兴趣的可以重点关注下该文章中对于图片放大的bahavoir实现方式,当时自己看到这篇文章首先理解了下作者的思路,发现作者在处理放大图片时的处理逻辑比较巧妙,这里直接上作者的代码

public class AppbarZoomBehavior extends AppBarLayout.Behavior {

    private ImageView mImageView;
    private int mAppbarHeight;//记录AppbarLayout原始高度
    private int mImageViewHeight;//记录ImageView原始高度

    private static final float MAX_ZOOM_HEIGHT = 500;//放大最大高度
    private float mTotalDy;//手指在Y轴滑动的总距离
    private float mScaleValue;//图片缩放比例
    private int mLastBottom;//Appbar的变化高度

    private boolean isAnimate;//是否做动画标志


    public AppbarZoomBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);

    }

    @Override
    public boolean onLayoutChild(CoordinatorLayout parent, AppBarLayout abl, int layoutDirection) {
        boolean handled = super.onLayoutChild(parent, abl, layoutDirection);
        init(abl);
        return handled;
    }

    /**
     * 进行初始化操作,在这里获取到ImageView的引用,和Appbar的原始高度
     *
     * @param abl
     */
    private void init(AppBarLayout abl) {
        abl.setClipChildren(false);
        mAppbarHeight = abl.getHeight();
        mImageView = (ImageView) abl.findViewById(R.id.iv_img);
        if (mImageView != null) {
            mImageViewHeight = mImageView.getHeight();
        }
    }

    /**
     * 是否处理嵌套滑动
     *
     * @param parent
     * @param child
     * @param directTargetChild
     * @param target
     * @param nestedScrollAxes
     * @param type
     * @return
     */
    @Override
    public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes, int type) {
        isAnimate = true;
        return true;
    }

    /**
     * 在这里做具体的滑动处理
     *
     * @param coordinatorLayout
     * @param child
     * @param target
     * @param dx
     * @param dy
     * @param consumed
     * @param type
     */
    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed, int type) {
        if (mImageView != null && child.getBottom() >= mAppbarHeight && dy < 0 && type == ViewCompat.TYPE_TOUCH) {
            zoomHeaderImageView(child, dy);
        } else {
            if (mImageView != null && child.getBottom() > mAppbarHeight && dy > 0 && type == ViewCompat.TYPE_TOUCH) {
                consumed[1] = dy;
                zoomHeaderImageView(child, dy);
            } else {
                if (valueAnimator == null || !valueAnimator.isRunning()) {
                    super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
                }

            }
        }

    }


    /**
     * 对ImageView进行缩放处理,对AppbarLayout进行高度的设置
     *
     * @param abl
     * @param dy
     */
    private void zoomHeaderImageView(AppBarLayout abl, int dy) {
        mTotalDy += -dy;
        mTotalDy = Math.min(mTotalDy, MAX_ZOOM_HEIGHT);
        mScaleValue = Math.max(1f, 1f + mTotalDy / MAX_ZOOM_HEIGHT);
        ViewCompat.setScaleX(mImageView, mScaleValue);
        ViewCompat.setScaleY(mImageView, mScaleValue);
        mLastBottom = mAppbarHeight + (int) (mImageViewHeight / 2 * (mScaleValue - 1));
        abl.setBottom(mLastBottom);
    }


    /**
     * 处理惯性滑动的情况
     *
     * @param coordinatorLayout
     * @param child
     * @param target
     * @param velocityX
     * @param velocityY
     * @return
     */
    @Override
    public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY) {
        if (velocityY > 100) {
            isAnimate = false;
        }
        return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
    }


    /**
     * 滑动停止的时候,恢复AppbarLayout、ImageView的原始状态
     *
     * @param coordinatorLayout
     * @param abl
     * @param target
     * @param type
     */
    @Override
    public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout abl, View target, int type) {
        recovery(abl);
        super.onStopNestedScroll(coordinatorLayout, abl, target, type);
    }

    ValueAnimator valueAnimator;

    /**
     * 通过属性动画的形式,恢复AppbarLayout、ImageView的原始状态
     *
     * @param abl
     */
    private void recovery(final AppBarLayout abl) {
        if (mTotalDy > 0) {
            mTotalDy = 0;
            if (isAnimate) {
                valueAnimator = ValueAnimator.ofFloat(mScaleValue, 1f).setDuration(220);
                valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator animation) {
                        float value = (float) animation.getAnimatedValue();
                        ViewCompat.setScaleX(mImageView, value);
                        ViewCompat.setScaleY(mImageView, value);
                        abl.setBottom((int) (mLastBottom - (mLastBottom - mAppbarHeight) * animation.getAnimatedFraction()));
                    }
                });
                valueAnimator.start();
            } else {
                ViewCompat.setScaleX(mImageView, 1f);
                ViewCompat.setScaleY(mImageView, 1f);
                abl.setBottom(mAppbarHeight);
            }
        }
    }
}

作者:李晨玮
链接:https://www.jianshu.com/p/36e974cb3af5
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

要想实现下拉放大图片的效果,代码重点有两个地方,一个是

 abl.setClipChildren(false);

通过该代码允许imageview的越界放大,另一个比较关键的地方在于zoomHeaderImageView方法中通过调用setBottom方法实现appbarlayout的高度变化,这是我个人觉得非常巧妙且高效的做法。关于将view的高度改变可能很多人第一时间想到的方法都是通过setlayoutparams的形式修改高度值,自己在理解作者思路的同时确实也通过这种方法方法实现了和作者一样的ui效果,并且通过使用setlayoutparams修改appbarlayout高度可以做到注释掉abl.setClipChildren(false);也没问题。但是实际情况是作者的方法更加的高效,因为setlayoutparams方法会引起控件的绘制流程重新走一遍,导致不必要的measure和layout执行,而使用setbotom并不会引起上述问题,可以说从看作者的源码让自己也学到了一个改变view高度的小技巧。

最后

之所以会使用setlayoutparams重新实现下效果,最主要的原因还是纠结在setClipChildren方法上,我的理解只要appbarlayout的高度改变了,内部的imageview会跟着改变,其实是不需要使用setClipChildren方法也能实现相同效果的,最开始的做法就是直接在zoomHeaderImageView最后加上一句requestlayout,并且注释掉setClipChildren,想法很简单setbottom之后控件高度已经改变,调用requestlayout想让imageview重新测绘后高度和appbarlayout一致,但在实际运行后发现这样做行不通,并且会出现无法下拉的情况。原因就在于调用reqeustlayout之后bottom值的设置是通过layout方法,而layout传递的参数是由layoutparams决定,没有修改layoutparams值最终bottom值是无法被修改的,在理解直接调用reqeustlayout行不通的情况下我才使用setlayoutparams去实现,并且成功解决问题也证明了我的想法是正确的。

上一篇下一篇

猜你喜欢

热点阅读