Android MD控件
最近记忆不咋滴,凡事还是拿笔记下来比较好。需要用到的时候,看一眼就可以了。
一、简单用法
<?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">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="@dimen/actionBarWithStatusBarSize"
android:background="@color/colorAccent"
android:paddingTop="@dimen/statusBarSize"
app:titleTextAppearance="@style/TextAppearance.Title" />
</android.support.design.widget.AppBarLayout>
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:padding="@dimen/len_16"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</android.support.design.widget.CoordinatorLayout>
如上代码所示,这是一个简单的,比较标准的用法。其中:
- CoordinatorLayout 是一个协调布局,最主要的是,其内部包含了静态的抽象的Behavior<V extends View>类,通过复写该类,并在复写的LayoutParams中解子类的Behavior,可实现该布局下的直接子组件的相互关联,达到协调作用。
- AppBarLayout 继承的是布局垂直的LinearLayout,内部包含一个AppBarLayout垂直偏移量的改变回调。
- Toolbar 是Android5.0后出来,代替Actionbar的
- RecyclerView 就不多说了,集合比较少的时候效率还是ListView高
二、CoordinatorLayout
CoordinatorLayout被称为协调布局,原理上就是通过Behavior实现的。
问题:
1、CoordinatorLayout的用法?
2、CoordinatorLayout为什么只能对直接子控件有效?
3、CoordinatorLayout中的自定义Behavior?
(1)用法
第一步:通常讲android.support.design.widget.CoordinatorLayout放在最外层,但也不是绝对的。
第二步:在子控件中添加Behavior,比如上面代码中的 app:layout_behavior="@string/appbar_scrolling_view_behavior",他是CoordinatorLayout的属性,不是直接子控件是没有用的。
其中的@string/appbar_scrolling_view_behavior内容是android.support.design.widget.AppBarLayout$ScrollingViewBehavior,指明了Behavior类的位置。
public static class ScrollingViewBehavior extends HeaderScrollingViewBehavior
ScrollingViewBehavior继承的就是CoordinatorLayout中的Behavior。
那么CoordinatorLayout是怎么获取到子控件中的Behavior了?
先看看CoordinatorLayout中的LayoutParams,只摘取主要部分,只有该类才能获取到子控件中的参数。类似于android:layout_width="match_parent",这个是父控件约束子控件用的。
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
/**
* A {@link Behavior} that the child view should obey.
*/
Behavior mBehavior;
public LayoutParams(int width, int height) {
super(width, height);
}
LayoutParams(Context context, 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);
mBehaviorResolved = a.hasValue(
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);
}
}
如上,解析子控件中包不包含R.styleable.CoordinatorLayout_Layout_layout_behavior,如果存在就开始解析Behavior。
解析过程:
static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
if (TextUtils.isEmpty(name)) {
return null;
}
final String fullName;
// 开始如果是.开头,那么就需要补全名称
if (name.startsWith(".")) {
// Relative to the app package. Prepend the app package name.
fullName = context.getPackageName() + name;
} else if (name.indexOf('.') >= 0) {
// Fully qualified package name.
fullName = name;
} else {
// Assume stock behavior in this package (if we have one)
fullName = !TextUtils.isEmpty(WIDGET_PACKAGE_NAME)
? (WIDGET_PACKAGE_NAME + '.' + name)
: name;
}
// 到此定义的Behavior是个全路径了
try {
// static final ThreadLocal<Map<String, Constructor<Behavior>>> sConstructors =
new ThreadLocal<>();
Map<String, Constructor<Behavior>> constructors = sConstructors.get();
if (constructors == null) {
constructors = new HashMap<>();
sConstructors.set(constructors);
}
// 通过ThreadLocal缓存
Constructor<Behavior> c = constructors.get(fullName);
if (c == null) {
final Class<Behavior> clazz = (Class<Behavior>) Class.forName(fullName, true,
context.getClassLoader());
c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
c.setAccessible(true);
constructors.put(fullName, c);
}
// 通过反射生产Behavior对象
return c.newInstance(context, attrs);
} catch (Exception e) {
throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
}
}
到此,就知道了CoordinatorLayout和Behavior的大体工作流程了。而且还要重写了ViewGroup.MarginLayoutParams。
CoordinatorLayout的具体原理,还没研究,也暂不研究了。我觉得只要理解大体工作流程,会写自定义Behavior就可以了。关于自定义Behavior会放在后面介绍。
三、AppBarLayout
AppBarLayout相当于一个垂直结构的LinearLayout,例如上面通过ScrollingViewBehavior将滑动的RecyclerView和AppBarLayout绑定在一起,通过对方的滑动来决定AppBarLayout的滑动状态。
(1)、LayoutParams
部分源码:
public static class LayoutParams extends LinearLayout.LayoutParams {
/** @hide */
@RestrictTo(LIBRARY_GROUP)
@IntDef(flag=true, value={
SCROLL_FLAG_SCROLL,
SCROLL_FLAG_EXIT_UNTIL_COLLAPSED,
SCROLL_FLAG_ENTER_ALWAYS,
SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED,
SCROLL_FLAG_SNAP
})
从代码中可以看出它是继承了LinearLayout.LayoutParams,所以它具有LinearLayout中的所有的loyout属性,自己特有的就是layout_scrollFlags属性。其中只有上面中的5个选项。分别代表的是不同的行为,下面就从字面上说说具体的作用。
- scroll
设为scroll的View会跟随滚动事件一起发生移动。如最开始代码,该View会有RecyclerView的滑动一起滑动。 - enterAlways
设为enterAlways的View,RecyclerView往下滚动时,该View会直接往下滚动,到底之后在执行RecyclerView的向下滚动。 - exitUntilCollapsed
设为exitUntilCollapsed的View,通常要设置该View一个minHeight,往上滑动时,该View直到滑动至它的最小高度后,再响应ScrollView的内部滑动事件。 - enterAlwaysCollapsed
enterAlwaysCollapsed一般跟enterAlways一起使用,它是指,View在往下“出现”的时候,首先是RecyclerView效果,当View的高度达到最小高度时,View就暂时不去往下滚动,直到RecyclerView滑动到顶部不再滑动时,View再继续往下滑动,直到滑到View的顶部结束 - snap
当松开手指时,AppBarLayout的子View要么向上全部滚出屏幕,要么向下全部滚进屏幕,不会存在滑动在中间的地方。
(2)、OnOffsetChangedListener
public interface OnOffsetChangedListener {
/**
* Called when the {@link AppBarLayout}'s layout offset has been changed. This allows
* child views to implement custom behavior based on the offset (for instance pinning a
* view at a certain y value).
*
* @param appBarLayout the {@link AppBarLayout} which offset has changed
* @param verticalOffset the vertical offset for the parent {@link AppBarLayout}, in px
*/
void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset);
}
该回调是AppBarLayout类提供给使用者的,他会时时的返回给调用者AppBarLayout的偏移值,可以根据偏移值来做一些额外的工作,比如显示的动画效果。
四、CollapsingToolbarLayout 折叠布局
(1)、作用
从字面的意思就是,他提供了一个可以折叠的Toolbar,它继承至FrameLayout。通过给直接子控件设置layout_collapseMode和layout_collapseParallaxMultiplier属性,不同的折叠方式和视差因子带来不同的效果。
(2)、自定义属性
<declare-styleable name="CollapsingToolbarLayout">
<attr name="expandedTitleMargin" format="dimension" />
<attr name="expandedTitleMarginStart" format="dimension" />
<attr name="expandedTitleMarginTop" format="dimension" />
<attr name="expandedTitleMarginEnd" format="dimension" />
<attr name="expandedTitleMarginBottom" format="dimension" />
<attr name="expandedTitleTextAppearance" format="reference" />
<attr name="collapsedTitleTextAppearance" format="reference" />
<attr name="contentScrim" format="color" />
<attr name="statusBarScrim" format="color" />
<attr name="toolbarId" format="reference" />
<attr name="scrimVisibleHeightTrigger" format="dimension" />
<attr name="scrimAnimationDuration" format="integer" />
<attr name="collapsedTitleGravity">
<flag name="top" value="0x30" />
<flag name="bottom" value="0x50" />
<flag name="left" value="0x03" />
<flag name="right" value="0x05" />
<flag name="center_vertical" value="0x10" />
<flag name="fill_vertical" value="0x70" />
<flag name="center_horizontal" value="0x01" />
<flag name="center" value="0x11" />
<flag name="start" value="0x00800003" />
<flag name="end" value="0x00800005" />
</attr>
<attr name="expandedTitleGravity">
<flag name="top" value="0x30" />
<flag name="bottom" value="0x50" />
<flag name="left" value="0x03" />
<flag name="right" value="0x05" />
<flag name="center_vertical" value="0x10" />
<flag name="fill_vertical" value="0x70" />
<flag name="center_horizontal" value="0x01" />
<flag name="center" value="0x11" />
<flag name="start" value="0x00800003" />
<flag name="end" value="0x00800005" />
</attr>
<attr name="titleEnabled" format="boolean" />
<attr name="title" />
</declare-styleable>
- expandedTitleMargin、expandedTitleMarginStart、expandedTitleMarginTop、expandedTitleMarginEnd、expandedTitleMarginBottom
展开状态下,标题栏的位置信息,通过Margin值来决定的。而闭合非展开时候的标题栏的位置是固定好的。 - expandedTitleTextAppearance
展开状态下,Toolbar标题的样式设置 - collapsedTitleTextAppearance
折叠状态下,Toolbar标题的样式设置 - contentScrim
设置折叠时工具栏布局的颜色 - statusBarScrim
设置折叠时状态栏的颜色 - toolbarId
绑定的Toolbar的ID - collapsedTitleGravity
折叠状态的标题如何放置,可选值:top、bottom等 - expandedTitleGravity
展开状态的标题如何放置,可选值:top、bottom等
(3)、CollapsingToolbarLayout.LayoutParams
直接看解析布局代码:
private static final float DEFAULT_PARALLAX_MULTIPLIER = 0.5f;
@RestrictTo(GROUP_ID)
@IntDef({
COLLAPSE_MODE_OFF,
COLLAPSE_MODE_PIN,
COLLAPSE_MODE_PARALLAX
})
@Retention(RetentionPolicy.SOURCE)
@interface CollapseMode {}
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
TypedArray a = c.obtainStyledAttributes(attrs,
R.styleable.CollapsingToolbarLayout_Layout);
mCollapseMode = a.getInt(
R.styleable.CollapsingToolbarLayout_Layout_layout_collapseMode,
COLLAPSE_MODE_OFF);
setParallaxMultiplier(a.getFloat(
R.styleable.CollapsingToolbarLayout_Layout_layout_collapseParallaxMultiplier,
DEFAULT_PARALLAX_MULTIPLIER));
a.recycle();
}
一共有两个布局属性,app:layout_collapseMode="parallax"和app:layout_collapseParallaxMultiplier="0.7",也就是展开模式和视差因子。
-
layout_collapseMode
1、off:这个是默认属性,布局将正常显示,没有折叠的行为。
2、pin:CollapsingToolbarLayout折叠后,此布局将固定在顶部。
3、parallax:CollapsingToolbarLayout折叠时,此布局也会有视差折叠效果。 -
layout_collapseParallaxMultiplier
视差滚动因子,值为:0~1。
(4)、简单例子
<?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:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="156dp"
android:fitsSystemWindows="true"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<android.support.design.widget.CollapsingToolbarLayout
android:id="@+id/collapsing_toolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
app:collapsedTitleTextAppearance="@style/TextAppearance.Title"
app:contentScrim="#647743"
app:expandedTitleGravity="bottom|center_horizontal"
app:expandedTitleMarginEnd="64dp"
app:expandedTitleMarginStart="48dp"
app:expandedTitleTextAppearance="@style/TextAppearance.Title"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
app:statusBarScrim="@android:color/transparent">
<ImageView
android:id="@+id/backdrop"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:scaleType="centerCrop"
android:src="@mipmap/ic_launcher"
app:layout_collapseMode="parallax"
app:layout_collapseParallaxMultiplier="1" />
<android.support.v7.widget.Toolbar
android:id="@+id/tool_bar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
</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">
....省略
</android.support.v4.widget.NestedScrollView>
<android.support.design.widget.FloatingActionButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:clickable="true"
android:src="@mipmap/ic_launcher"
app:layout_anchor="@id/appbar" // 设置一个锚点
app:layout_anchorGravity="bottom|end" />
</android.support.design.widget.CoordinatorLayout>
四、Toolbar
(1)、简单用法
- 布局
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.Toolbar
android:paddingTop="25dp"
android:background="@color/colorPrimary"
android:id= "@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="73dp">
</android.support.v7.widget.Toolbar>
</RelativeLayout>
- menu布局
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_edit"
android:icon="@mipmap/ic_launcher"
android:orderInCategory="80"
android:title="删除"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_share"
android:icon="@mipmap/ic_launcher"
android:orderInCategory="90"
android:title="编辑"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_settings"
android:icon="@mipmap/ic_launcher"
android:orderInCategory="100"
android:title="设置"
app:showAsAction="ifRoom" />
</menu>
- Theme
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
<item name="windowNoTitle">true</item>
<item name="android:statusBarColor">@null</item>
<!--去除顶部的状态栏-->
<item name="android:windowTranslucentStatus">true</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowAnimationStyle">@null</item>
<item name="android:windowIsFloating">false</item>
<item name="android:windowFrame">@null</item>
<!--去除默认阴影-->
<item name="android:elevation">0dp</item>
<item name="android:outlineProvider">none</item>
<item name="actionBarSize">48dp</item>
<item name="toolbarTitleColor">@color/colorAccent</item>
<item name="toolbarTitleSize">30sp</item>
</style>
- Activity
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_toolbar);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
// 主标题
toolbar.setTitle("Title");
//设置toolbar
setSupportActionBar(toolbar);
//左边的小箭头(注意需要在setSupportActionBar(toolbar)之后才有效果)
toolbar.setNavigationIcon(R.drawable.ic_back);
//菜单点击事件(注意需要在setSupportActionBar(toolbar)之后才有效果)
toolbar.setOnMenuItemClickListener(this);
}
@Override
public boolean onMenuItemClick(MenuItem item) {
Toast.makeText(this,item.getTitle(),Toast.LENGTH_SHORT).show();
return true;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_toolbar, menu);
return true;
}
五、自定义Behavior
Behavior是关联着CoordinatorLayout下面的直接子布局,让他们能够相互监听对方滑动状态。CoordinatorLayout只是相当于一个桥梁的作用。
下面通过一个自定义android.support.design.widget.FloatingActionButton的Behavior,让向上滑动的时候,FloatingActionButton显示,向下滑动时隐藏。
public class TranslationBehavior extends FloatingActionButton.Behavior {
public TranslationBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
// 关注垂直滚动 ,而且向上的时候是出来,向下是隐藏
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child, View directTargetChild, View target, int nestedScrollAxes) {
return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL;
}
private boolean isOut = false;
@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) {
if (!isOut) {
int translationY = ((CoordinatorLayout.LayoutParams) child.getLayoutParams()).bottomMargin + child.getMeasuredHeight();
child.animate().cancel();
child.animate().translationY(translationY).setDuration(200).start();
isOut = true;
}
} else {
if (isOut) {
// 往下滑动
child.animate().cancel();
child.animate().translationY(0).setDuration(200).start();
isOut = false;
}
}
}
}