效果演示
效果分解
动画效果无非的改变View的坐标以及scale。
可以简单将效果分解为以下步骤
通过监听滑动状态,获取AppBarLayout的缩放比例,改变View(如gif中的ImageView)的scale
通过监听滑动状态,获取AppBarLayout的缩放比例,改变View的定位
Behavior源码分析
Behavior是CoordinatorLayout类的一个静态抽象内部类。在源码的注释中,我们可以知道Behavior是用来协调CoordinatorLayout的子View的交互,包括拖拽等手势操作。
Behavior的类定义
public static abstract class Behavior {
/**
* 默认构造方法
*/
public Behavior() {
}
/**
* 自定义Behavior必须实现
*
* @param context
* @param attrs
*/
public Behavior(Context context, AttributeSet attrs) {
}
/**
* 绑定layoutParams
*/
public void onAttachedToLayoutParams(@NonNull CoordinatorLayout.LayoutParams params) {
}
/**
* 解绑layoutParams
*/
public void onDetachedFromLayoutParams() {
}
.....
public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) {
return false;
}
.....
public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
return false;
}
.....
}
划一下重点
- Behavior可以指定一个View,在上面的例子中就是ImageView,认为是依赖其他控件变化的View
- 在CoordinatorLayout中,LayoutParams 继承于 ViewGroup.MarginLayoutParams,在之基础上加上了Behavior和Anchor(参照物)
- 下面两个方法可以认为是实现所需效果的重点,决定被依赖的控件类型,以及在被依赖控件Change时,ImageView所要进行的改变。
Behavior初始化
在CoordinatorLayout.LayoutParams的构造方法中,我们可以找到Behavior的初始化过程
mBehaviorResolved = a.hasValue(
R.styleable.CoordinatorLayout_Layout_layout_behavior);
if (mBehaviorResolved) {
mBehavior = parseBehavior(context, attrs, a.getString(
R.styleable.CoordinatorLayout_Layout_layout_behavior));
}
判断是否存在Behavior,如果存在就调用parseBehavior方法对Behavior进行初始化。
现在我们再来看parseBehavior方法。
static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
.....
try {
Map> constructors = sConstructors.get();
if (constructors == null) {
constructors = new HashMap<>();
sConstructors.set(constructors);
}
Constructor c = constructors.get(fullName);
if (c == null) {
final Class clazz = (Class) Class.forName(fullName, true,
context.getClassLoader());
c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
c.setAccessible(true);
constructors.put(fullName, c);
}
return c.newInstance(context, attrs);
} catch (Exception e) {
throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
}
}
非常标准的用反射的方法初始化对象的代码。
我们可以发现使用了带 Context context, AttributeSet attrs 两个参数的构造方法。
所以在自定义Behavior时,我们必须要对该构造方法进行重写。
onDependentViewChanged的调用过程
在CoordinatorLayout中定义了三种事件类型
static final int EVENT_PRE_DRAW = 0;
static final int EVENT_NESTED_SCROLL = 1;
static final int EVENT_VIEW_REMOVED = 2;
在不同的状态改变时调用onChildViewsChanged方法
final void onChildViewsChanged(@DispatchChangeEvent final int type) {
final int childCount = mDependencySortedChildren.size();
.....
// Update any behavior-dependent views for the change
for (int j = i + 1; j < childCount; j++) {
final View checkChild = mDependencySortedChildren.get(j);
final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
final Behavior b = checkLp.getBehavior();
if (b != null && b.layoutDependsOn(this, checkChild, child)) {
.....
final boolean handled;
switch (type) {
case EVENT_VIEW_REMOVED:
// EVENT_VIEW_REMOVED means that we need to dispatch
// onDependentViewRemoved() instead
b.onDependentViewRemoved(this, checkChild, child);
handled = true;
break;
default:
// Otherwise we dispatch onDependentViewChanged()
handled = b.onDependentViewChanged(this, checkChild, child);
break;
}
if (type == EVENT_NESTED_SCROLL) {
// If this is from a nested scroll, set the flag so that we may skip
// any resulting onPreDraw dispatch (if needed)
checkLp.setChangedAfterNestedScroll(handled);
}
}
}
}
}
略去部分代码后,可以清楚的看到behavior的调用过程
- 循环CoordinatorLayout下childView
- 判断View是否有Behavior以及View是否是该Behavior所依赖的视图
- 若条件成立,非特殊情况(默认)下调用onDependentViewChanged。
再一个是通过AppBarLayout中的方法实现调用,获取到在xml中设置的layout_anchor参照物,这也是实现需求的一个关键。
public void dispatchDependentViewsChanged(View view) {
final List dependents = mChildDag.getIncomingEdges(view);
if (dependents != null && !dependents.isEmpty()) {
for (int i = 0; i < dependents.size(); i++) {
final View child = dependents.get(i);
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams)
child.getLayoutParams();
CoordinatorLayout.Behavior b = lp.getBehavior();
if (b != null) {
b.onDependentViewChanged(this, child, view);
}
}
}
}
自定义Behavior的实现
public AvatarBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
if (attrs != null) {
TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.AvatarImageBehavior);
.....//自定义参数,如定位,尺寸等
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) {
//初始化参数
init(child, dependency);
//计算缩放比
mPercent = calcPercent(dependency);
//计算并设置View的Y坐标
setViewY(child, mOriginalY, mFinalY - mScaleSize, mPercent);
//计算并设置View的尺寸
scaleView(child, mOriginalSize, mFinalSize, mPercent);
}
}
总结
实现效果的过程中,踩了非常多的坑,主要是Android版本不同导致的兼容问题,现总结如下
- 在上述代码中,没有对setViewY方法进行具体的实现,因为在不用的Android版本下,statusbar的高度不同,在计算Y的值时需要加上状态栏的高度,并且在dimens的不用包下设置不同的size,普通的为0dp,v21中是25dp,v23中是24dp。
- 在6.0及以上系统中,当AppBarLayout完全折叠是,ToolBar会遮盖ImageView,所以需要在CollapsingToolbarLayout中addView来保证完全折叠状态下,头像的展示。
- 上文也有提到,调用onDependentViewChanged方法有两种路径,这也是在该方法中仍要进行类型判断的原因。结合总结的第二点实现6.0以上系统的完整展示过程。
参考
本文在源码之外的内容主要参考了MaterialDesignFeatures项目,在实现上有疑惑的请移步gayhub参考。