CoordinatorLayout自定义Behavior的运用

之前写了关于CoordinatorLayout的使用介绍,现在补充下自定义Behavior的运用,如果对CoordinatorLayout的基本使用不了解可以先看这个:CoordinatorLayout的详细介绍

先看下Behavior类比较常用的几个方法:

public static abstract class Behavior<V extends View> {
    // 略......
	
    /**
     * Determine whether the supplied child view has another specific sibling view as a
     * layout dependency.
     *
     * <p>This method will be called at least once in response to a layout request. If it
     * returns true for a given child and dependency view pair, the parent CoordinatorLayout
     * will:</p>
     * <ol>
     *     <li>Always lay out this child after the dependent child is laid out, regardless
     *     of child order.</li>
     *     <li>Call {@link #onDependentViewChanged} when the dependency view's layout or
     *     position changes.</li>
     * </ol>
     *
     * @param parent the parent view of the given child
     * @param child the child view to test
     * @param dependency the proposed dependency of child
     * @return true if child's layout depends on the proposed dependency's layout,
     *         false otherwise
     *
     * @see #onDependentViewChanged(CoordinatorLayout, android.view.View, android.view.View)
     */
    public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) {
        return false;
    }

    /**
     * Respond to a change in a child's dependent view
     *
     * <p>This method is called whenever a dependent view changes in size or position outside
     * of the standard layout flow. A Behavior may use this method to appropriately update
     * the child view in response.</p>
     *
     * <p>A view's dependency is determined by
     * {@link #layoutDependsOn(CoordinatorLayout, android.view.View, android.view.View)} or
     * if {@code child} has set another view as it's anchor.</p>
     *
     * <p>Note that if a Behavior changes the layout of a child via this method, it should
     * also be able to reconstruct the correct position in
     * {@link #onLayoutChild(CoordinatorLayout, android.view.View, int) onLayoutChild}.
     * <code>onDependentViewChanged</code> will not be called during normal layout since
     * the layout of each child view will always happen in dependency order.</p>
     *
     * <p>If the Behavior changes the child view's size or position, it should return true.
     * The default implementation returns false.</p>
     *
     * @param parent the parent view of the given child
     * @param child the child view to manipulate
     * @param dependency the dependent view that changed
     * @return true if the Behavior changed the child view's size or position, false otherwise
     */
    public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
        return false;
    }

    /**
     * Respond to a child's dependent view being removed.
     *
     * <p>This method is called after a dependent view has been removed from the parent.
     * A Behavior may use this method to appropriately update the child view in response.</p>
     *
     * <p>A view's dependency is determined by
     * {@link #layoutDependsOn(CoordinatorLayout, android.view.View, android.view.View)} or
     * if {@code child} has set another view as it's anchor.</p>
     *
     * @param parent the parent view of the given child
     * @param child the child view to manipulate
     * @param dependency the dependent view that has been removed
     */
    public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) {
    }
}
上篇文章说过这几个方法的作用:

1. layoutDependsOn():用来确定关联的视图;
2. onDependentViewChanged():当关联视图发生改变时回调接口;
3. onDependentViewRemoved():关联视图移除时回调接口;

自定义Behavior需要注意的地方:
1. 需要实现构造方法public Behavior(Context context, AttributeSet attrs),不然布局中无法使用;
2. 关联的视图除了layoutDependsOn()指定的视图外还包括layout_anchor属性指定的视图,可在onDependentViewChanged()方法中用instanceof判断;
3. onDependentViewChanged()方法执行需要关联的视图大小或位置改变;
4. 除了和关联视图做交互动画外,还能通过检测滚动来做交互动画;

一. 检测滚动Behavior

动画实现引用这篇文章的Behavior:FloatingActionButton滚动时的显示与隐藏小结

来看下Behavior的实现:

public class ScrollAwareFABBehavior extends FloatingActionButton.Behavior {

    private boolean mIsAnimatingOut = false;

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

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

    @Override
    public void onNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child,
                               View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed,
                dyUnconsumed);

        if (dyConsumed > 0 && !mIsAnimatingOut && child.getVisibility() == View.VISIBLE) {
//            child.hide();
            animateOut(child);
        } else if (dyConsumed < 0 && child.getVisibility() != View.VISIBLE) {
//            child.show();
            animateIn(child);
        }
    }

    // Same animation that FloatingActionButton.Behavior uses to hide the FAB when the AppBarLayout exits
    private void animateOut(final FloatingActionButton button) {
        if (Build.VERSION.SDK_INT >= 14) {
            ViewCompat.animate(button).translationY(button.getHeight() + getMarginBottom(button))
                    .setInterpolator(new FastOutSlowInInterpolator())
                    .withLayer()
                    .setListener(new ViewPropertyAnimatorListener() {
                        public void onAnimationStart(View view) {
                            mIsAnimatingOut = true;
                        }

                        public void onAnimationCancel(View view) {
                            mIsAnimatingOut = false;
                        }

                        public void onAnimationEnd(View view) {
                            mIsAnimatingOut = false;
                            view.setVisibility(View.GONE);
                        }
                    }).start();
        }
    }

    // Same animation that FloatingActionButton.Behavior uses to show the FAB when the AppBarLayout enters
    private void animateIn(FloatingActionButton button) {
        button.setVisibility(View.VISIBLE);
        if (Build.VERSION.SDK_INT >= 14) {
            ViewCompat.animate(button).translationY(0)
                    .setInterpolator(new FastOutSlowInInterpolator())
                    .withLayer()
                    .setListener(null)
                    .start();
        }
    }

    private int getMarginBottom(View v) {
        int marginBottom = 0;
        final ViewGroup.LayoutParams layoutParams = v.getLayoutParams();
        if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
            marginBottom = ((ViewGroup.MarginLayoutParams) layoutParams).bottomMargin;
        }
        return marginBottom;
    }
}
这个Behavior实现了FloatingActionButton跟随垂直滚动做上移和下移动画,整个实现还是比较简单的,在 onStartNestedScroll()中判断垂直滚动,在 onNestedScroll()中检测上下滚动操作并做FloatingActionButton移动动画。效果如下:


二. 关联Behavior

使用Depend实现交互动画比检测滚动来得复杂,但它的功能也相对更强大,实现方法也更灵活。

下面来实现头像移动缩小的Behavior,先看下效果图等下比较好描述:


实现的效果就是把指定的头像图片缩小移动并最终固定在左上角,来看下实现代码进行说明吧:

public class AvatarBehavior extends CoordinatorLayout.Behavior<CircleImageView> {

    // 缩放动画变化的支点
    private static final float ANIM_CHANGE_POINT = 0.2f;

    private Context mContext;
    // 整个滚动的范围
    private int mTotalScrollRange;
    // AppBarLayout高度
    private int mAppBarHeight;
    // AppBarLayout宽度
    private int mAppBarWidth;
    // 控件原始大小
    private int mOriginalSize;
    // 控件最终大小
    private int mFinalSize;
    // 控件最终缩放的尺寸,设置坐标值需要算上该值
    private float mScaleSize;
    // 原始x坐标
    private float mOriginalX;
    // 最终x坐标
    private float mFinalX;
    // 起始y坐标
    private float mOriginalY;
    // 最终y坐标
    private float mFinalY;
    // ToolBar高度
    private int mToolBarHeight;
    // AppBar的起始Y坐标
    private float mAppBarStartY;
    // 滚动执行百分比[0~1]
    private float mPercent;
    // Y轴移动插值器
    private DecelerateInterpolator mMoveYInterpolator;
    // X轴移动插值器
    private AccelerateInterpolator mMoveXInterpolator;
    // 最终变换的视图,因为在5.0以上AppBarLayout在收缩到最终状态会覆盖变换后的视图,所以添加一个最终显示的图片
    private CircleImageView mFinalView;
    // 最终变换的视图离底部的大小
    private int mFinalViewMarginBottom;


    public AvatarBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        mMoveYInterpolator = new DecelerateInterpolator();
        mMoveXInterpolator = new AccelerateInterpolator();
        if (attrs != null) {
            TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.AvatarImageBehavior);
            mFinalSize = (int) a.getDimension(R.styleable.AvatarImageBehavior_finalSize, 0);
            mFinalX = a.getDimension(R.styleable.AvatarImageBehavior_finalX, 0);
            mToolBarHeight = (int) a.getDimension(R.styleable.AvatarImageBehavior_toolBarHeight, 0);
            a.recycle();
        }
    }

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, CircleImageView child, View dependency) {
        return dependency instanceof AppBarLayout;
    }

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, CircleImageView child, View dependency) {

        if (dependency instanceof AppBarLayout) {
            _initVariables(child, dependency);

            mPercent = (mAppBarStartY - dependency.getY()) * 1.0f / mTotalScrollRange;

            float percentY = mMoveYInterpolator.getInterpolation(mPercent);
            AnimHelper.setViewY(child, mOriginalY, mFinalY - mScaleSize, percentY);

            if (mPercent > ANIM_CHANGE_POINT) {
                float scalePercent = (mPercent - ANIM_CHANGE_POINT) / (1 - ANIM_CHANGE_POINT);
                float percentX = mMoveXInterpolator.getInterpolation(scalePercent);
                AnimHelper.scaleView(child, mOriginalSize, mFinalSize, scalePercent);
                AnimHelper.setViewX(child, mOriginalX, mFinalX - mScaleSize, percentX);
            } else {
                AnimHelper.scaleView(child, mOriginalSize, mFinalSize, 0);
                AnimHelper.setViewX(child, mOriginalX, mFinalX - mScaleSize, 0);
            }
            if (mFinalView != null) {
                if (percentY == 1.0f) {
                    // 滚动到顶时才显示
                    mFinalView.setVisibility(View.VISIBLE);
                } else {
                    mFinalView.setVisibility(View.GONE);
                }
            }
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && dependency instanceof CollapsingToolbarLayout) {
            // 大于5.0才生成新的最终的头像,因为5.0以上AppBarLayout会覆盖变换后的头像
            if (mFinalView == null && mFinalSize != 0 && mFinalX != 0 && mFinalViewMarginBottom != 0) {
                mFinalView = new CircleImageView(mContext);
                mFinalView.setVisibility(View.GONE);
                // 添加为CollapsingToolbarLayout子视图
                ((CollapsingToolbarLayout) dependency).addView(mFinalView);
                FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) mFinalView.getLayoutParams();
                // 设置大小
                params.width = mFinalSize;
                params.height = mFinalSize;
                // 设置位置,最后显示时相当于变换后的头像位置
                params.gravity = Gravity.BOTTOM;
                params.leftMargin = (int) mFinalX;
                params.bottomMargin = mFinalViewMarginBottom;
                mFinalView.setLayoutParams(params);
                mFinalView.setImageDrawable(child.getDrawable());
                mFinalView.setBorderColor(child.getBorderColor());
                int borderWidth = (int) ((mFinalSize * 1.0f / mOriginalSize) * child.getBorderWidth());
                mFinalView.setBorderWidth(borderWidth);
            }
        }

        return true;
    }

    /**
     * 初始化变量
     * @param child
     * @param dependency
     */
    private void _initVariables(CircleImageView child, View dependency) {
        if (mAppBarHeight == 0) {
            mAppBarHeight = dependency.getHeight();
            mAppBarStartY = dependency.getY();
        }
        if (mTotalScrollRange == 0) {
            mTotalScrollRange = ((AppBarLayout) dependency).getTotalScrollRange();
        }
        if (mOriginalSize == 0) {
            mOriginalSize = child.getWidth();
        }
        if (mFinalSize == 0) {
            mFinalSize = mContext.getResources().getDimensionPixelSize(R.dimen.avatar_final_size);
        }
        if (mAppBarWidth == 0) {
            mAppBarWidth = dependency.getWidth();
        }
        if (mOriginalX == 0) {
            mOriginalX = child.getX();
        }
        if (mFinalX == 0) {
            mFinalX = mContext.getResources().getDimensionPixelSize(R.dimen.avatar_final_x);
        }
        if (mOriginalY == 0) {
            mOriginalY = child.getY();
        }
        if (mFinalY == 0) {
            if (mToolBarHeight == 0) {
                mToolBarHeight = mContext.getResources().getDimensionPixelSize(R.dimen.toolbar_height);
            }
            mFinalY = (mToolBarHeight - mFinalSize) / 2 + mAppBarStartY;
        }
        if (mScaleSize == 0) {
            mScaleSize = (mOriginalSize - mFinalSize) * 1.0f / 2;
        }
        if (mFinalViewMarginBottom == 0) {
            mFinalViewMarginBottom = (mToolBarHeight - mFinalSize) / 2;
        }
    }
}
代码注释还是标的挺清楚,我说几个注意的地方:
1. 所有的动画控制都是在 if (dependency instanceof AppBarLayout) 这个条件分支里实现,并且AppBarLayout是layoutDependsOn()所指定依赖的对象;
2. Y方向上做减速移动DecelerateInterpolator;
3. 设置了个动画分界点ANIM_CHANGE_POINT(0.2f),在这里开始做X方向移动和缩小动画,X方向做加速移动AccelerateInterpolator;
4. 在 if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && dependency instanceof CollapsingToolbarLayout)的依赖分支生成了一个小的头像mFinalView,CollapsingToolbarLayout左下角,其实就是头像变换最终所在的位置,这么做的原因是在版本 5.0以上变换的头像最后会被AppBarLayout覆盖掉;

在看下布局文件:

<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.dl7.coordinator.activity.CustomBehaviorActivity">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/app_bar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/collapsing_tool_bar"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:contentScrim="@color/colorPrimary"
            app:expandedTitleGravity="bottom|center_horizontal"
            app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">

            <ImageView
                android:id="@+id/iv_head"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:scaleType="fitXY"
                android:src="@mipmap/bane"
                app:layout_collapseMode="parallax"
                app:layout_collapseParallaxMultiplier="0.6"/>

            <!-- 设置app:navigationIcon="@android:color/transparent"给头像预留位置 -->
            <android.support.v7.widget.Toolbar
                android:id="@+id/tool_bar"
                android:layout_width="match_parent"
                android:layout_height="@dimen/toolbar_height"
                app:layout_collapseMode="pin"
                app:navigationIcon="@android:color/transparent"
                app:theme="@style/ThemeOverlay.AppCompat.Dark"
                app:title="Tom Hardy"/>
        </android.support.design.widget.CollapsingToolbarLayout>
    </android.support.design.widget.AppBarLayout>

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

        <include layout="@layout/layout_content_tom"/>
    </android.support.v4.widget.NestedScrollView>

    <!-- layout_anchor属性5.0以上需要设置为CollapsingToolbarLayout,不然头像最后会被覆盖 -->
    <de.hdodenhof.circleimageview.CircleImageView
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_margin="16dp"
        android:src="@mipmap/tom"
        app:border_color="@android:color/white"
        app:border_width="1dp"
        app:layout_anchor="@id/collapsing_tool_bar"
        app:layout_anchorGravity="bottom|right"
        app:layout_behavior="com.dl7.coordinator.behavior.AvatarBehavior"/>
</android.support.design.widget.CoordinatorLayout>


自定义Behavior就到这边了,推荐一个相关的开源项目:https://github.com/saulmm/CoordinatorBehaviorExample,这里实现的效果更好看,但代码逻辑更复杂,而且耦合比较厉害,需要关联好多个地方。

源代码:CoordinatorLayoutSample

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
实现这样的效果,需要自定义一个 Behavior 类。下面是一个简单的示例代码: ``` public class MyBehavior extends CoordinatorLayout.Behavior<View> { private int mTotalScrollRange; public MyBehavior() { super(); } public MyBehavior(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) { return dependency instanceof AppBarLayout; } @Override public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) { if (dependency instanceof AppBarLayout) { mTotalScrollRange = ((AppBarLayout) dependency).getTotalScrollRange(); float ratio = -dependency.getY() / mTotalScrollRange; child.setTranslationY(ratio * child.getHeight()); return true; } return false; } } ``` 在这个 Behavior 类中,我们首先判断依赖的 view 是否是 AppBarLayout,如果是就设置一个 mTotalScrollRange 变量,该变量保存了 AppBarLayout 的总滑动范围。在 onDependentViewChanged 方法中,我们计算出当前的滑动比例 ratio,并根据这个比例来设置子 view 的 Y 轴偏移量,以实现子 view 的折叠和展开。 接下来,在布局文件中将该 Behavior 应用到 RecyclerView 对应的子 view 上即可: ``` <androidx.recyclerview.widget.RecyclerView android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior=".MyBehavior" /> ``` 注意,这个 Behavior 只是一个简单的示例,实际应用中可能需要根据具体需求进行修改。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值