MD - CoordinatorLayout源码分析Behavi
1.概述
Material Design从Android5.0开始引入的,是一种全新的设计语言(翻译为“原材料设计”),其实是谷歌提倡的一种设计风格、理念、原则。结合Behavior实现非常酷炫的效果。接下来从上一篇文章 MD - 简单控件使用以及自定义Behavior 来从源码角度分析一下CoordinatorLayout和Behavior的工作流程。
先看一下效果:
在这里插入图片描述2.分析前先思考三个问题
1.Behavior要有效果为什么必须要使用CoordinatorLayout包裹
2.为什么自定义的Behavior要放全类名
3.Behavior效果怎么传递的
3. CoordinatorLayout源码分析
带着上面三个问题来一一进行分析
3.1.CoordinatorLayout的LayoutParams首先会获取子类属性。进行解析。
打个比方只能在对应下才有效:
LinearLayout:android:layout_weight属性
RelativeLayout:android:layout_centerInParent属性
LayoutParams(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
final TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.CoordinatorLayout_Layout);
this.gravity = a.getInteger(
R.styleable.CoordinatorLayout_Layout_android_layout_gravity,
Gravity.NO_GRAVITY);
//1.获取我们layout_behavior属性,如果有值就为true
mBehaviorResolved = a.hasValue(
R.styleable.CoordinatorLayout_Layout_layout_behavior);
/**
* 2.进行解析属性
* 获取属性的值 a.getString(R.styleable.CoordinatorLayout_Layout_layout_behavior)
*/
if (mBehaviorResolved) {
mBehavior = parseBehavior(context, attrs, a.getString(
R.styleable.CoordinatorLayout_Layout_layout_behavior));
}
a.recycle();
//
if (mBehavior != null) {
// If we have a Behavior, dispatch that it has been attached
mBehavior.onAttachedToLayoutParams(this);
}
}
3.2.解析Behavior属性值通过反射实例化对象
步骤:1.获取我们布局中设置的全类名,xxx.xxx.xx 或者 .xxx
2.通过获取类名获取class,获取两个参数的构造方法
3.通过反射实例化 Behavior对象,newInstance(),所有的Behavior都会放到集合中
ThreadLoacal:保护线程安全,一个线程对应一个ThreadLocal
static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
// name就是app:layout_behavior的属性值
if (TextUtils.isEmpty(name)) {
return null;
}
final String fullName;
//如果是以.开头,它会加上包名
if (name.startsWith(".")) {
fullName = context.getPackageName() + name;
} else if (name.indexOf('.') >= 0) {
fullName = name;
} else {
fullName = !TextUtils.isEmpty(WIDGET_PACKAGE_NAME)
? (WIDGET_PACKAGE_NAME + '.' + name)
: name;
}
//重点:
try {
/**
* 获取构造函数,进行存储 使用了ThreadLaocal来保护线程安全,可以去看一下Handler的源码。
* String:代表我们的类名
* Constructor<Behavior> 是我们的构造方法
*/
Map<String, Constructor<Behavior>> constructors = sConstructors.get();
if (constructors == null) {
constructors = new HashMap<>();
sConstructors.set(constructors);
}
Constructor<Behavior> c = constructors.get(fullName);
if (c == null) {
//获取我们自定义Behavior的class对象
final Class<Behavior> clazz = (Class<Behavior>) context.getClassLoader()
.loadClass(fullName);
c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
//暴力访问所有private修饰的,不设置不能获取到私有的属性
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);
}
}
3.3.Behavior是通过RecyclerView的onTouchEvent进行传递
在这里插入图片描述
1.RecyclerView:onTouchEvent方法
@Override
public boolean onTouchEvent(MotionEvent e) {
switch (action) {
case MotionEvent.ACTION_DOWN: {
//调用了startNestedScroll方法
startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
} break;
return true;
}
2.ViewParentCompat:onStartNestedScroll方法
public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
int nestedScrollAxes, int type) {
if (Build.VERSION.SDK_INT >= 21) {
try {
//调用了父类(CoordinatorLayout) 的onStartNestedScroll
return parent.onStartNestedScroll(child, target, nestedScrollAxes);
} catch (AbstractMethodError e) {
Log.e(TAG, "ViewParent " + parent + " does not implement interface "
+ "method onStartNestedScroll", e);
}
}
return false;
}
3.CoordinatorLayout:onStartNestedScroll方法
@Override
public boolean onStartNestedScroll(View child, View target, int axes, int type) {
boolean handled = false;
//1.获取子孩子个数
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
if (view.getVisibility() == View.GONE) {
// If it's GONE, don't dispatch
continue;
}
//2.获取子孩子的Behavior
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
//3.调用子View的onStartNestedScroll方法
final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child,
target, axes, type);
handled |= accepted;
lp.setNestedScrollAccepted(type, accepted);
} else {
lp.setNestedScrollAccepted(type, false);
}
}
return handled;
}
4.由此可知所有的方法,都是通过子类传递给父类,调用父类的方法完成
4.SnackBar 源码分析
先看一下效果:
4.1.使用:
floatingActionButton = findViewById(R.id.fab);
floatingActionButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Snackbar.make(v,"haha",Snackbar.LENGTH_SHORT).setAction("你好", new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(BehaviorActivity.this,"再见",Toast.LENGTH_SHORT).show();
}
}).show();
}
});
4.2.源码分析:
1.首先查找父容器,必须是CoordinatorLayout 或者 FramLayout
2.通过渲染一个布局进行添加,设置文本时间
3.通过show方法通过Handler开启了两个动画 showView() 和 hideView()
@NonNull
public static Snackbar make(@NonNull View view, @NonNull CharSequence text, int duration) {
//1.查找父容器,必须是CoordinatorLayout 或者 FramLayout
ViewGroup parent = findSuitableParent(view);
if (parent == null) {
throw new IllegalArgumentException("No suitable parent found from the given view. Please provide a valid view.");
} else {
//2.加载系统的布局,布局就只有一个TextView 和一个 Button
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
SnackbarContentLayout content = (SnackbarContentLayout)inflater.inflate(hasSnackbarButtonStyleAttr(parent.getContext()) ? layout.mtrl_layout_snackbar_include : layout.design_layout_snackbar_include, parent, false);
Snackbar snackbar = new Snackbar(parent, content, content);
snackbar.setText(text);
snackbar.setDuration(duration);
return snackbar;
}
}
//以view为起点寻找合适的父布局必须是CoordinatorLayout 或者 FramLayout
private static ViewGroup findSuitableParent(View view) {
ViewGroup fallback = null;
do {
if (view instanceof CoordinatorLayout) {
return (ViewGroup)view;
}
if (view instanceof FrameLayout) {
//id就是android.R.id.content,可以查看布局加载流程setContentView
if (view.getId() == 16908290) {
return (ViewGroup)view;
}
fallback = (ViewGroup)view;
}
if (view != null) {
ViewParent parent = view.getParent();
view = parent instanceof View ? (View)parent : null;
}
} while(view != null);
return fallback;
}
public void show(int duration, SnackbarManager.Callback callback) {
synchronized(this.lock) {
if (this.isCurrentSnackbarLocked(callback)) {
this.currentSnackbar.duration = duration;
this.handler.removeCallbacksAndMessages(this.currentSnackbar);
//1.Hanlder发送消息开启动画
this.scheduleTimeoutLocked(this.currentSnackbar);
} else {
if (this.isNextSnackbarLocked(callback)) {
this.nextSnackbar.duration = duration;
} else {
this.nextSnackbar = new SnackbarManager.SnackbarRecord(duration, callback);
}
if (this.currentSnackbar == null || !this.cancelSnackbarLocked(this.currentSnackbar, 4)) {
this.currentSnackbar = null;
this.showNextSnackbarLocked();
}
}
}
}
private void scheduleTimeoutLocked(SnackbarManager.SnackbarRecord r) {
if (r.duration != -2) {
int durationMs = 2750;
if (r.duration > 0) {
durationMs = r.duration;
} else if (r.duration == -1) {
durationMs = 1500;
}
this.handler.removeCallbacksAndMessages(r);
//1.发送消息
this.handler.sendMessageDelayed(Message.obtain(this.handler, 0, r), (long)durationMs);
}
}
//Handler处理消息
static {
USE_OFFSET_API = VERSION.SDK_INT >= 16 && VERSION.SDK_INT <= 19;
SNACKBAR_STYLE_ATTR = new int[]{attr.snackbarStyle};
handler = new Handler(Looper.getMainLooper(), new android.os.Handler.Callback() {
public boolean handleMessage(Message message) {
switch(message.what) {
case 0:
((BaseTransientBottomBar)message.obj).showView();
return true;
case 1:
((BaseTransientBottomBar)message.obj).hideView(message.arg1);
return true;
default:
return false;
}
}
});
}
//开启动画显示
final void showView() {
if (ViewCompat.isLaidOut(this.view)) {
if (this.shouldAnimate()) {
this.animateViewIn();
} else {
this.onViewShown();
}
} else {
this.view.setOnLayoutChangeListener(new BaseTransientBottomBar.OnLayoutChangeListener() {
public void onLayoutChange(View view, int left, int top, int right, int bottom) {
BaseTransientBottomBar.this.view.setOnLayoutChangeListener((BaseTransientBottomBar.OnLayoutChangeListener)null);
if (BaseTransientBottomBar.this.shouldAnimate()) {
BaseTransientBottomBar.this.animateViewIn();
} else {
BaseTransientBottomBar.this.onViewShown();
}
}
});
}
}
//进入动画,使用的是属性动画,设置监听等信息
void animateViewIn() {
final int translationYBottom = this.getTranslationYBottom();
if (USE_OFFSET_API) {
ViewCompat.offsetTopAndBottom(this.view, translationYBottom);
} else {
this.view.setTranslationY((float)translationYBottom);
}
ValueAnimator animator = new ValueAnimator();
animator.setIntValues(new int[]{translationYBottom, 0});
animator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
animator.setDuration(250L);
animator.addListener(new AnimatorListenerAdapter() {
public void onAnimationStart(Animator animator) {
BaseTransientBottomBar.this.contentViewCallback.animateContentIn(70, 180);
}
public void onAnimationEnd(Animator animator) {
BaseTransientBottomBar.this.onViewShown();
}
});
animator.addUpdateListener(new AnimatorUpdateListener() {
private int previousAnimatedIntValue = translationYBottom;
public void onAnimationUpdate(ValueAnimator animator) {
int currentAnimatedIntValue = (Integer)animator.getAnimatedValue();
if (BaseTransientBottomBar.USE_OFFSET_API) {
ViewCompat.offsetTopAndBottom(BaseTransientBottomBar.this.view, currentAnimatedIntValue - this.previousAnimatedIntValue);
} else {
BaseTransientBottomBar.this.view.setTranslationY((float)currentAnimatedIntValue);
}
this.previousAnimatedIntValue = currentAnimatedIntValue;
}
});
animator.start();
}
//弹出动画
private void animateViewOut(final int event) {
ValueAnimator animator = new ValueAnimator();
animator.setIntValues(new int[]{0, this.getTranslationYBottom()});
animator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
animator.setDuration(250L);
animator.addListener(new AnimatorListenerAdapter() {
public void onAnimationStart(Animator animator) {
BaseTransientBottomBar.this.contentViewCallback.animateContentOut(0, 180);
}
public void onAnimationEnd(Animator animator) {
BaseTransientBottomBar.this.onViewHidden(event);
}
});
animator.addUpdateListener(new AnimatorUpdateListener() {
private int previousAnimatedIntValue = 0;
public void onAnimationUpdate(ValueAnimator animator) {
int currentAnimatedIntValue = (Integer)animator.getAnimatedValue();
if (BaseTransientBottomBar.USE_OFFSET_API) {
ViewCompat.offsetTopAndBottom(BaseTransientBottomBar.this.view, currentAnimatedIntValue - this.previousAnimatedIntValue);
} else {
BaseTransientBottomBar.this.view.setTranslationY((float)currentAnimatedIntValue);
}
this.previousAnimatedIntValue = currentAnimatedIntValue;
}
});
animator.start();
}
5.Snackbar与Dialog和Toast的比较
通过上文的介绍,我们知道了Snackbar和Dialog、Toast一样都是用来作为android内提示信息的,三者之间的应用场景也有所不同。
5.1.Dialog
模态对话框。也就说,此刻该对话框中的内容获取了焦点,想要操作对话框以外的功能,必须先对该对话框进行响应。
应用场景:对于删除确认、版本更新等重要性提示信息,需要用户做出选择的情况下,使用Dialog。
5.2.Toast
非模态提示框。也就说提示框的显示并不影响我们对其他地方的操作,Toast无法手动控制隐藏,需要设置Toast的显示时长,一旦显示时间结束,Toast会自动消失。如果多次点击并显示Toast,就会出现Toast重复创建并显示,给用户造成一种Toast长时间不隐藏的幻觉。
应用场景:对于无网络提示、删除成功、发布操作完成等这类不重要的提示性信息,使用Toast;
5.3.Snackbar
Snackbar和Toast比较相似,但是用途更加广泛,并且它是可以和用户进行交互的。Snackbar使用一个动画效果从屏幕的底部弹出来,过一段时间后也会自动消失。
应用场景:删除操作时,弹出Snackbar用于确认删除操作;消息发送失败时,弹出Snackbar,用于重新发送操作;当然重要的是与MD组件相结合,用户体验效果更佳。