自定义Behavior之Floating控件进阶版

本篇文章介绍使用CoordinatorLayout的自定义Behavior来实现如下的效果

这里写图片描述

本例效果和上篇文章的效果类似, 因此建议先阅读上篇文章

自定义Behavior之ToolBar上滑TabLayout颜色渐变

分析本例效果

首先我们来分析下整个例子需要实现哪些效果:

NestedScrollView上滑和下滑时覆盖背景
ImageView跟随NestedScrollView放大缩小与位移
滑动时会有黏性效果
滑动距离超过中间值后放开会自动滑向想要的方向
滑动距离未超过中间值放开则会自动回弹

本例需要的几个重要方法介绍

我们的例子中重写了Behavior的几个重要方法:

  • layoutDependsOn
  • onDependentViewChanged
  • onLayoutChild
  • onStartNestedScroll
  • onNestedPreScroll
  • onNestedScroll
  • onStopNestedScroll
  • onNestedScrollAccepted
  • onNestedPreFling
  • onStartNestedScroll

这些方法具体的说明可以参考:CoordinatorLayout自定义Behavior的简单总结

自定义 Behavior 实现思路

本例使用了两个自定义behavior, NestedScrollView不设依赖, 仅改变自己的位置, imageView将NestedScrollView作为依赖视图,通过获取NestedScrollView位置的改变, 计算出一个百分比值, 利用这个百分比值来调整imageView的位置以及大小

实现过程具体分析

首先继承Behavior, 在构造函数内做初始化工作

public class ContentScrollBehavior extends CoordinatorLayout.Behavior<View> {
    private static final String TAG = ContentScrollBehavior.class.getSimpleName();
    private WeakReference<View> mChildView;
    private OverScroller mOverScroller;
    private Handler mHandler;
    // 依赖视图起始y坐标
    private float mDependencyOriginalY;
    // 依赖视图最终y坐标
    private float mDependencyFinalY;
    private boolean isScrolling = false;

    public ContentScrollBehavior(Context context, AttributeSet attributeSet) {
        super(context, attributeSet);
        mOverScroller = new OverScroller(context);
        mHandler = new Handler();
        mDependencyOriginalY = context.getResources().getDimensionPixelOffset(R.dimen.content_height);
        mDependencyFinalY = context.getResources().getDimensionPixelOffset(R.dimen.content_offset);
    }
    .....
}

mDependencyOriginalY和mDependencyFinalY是NestedScrollView的初始Y位置和最终Y位置

@Override
public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
    CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
    if (params != null && params.height == CoordinatorLayout.LayoutParams.MATCH_PARENT) {
        child.layout(0, 0, parent.getWidth(), parent.getHeight());
        child.setTranslationY(mDependencyOriginalY);
        mChildView = new WeakReference<>(child);
        return true;
    }

    return super.onLayoutChild(parent, child, layoutDirection);
}

在onLayoutChild里设置NestedScrollView的初始位置

@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
    return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}

onStartNestedScroll在用户按下手指的时候回调,该方法在返回true的时候才会引发其他一系列的回调,这里我们只需要考虑垂直滑动,因此在垂直滑动条件成立的时候返回true

@Override
public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
    isScrolling = false;
    mOverScroller.abortAnimation();
    super.onNestedScrollAccepted(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
}

onNestedScrollAccepted方法里我们可以做一些准备工作,比如让之前的滑动动画结束

@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View get, int dx, int dy, int[] consumed) {
    //上滑操作dy值大于0,因此小于0直接分发给onNestedScroll
    if (dy < 0) {
        return;
    }

    //上滑后的位置不小于mDependencyFinalY(100dp)
    float transY = child.getTranslationY() - dy;
    if (transY > 0 && transY >= mDependencyFinalY) {
        child.setTranslationY(transY);
        consumed[1] = dy;
    }
}

@Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
    //上滑到底部并越界后dyUnconsumed值大于0直接忽略
    if (dyUnconsumed > 0) {
        return;
    }

    //下滑后的位置不大于mDependencyOriginalY(250dp)
    float transY = child.getTranslationY() - dyUnconsumed;
    if (transY > 0 && transY <= mDependencyOriginalY) {
        child.setTranslationY(transY);
    }
}

由于onNestedPreScroll方法会优先于onNestedScroll之前调用,因此我们可以将上滑动作分配到onNestedPreScroll,下滑动作分配到onNestedScroll,我们来分析下这样实现的原理:

  • 上滑
    当用户上滑时onNestedPreScroll优先调用,我们判断滑动方向,向上滑动才继续执行,通过调整子视图自己的translateY值来进行上移操作,并且消耗相应的consumed值,之后会回调onNestedScroll方法,如果dyUnconsumed还有值的话说明没有上滑操作没有完成,直接中断,然后继续回调onNestedPreScroll方法,重复一遍上面的操作,直到onNestedScroll方法里的dyUnconsumed消耗到0时就表示上滑到头了,整个上滑操作完成
  • 下滑
    我们在onNestedPreScroll方法中只有上滑时dy>0的情况才继续执行,因此下滑时dy<0的值不会在onNestedPreScroll中消耗掉,会直接传递到onNestedScroll方法中的dyUnconsumed,然后我们可以通过调整子视图自己的translateY值来进行下移操作,并消耗相应的dyUnconsumed值,然后不断重复上面步骤直到依赖视图完全实现完毕,整个下滑操作完成

最后解释下为什么要分别分配到两个方法中,因为子视图是NestedScrollView, 内部还包含了一个子视图(textview), 如果NestedScrollView还没消费完移动事件,而NestedScrollView内的子视图又可以向下滚动,这时我们就不能决定是让NestedScrollView位移还是NestedScrollView内的子视图滚动了,只有让NestedScrollView先消费完移动事件才能保证唯一性

@Override
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target, float velocityX, float velocityY) {
    return onUserStopDragging(velocityY, child);
}

用户松开手指并且会发生惯性滚动之前调用,在这个方法内我们可以实现快速上滑或者快速下滑的操作

@Override
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target) {
    if (!isScrolling) {
        onUserStopDragging(800, child);
    }
}

用户松开手指如果不发生惯性滚动,就会执行该方法,这里我们可以用来实现黏性滑动的效果

private boolean onUserStopDragging(float velocity, View child) {
    float translateY = child.getTranslationY();
    float minHeaderTranslate = mDependencyFinalY;
    float maxHeaderTranslate = mDependencyOriginalY;
    float midHeaderTranslate = maxHeaderTranslate - minHeaderTranslate;
    //中间态对比参数
    float y = translateY - maxHeaderTranslate;
    float ym = y + midHeaderTranslate;

    if (translateY == mDependencyFinalY || translateY == mDependencyOriginalY) {
        return false;
    }

    //在这里计算有没有超过中间态
    boolean targetState; // Flag indicates whether to expand the content.
    if (Math.abs(velocity) <= 800) {
        //y范围(-450~0) ym范围(0~450),取绝对值比大小来判断滑动距离有没有超过1/2
        targetState = Math.abs(y) >= Math.abs(ym);
        velocity = 800; // Limit velocity's minimum value.
    } else {
        targetState = velocity > 0;
    }

    //根据targetState判断,超过中间态自动滑动剩余距离,没有则回到原处
    float targetTranslateY = targetState ? minHeaderTranslate : maxHeaderTranslate;

    //根据targetTranslateY的值来减去translateY来计算dy
    mOverScroller.startScroll(0, (int) translateY, 0, (int) (targetTranslateY - translateY), (int) (1000000 / Math.abs(velocity)));
    mHandler.post(flingRunnable);
    isScrolling = true;

    return true;
}

private Runnable flingRunnable = new Runnable() {
    @Override
    public void run() {
        if (mOverScroller.computeScrollOffset()) {
            getChildView().setTranslationY(mOverScroller.getCurrY());
            mHandler.post(this);
        } else {
            isScrolling = false;
        }
    }
};

实现黏性滑动的代码, 如果提供了速度的话使用速度来滑动, 否则使用默认速度来滑动, 如果手指滑动如果超过中间值的话滑动到目标位置, 否则滑回原来的位置, 在计算出需要滑动的剩余距离后, 通过Scroller 配合 Handler 来实现该效果

最后贴一下xml布局

<?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:orientation="vertical">

    <ImageView
        android:id="@+id/scrolling_header"
        android:layout_width="match_parent"
        android:layout_height="250dp"
        android:background="@mipmap/banner3"/>

    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="@color/colorPrimary"
        android:fitsSystemWindows="true"
        android:theme="@style/ThemeOverlay.AppCompat.ActionBar"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>

    <TextView
        android:id="@+id/text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="350dp"
        android:paddingBottom="60dp"
        android:text="想告别起床气,快去定个闹铃吧!"
        android:textColor="@android:color/white"
        app:layout_anchor="@id/scrollView"
        app:layout_anchorGravity="top|center_horizontal"
        />

    <android.support.v4.widget.NestedScrollView
        android:id="@+id/scrollView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/white"
        app:layout_behavior="@string/content_behavior">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/large_text"/>

    </android.support.v4.widget.NestedScrollView>

    <ImageView
        android:id="@+id/add_alarm_clock"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@mipmap/add_alarm_clock_button_pressed"
        app:layout_anchor="@id/scrolling_header"
        app:layout_anchorGravity="center"
        app:layout_behavior="@string/floating_image_behavoir"
        />

</android.support.design.widget.CoordinatorLayout>

代码示例:
MaterialDesignFeatures

参考:
http://www.jianshu.com/p/7f50faa65622
http://www.jianshu.com/p/82d18b0d18f4

1

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值