CoordinatorLayout自定义Behavior的运用

原创 2016年07月11日 15:35:29

之前写了关于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

Material Design系列,自定义Behavior支持所有View

Material Design系列,自定义Behavior支持所有View,继承CoordinatorLayout.Behavior类,实现自己的Behavior,可扩展动画等!...

Material Design系列,自定义Behavior实现Android知乎首页

Material Design系列,自定义自定义Behavior实现Android知乎首页,用Behavior实现知乎首页上滑或者下滑时FAB和TAB导航的显示和隐藏,留给用户更多的位置来显示内容。...

一起玩转CoordinatorLayout

作为Material Design风格的重要组件,CoordinatorLayout协调多种组件的联动,实现各种复杂的效果,在实际项目中扮演着越来越重要的角色。本篇博客将由浅到深,带你一起玩转Coor...
  • tyk0910
  • tyk0910
  • 2016年12月21日 23:00
  • 4046

CoordinatorLayout布局的使用方式

作为Android的控件, CoordinatorLayout已经加入最新的HelloWorld项目中, 也是Material风格的重要组件, 协调(Coordinate)其他组件, 实现联动. 那么...

使用CoordinatorLayout打造各种炫酷的效果

CoordinatorLayout是在 Google IO/15 大会发布的,遵循Material 风格,包含在 support Library中,结合AppbarLayout, Collapsing...

CoordinatorLayout-带图片伸缩工具栏

效果图:步骤一:在build.gilde中添加以下代码dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) ...
  • pengkv
  • pengkv
  • 2015年06月09日 17:49
  • 12288

CoordinatorLayout简述

作为Android的控件, CoordinatorLayout已经加入最新的HelloWorld项目中, 也是Material风格的重要组件, 协调(Coordinate)其他组件, 实现联动. 那么...

android点击事件,第一次无效,第二次才响应的问题

今天碰到的问题,android登陆界面,有个重置密码TextView,点击第一次没有反应,点击第二次,才跳转到重置界面,搜索之后,发现,是 android:focusable="true" ...
  • rong_wz
  • rong_wz
  • 2016年06月25日 12:34
  • 3773

Android-->RecyclerView模仿探探左右滑动布局

站在巨人的肩膀上,才能走得更远.参考文章:http://blog.csdn.net/zxt0601/article/details/53730908我在此基础上优化了部分代码, 添加了滑动回调, 可自...
  • angcyo
  • angcyo
  • 2017年01月08日 14:56
  • 4915

CoordinatorLayout自定义Behavior

  • 2015年12月14日 09:08
  • 44KB
  • 下载
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:CoordinatorLayout自定义Behavior的运用
举报原因:
原因补充:

(最多只允许输入30个字)