Material Design的基本概念
Material Design是Google设计的一套视觉语言,将优先的经典的设计原理与科技创新相结合,为开发者提供一套完成视觉和交互设计规范。移动设备是这套设计语言的基础对象,让用户在不同的平台、不同尺寸的设备上能保持一致的体验。
Material Design强调交互上的即时反馈,即对于用户的触控等行为app需要给出即时的反应。同时Material Design要求应用给用户带入感,让用户在使用时是沉浸在当前的应用当中。例如Google给出了沉浸式状态栏等“工具”,希望通过改变StatusBar和NavigationBar来给用户更强的融入感,专注于应用本身提供的内容。
Google从动画、颜色、样式、触控反馈、布局等多个方面给出了Material Design的设计要求。无论是单一的控件还是图文布局,Google都给出了明确的设计说明,有兴趣的同学可以去上方提到的官方链接处做进一步了解。
RecyclerView的使用
写条目布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/tv_item"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
写Adapter以及其内部类自定义的ViewHolder:
public class MyRecyclerViewAdapter extends RecyclerView.Adapter<MyRecyclerViewAdapter.MyViewHolder> {
private List<String> mDatas;
private Context mContext;
public MyRecyclerViewAdapter(Context context, List<String> datas) {
mContext = context;
mDatas = datas;
}
//自定义ViewHolder
class MyViewHolder extends RecyclerView.ViewHolder {
TextView tv_item;
MyViewHolder(View itemView) {
super(itemView);
tv_item = (TextView) itemView.findViewById(R.id.tv_item);
}
}
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
//创建ViewHolder
View itemView = View.inflate(parent.getContext(), R.layout.item_list, null);
return new MyViewHolder(itemView);
}
@Override
public void onBindViewHolder(MyViewHolder holder, final int position) {
//数据绑定
holder.tv_item.setText(mDatas.get(position));
//设置点击监听
holder.tv_item.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(mContext, mDatas.get(position), Toast.LENGTH_SHORT).show();
}
});
}
@Override
public int getItemCount() {
//数据集大小
return mDatas.size();
}
}
在Activity中的使用,通过设置不同的LayoutManager就可以实现不同的布局效果:
public class MDRecyclerViewActivity extends AppCompatActivity {
private RecyclerView rv_list;
private MyRecyclerViewAdapter mAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_md_recyclerview);
rv_list = (RecyclerView) findViewById(R.id.rv_list);
List<String> datas = new ArrayList<>();
for (int i = 0; i < 100; i++) {
datas.add("第" + i + "个数据");
}
mAdapter = new MyRecyclerViewAdapter(this, datas);
//竖直线性,不反转布局
// rv_list.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
//表格布局
// rv_list.setLayoutManager(new GridLayoutManager(this, 3));
//瀑布流布局
rv_list.setLayoutManager(new StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL));
rv_list.setAdapter(mAdapter);
}
}
Inflate时的注意事项:
在Adapter中的onCreateViewHolder,需要Inflate布局文件,有三种写法:
View itemView = View.inflate(parent.getContext(), R.layout.item_list, null);
View itemView = View.inflate(parent.getContext(), R.layout.item_list, parent);
View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_list, parent, false);
写法一般情况下是没有问题的,但是当我们在onBindViewHolder中拿到布局中TextView的LayoutParams的时候,就有可能返回空。
写法二直接Crash,因为ItemView布局已经有一个Parent了(Inflate的时候把ItemView添加到Recycleview了),不能再添加一个Parent(Recycleview再次添加ItemView)。
写法三是一、二的两种兼容方案,推荐这种写法。
添加增删接口
在Adapter中添加以及删除的接口:
//条目的增删
public void addItem(String data, int position) {
mDatas.add(position, data);
notifyItemInserted(position);
}
public void removeItem(int position) {
mDatas.remove(position);
notifyItemRemoved(position);
}
注意如果你想使用RecyclerView提供的增删动画,那么就需要使用新增的notify方法。
添加条目点击监听
自定义一个点击回调接口:
//条目点击
ItemClickListener mItemClickListener;
public interface ItemClickListener {
void onclick(int position, String data);
}
public void setItemClickListener(ItemClickListener listener) {
mItemClickListener = listener;
}
public abstract class ItemClickListenerPosition implements View.OnClickListener {
private int mPosition;
public ItemClickListenerPosition(int position) {
mPosition = position;
}
public int getPosition() {
return mPosition;
}
}
ItemClickListenerPosition是一个自定义的OnClickListener,目的就是为了把Position和监听绑定在一起,同时也使用了getLayoutPosition方法。防止了点击Position错乱的问题。
(onBindViewHolder() 方法中的位置参数 position 不是实时更新的,例如在我们删除元素后,item 的 position 并没有改变。)
然后在onBindViewHolder里面进行监听:
@Override
public void onBindViewHolder(final MyViewHolder holder, int position) {
//数据绑定
//设置条目监听
holder.itemView.setOnClickListener(new ItemClickListenerPosition(holder.getLayoutPosition()) {
@Override
public void onClick(View v) {
if (mItemClickListener != null) {
mItemClickListener.onclick(getPosition(), mDatas.get(getPosition()));
}
}
});
}
想详细了解RecyclerView的使用,请参考《 一篇博客理解Recyclerview的使用》
DrawerLayout+NavigationView
使用DrawerLayout实现侧滑:
定义一个布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#D197F2"
app:title="我是标题"
app:titleTextColor="#fff"/>
<android.support.v4.widget.DrawerLayout
android:id="@+id/drawer"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="内容"/>
</LinearLayout>
<LinearLayout
android:layout_width="200dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="@android:color/holo_blue_light"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="侧滑菜单1"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="侧滑菜单2"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="侧滑菜单3"/>
</LinearLayout>
</android.support.v4.widget.DrawerLayout>
</LinearLayout>
这个布局侧滑菜单包括了菜单部分以及内容部分,用DrawerLayout来包裹起来。其中,菜单部分的根布局需要添加android:layout_gravity=”start”,如果是右滑的话,改为end即可。
这样就可以完成了一个基本的侧滑效果。
DrawerLayout的实现其实是通过ViewDragHelper来实现的,DrawerLayout构造函数的相关代码如下:
public DrawerLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mLeftCallback = new ViewDragCallback(Gravity.LEFT);
mRightCallback = new ViewDragCallback(Gravity.RIGHT);
mLeftDragger = ViewDragHelper.create(this, TOUCH_SLOP_SENSITIVITY, mLeftCallback);
mLeftDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
mLeftDragger.setMinVelocity(minVel);
mLeftCallback.setDragger(mLeftDragger);
}
利用DrawerLayout的监听实现一些效果
例如,我们可以实现侧滑的时候,Toolbar左上角的按钮实时变化,我们可以添加一个监听ActionBarDrawerToggle:
toolbar = (Toolbar) findViewById(R.id.toolbar);
drawer = (DrawerLayout) findViewById(R.id.drawer);
ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.drawer_open, R.string.drawer_close);
toggle.syncState();
drawer.addDrawerListener(toggle);
分析一下实现原理:
其中,ActionBarDrawerToggle实现了DrawerLayout.DrawerListener。并且在滑动的过程中不断 刷新左上角的Drawerable:
@Override
public void onDrawerSlide(View drawerView, float slideOffset) {
setPosition(Math.min(1f, Math.max(0, slideOffset)));
}
setPosition的实现如下:
private void setPosition(float position) {
if (position == 1f) {
mSlider.setVerticalMirror(true);
} else if (position == 0f) {
mSlider.setVerticalMirror(false);
}
mSlider.setProgress(position);
}
其实就是在滑动的过程中不断改变mSlider(一个自定义Drawerable对象)的Progress,从而不断刷新状态。
因此,我们可以做一些自定义的特效,例如侧滑的时候缩放、平移:
drawer.addDrawerListener(new DrawerLayout.DrawerListener() {
@Override
public void onDrawerStateChanged(int newState) {
// 状态发生改变
}
@Override
public void onDrawerSlide(View drawerView, float slideOffset) {
// 滑动的过程当中不断地回调 slideOffset:0~1
View content = drawer.getChildAt(0);
float scale = 1 - slideOffset;//1~0
float leftScale = (float) (1 - 0.3 * scale);
float rightScale = (float) (0.7f + 0.3 * scale);//0.7~1
drawerView.setScaleX(leftScale);//1~0.7
drawerView.setScaleY(leftScale);//1~0.7
content.setScaleX(rightScale);
content.setScaleY(rightScale);
content.setTranslationX(drawerView.getMeasuredWidth() * (1 - scale));//0~width
}
@Override
public void onDrawerOpened(View drawerView) {
// 打开
}
@Override
public void onDrawerClosed(View drawerView) {
// 关闭
}
});
DrawerLayout+NavigationView实现侧滑
<android.support.v4.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/drawer"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 内容部分 -->
<FrameLayout
android:id="@+id/fl"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
/>
<!-- 菜单部分 -->
<android.support.design.widget.NavigationView
android:id="@+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
app:headerLayout="@layout/navigation_headerlayout"
app:menu="@menu/navigation_menu"
/>
</android.support.v4.widget.DrawerLayout>
我们指定了头部如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical">
<ImageView
android:id="@+id/iv_icon"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_marginTop="20dp"
android:src="@drawable/icon_people"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="璐宝宝"
android:textSize="20sp"/>
</LinearLayout>
菜单部分如下(menu文件夹下建立),其中菜单可以嵌套:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/action_gallery"
android:icon="@android:drawable/ic_menu_gallery"
android:orderInCategory="100"
android:title="相册"
/>
<item
android:id="@+id/action_details"
android:icon="@android:drawable/ic_menu_info_details"
android:orderInCategory="100"
android:title="详情"
/>
<item
android:id="@+id/action_about"
android:icon="@android:drawable/ic_menu_help"
android:orderInCategory="100"
android:title="关于"
/>
<item
android:id="@+id/action_music"
android:icon="@android:drawable/ic_menu_more"
android:orderInCategory="100"
android:title="音乐"
>
<menu>
<item
android:id="@+id/action_play"
android:icon="@android:drawable/ic_media_play"
android:title="播放"/>
<item
android:id="@+id/action_pause"
android:icon="@android:drawable/ic_media_pause"
android:title="暫停"/>
</menu>
</item>
</menu>
到现在为止,就可以实现侧滑了,最后我们添加上对应的点击事件,然后关闭菜单:
nav_view = (NavigationView) findViewById(R.id.nav_view);
drawer = (DrawerLayout) findViewById(R.id.drawer);
nav_view.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
Toast.makeText(NavigationViewActivity.this, item.getTitle(), Toast.LENGTH_SHORT).show();
drawer.closeDrawer(nav_view);
return false;
}
});
nav_view.getHeaderView(0).findViewById(R.id.iv_icon).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(NavigationViewActivity.this, "点击了头部的图标", Toast.LENGTH_SHORT).show();
drawer.closeDrawer(nav_view);
}
});
SnackBar
//其中View是一个锚点
Snackbar snackbar = Snackbar.make(v, "是否打开XXX模式", Snackbar.LENGTH_SHORT);
//只能设置一个Action
snackbar.setAction("打开", new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.e(TAG, "打开XXX模式");
}
});
//监听打开与关闭
snackbar.setCallback(new Snackbar.Callback() {
@Override
public void onShown(Snackbar snackbar) {
super.onShown(snackbar);
Log.e(TAG, "显示");
}
@Override
public void onDismissed(Snackbar snackbar, int event) {
super.onDismissed(snackbar, event);
Log.e(TAG, "关闭");
}
});
snackbar.show();
Snackbar的Duration有三种:
Snackbar.LENGTH_SHORT
Snackbar.LENGTH_LONG
Snackbar.LENGTH_INDEFINITE—无限长
make方法传入的是一个锚点,这里传入了一个Button对象。然后还可以设置动作以及回调监听。
Snackbar的详细使用参见《轻量级控件SnackBar使用以及源码分析》
TextInputLayout
布局:
<android.support.design.widget.TextInputLayout
android:id="@+id/til_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:hintAnimationEnabled="true">
<EditText
android:id="@+id/et_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="请输入用户名"/>
</android.support.design.widget.TextInputLayout>
hintAnimationEnabled属性是设置是否开启Hint的动画。
需要注意的是,TextInputLayout必须包含一个EditText。
下面是一个基本的例子:
public class TextInputMainActivity extends AppCompatActivity {
private TextInputLayout til_input;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_text_input);
til_input = (TextInputLayout) findViewById(R.id.til_input);
til_input.getEditText().addTextChangedListener(new MaxTextTextWatcher(til_input, "字数不能大于6", 6));
//开启计数
til_input.setCounterEnabled(true);
til_input.setCounterMaxLength(6);
}
class MaxTextTextWatcher implements TextWatcher {
private TextInputLayout mTextInputLayout;
private String mErrorString;
private int maxTextCount;
public MaxTextTextWatcher(TextInputLayout textInputLayout, String errorString, int maxTextCount) {
mTextInputLayout = textInputLayout;
mErrorString = errorString;
this.maxTextCount = maxTextCount;
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
String str = mTextInputLayout.getEditText().getText().toString().trim();
if (!TextUtils.isEmpty(str)) {
if (str.length() > maxTextCount) {
//显示错误
//设置错误提示
mTextInputLayout.setError(mErrorString);
mTextInputLayout.setErrorEnabled(true);
} else {
//关闭错误
mTextInputLayout.setErrorEnabled(false);
}
}
}
}
}
在这个例子里面,我们利用了TextInputLayout的错误提示、字数统计功能,基本的使用都比较简单。
在TextInputLayout可以轻松地通过getEditText方法找到它所包裹的EditText。、
在显示错误的时候,需要先设置错误的提示,每次显示的时候都要设置。
大部分属性都可以通过xml的方式设置,这里通过代码动态设置只是为了方便演示。
TextInputLayout详细使用请参见强大的提示控件TextInputLayout使用以及源码分析
Toolbar
<android.support.v7.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
app:logo="@drawable/ic_launcher"
app:subtitle="子标题"
app:navigationIcon="@drawable/abc_ic_ab_back_mtrl_am_alpha"
app:subtitleTextColor="#fff"
app:title="我是标题"
app:titleTextColor="#fff"></android.support.v7.widget.Toolbar>
Toolbar是一个ViewGroup,里面可以放子控件。因此,如果你想标题居中的话,那么就放入一个TextView吧。
这里的?attr/colorPrimary是使用了系统的颜色值,当然我们也可以在主题中重写。
注意:Toolbar需要使用Appcompat的一套东西。
返回监听:
toolbar.setNavigationOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
实现Toolbar随着界面滑动透明度变化效果
首先我们需要一个布局,通过相对布局把Toolbar压在ScrollView(或者ListView、RecyclerView)的上面。Toolbar的高度与ScrollView上方内边距都使用系统的actionBarSize。
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.nan.advancedui.toolbar.MyScrollView
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false"
android:paddingTop="?attr/actionBarSize">
<!--这里是我们的内容布局-->
</com.nan.advancedui.toolbar.MyScrollView>
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:title="标题"
>
</android.support.v7.widget.Toolbar>
</RelativeLayout>
还需要注意给ScrollView设置多两个属性,不然的话滑出去以后上内边距会一直保留:
android:clipToPadding=”false” 该控件的绘制范围是否不在Padding里面。false:绘制的时候范围会考虑padding即会往里面缩进。
android:clipChildren=”false” 子控件是否能不超出padding的区域(比如ScrollView上滑动的时候,child可以滑出该区域)
然后监听滑动事件,这里如果是ScrollView的话,需要自定义重写方法才能监听:
public class MyScrollView extends ScrollView {
private OnAlphaListener listener;
public void setOnAlphaListener(OnAlphaListener listener) {
this.listener = listener;
}
public MyScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
if (listener != null) {
int scrollY = getScrollY();
int screen_height = getContext().getResources().getDisplayMetrics().heightPixels;
if (scrollY <= screen_height / 3f) {//0~1f,而透明度应该是1~0f
listener.onAlpha(1 - scrollY / (screen_height / 3f));//alpha=滑出去的高度/(screen_height/3f)
}
}
}
}
透明度的计算需要根据实际情况来
自定义一个接口回调,Activity(Fragment)实:
public interface OnAlphaListener {
void onAlpha(float alpha);
}
界面的逻辑如下:
public class ToolbarActivity extends AppCompatActivity implements OnAlphaListener {
private Toolbar mToolbar;
private MyScrollView mScrollview;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_toolbar);
mToolbar = (Toolbar) findViewById(R.id.toolbar);
mScrollview = (MyScrollView) findViewById(R.id.scrollView);
mScrollview.setOnAlphaListener(this);
}
@Override
public void onAlpha(float alpha) {
mToolbar.setAlpha(alpha);
}
}
SearchView
SearchView也是V7包的控件,一般也是跟Toolbar中的菜单结合使用。
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:context="com.ricky.materialdesign.toolbar.MainActivity"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_search"
android:orderInCategory="100"
app:actionViewClass="android.support.v7.widget.SearchView"
app:showAsAction="always"
android:title="查找"/>
<item
android:id="@+id/action_settings"
android:orderInCategory="100"
app:showAsAction="never"
android:title="设置"/>
<item
android:id="@+id/action_share"
android:orderInCategory="100"
app:showAsAction="always"
android:title="分享"
android:icon="@android:drawable/ic_menu_share"/>
<item
android:id="@+id/action_edit"
android:orderInCategory="100"
app:showAsAction="ifRoom"
android:title="编辑"
android:icon="@android:drawable/ic_menu_edit"/>
</menu>
这里app:actionViewClass=”android.support.v7.widget.SearchView”是指定了菜单的View是一个SearchView。因此我们就可以在代码中使用了:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
//SearchView在Menu里面,我们通过Item的getActionView就可以找到
MenuItem item = menu.findItem(R.id.action_search);
SearchView searchView = (SearchView) MenuItemCompat.getActionView(item);
//设置一出来就直接呈现搜索框---SearchView
searchView.setIconified(false);
//进来就呈现搜索框并且不能被隐藏
//searchView.setIconifiedByDefault(false);
//有时候我们需要实现自定义扩展效果
//通过猜想,searchView用到了一个布局,去appcompat里面找到abc_search_view.xml,该里面的控件的属性
ImageView icon = (ImageView) searchView.findViewById(R.id.search_go_btn);
icon.setImageResource(R.drawable.abc_ic_voice_search_api_mtrl_alpha);
icon.setVisibility(View.VISIBLE);
searchView.setMaxWidth(200);
//输入提示
SearchView.SearchAutoComplete et = (SearchView.SearchAutoComplete) searchView.findViewById(R.id.search_src_text);
et.setHint("输入商品名或首字母");
et.setHintTextColor(Color.WHITE);
//设置提交按钮是否可用(可见)
searchView.setSubmitButtonEnabled(true);
//提交按钮监听
icon.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, "提交", 1).show();
}
});
//像AutoCompleteTextView一样使用提示
//searchView.setSuggestionsAdapter(adapter);
//监听焦点改变
searchView.setOnQueryTextFocusChangeListener(new OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
// TODO Auto-generated method stub
}
});
//searchView的关闭监听
searchView.setOnCloseListener(new OnCloseListener() {
@Override
public boolean onClose() {
// TODO Auto-generated method stub
return false;
}
});
searchView.setOnSearchClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, "提交", 0).show();
}
});
//监听文本变化,调用查询
searchView.setOnQueryTextListener(new OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String text) {
//提交文本
Toast.makeText(MainActivity.this, "提交文本:"+text, 0).show();
return false;
}
@Override
public boolean onQueryTextChange(String text) {
// 文本改变的时候回调
System.out.println("文本变化~~~~~"+text);
return false;
}
});
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
TabLayout
下面以TabLayout+ViewPager+Fragment为例,讲述TabLayout的基本使用。
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.design.widget.TabLayout
android:id="@+id/tablayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabGravity="center"
app:tabIndicatorColor="#4ce91c"
app:tabMode="scrollable"
app:tabSelectedTextColor="#4ce91c"
app:tabTextColor="#ccc"
app:tabIndicatorHeight="5dp"
/>
<android.support.v4.view.ViewPager
android:id="@+id/vp"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
/>
</LinearLayout>
其中,需要关注的属性有:
app:tabIndicatorColor="@color/colorPrimary_pink"//指示器的颜色
app:tabTextColor="@color/colorPrimary_pink"//tab的文字颜色
app:tabSelectedTextColor="@color/colorPrimary_pinkDark"//选中的tab的文字颜色
app:tabMode="fixed"//scrollable:可滑动;fixed:不能滑动,平分tabLayout宽度
app:tabGravity="center"// fill:tab平均填充整个宽度;center:tab居中显示
需要切换的Fragment,为了方便,我们重用一个Fragment:
public class NewsDetailFragment extends Fragment {
@Override
@Nullable
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
TextView tv = new TextView(getContext());
Bundle bundle = getArguments();
String title = bundle.getString("title");
tv.setBackgroundColor(Color.rgb((int)(Math.random()*255), (int)(Math.random()*255), (int)(Math.random()*255)));
tv.setText(title);
return tv;
}
}
Activity的代码:
public class TabLayoutActivity extends AppCompatActivity {
private TabLayout tabLayout;
private String[] title = {
"头条",
"新闻",
"娱乐",
"体育",
"科技",
"美女",
"财经",
"汽车",
"房子",
"头条"
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_tab_layout);
final ViewPager viewPager = (ViewPager) findViewById(R.id.vp);
tabLayout = (TabLayout) findViewById(R.id.tablayout);
MyPagerAdapter adapter = new MyPagerAdapter(getSupportFragmentManager());
//1.TabLayout和Viewpager关联
// tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
//
// @Override
// public void onTabUnselected(TabLayout.Tab arg0) {
//
// }
//
// @Override
// public void onTabSelected(TabLayout.Tab tab) {
// // 被选中的时候回调
// viewPager.setCurrentItem(tab.getPosition(), true);
// }
//
// @Override
// public void onTabReselected(TabLayout.Tab tab) {
//
// }
// });
//2.ViewPager滑动关联tabLayout
// viewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(tabLayout));
//设置tabLayout的标签来自于PagerAdapter
// tabLayout.setTabsFromPagerAdapter(adapter);
//设置tabLayout的标签来自于PagerAdapter
tabLayout.setupWithViewPager(viewPager);
viewPager.setAdapter(adapter);
//设置Indicator的左右间距(Indicator的宽度)
setIndicator(this, tabLayout, 15, 15);
}
class MyPagerAdapter extends FragmentPagerAdapter {
public MyPagerAdapter(FragmentManager fm) {
super(fm);
}
@Override
public CharSequence getPageTitle(int position) {
return title[position];
}
@Override
public Fragment getItem(int position) {
Fragment f = new NewsDetailFragment();
Bundle bundle = new Bundle();
bundle.putString("title", title[position]);
f.setArguments(bundle);
return f;
}
@Override
public int getCount() {
return title.length;
}
}
//下面三个方法是设置Indicator
public static void setIndicator(Context context, TabLayout tabs, int leftDip, int rightDip) {
Class<?> tabLayout = tabs.getClass();
Field tabStrip = null;
try {
tabStrip = tabLayout.getDeclaredField("mTabStrip");
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
tabStrip.setAccessible(true);
LinearLayout ll_tab = null;
try {
ll_tab = (LinearLayout) tabStrip.get(tabs);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
int left = (int) (getDisplayMetrics(context).density * leftDip);
int right = (int) (getDisplayMetrics(context).density * rightDip);
for (int i = 0; i < ll_tab.getChildCount(); i++) {
View child = ll_tab.getChildAt(i);
child.setPadding(0, 0, 0, 0);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, 1);
params.leftMargin = left;
params.rightMargin = right;
child.setLayoutParams(params);
child.invalidate();
}
}
public static DisplayMetrics getDisplayMetrics(Context context) {
DisplayMetrics metric = new DisplayMetrics();
((Activity) context).getWindowManager().getDefaultDisplay().getMetrics(metric);
return metric;
}
public static float getPXfromDP(float value, Context context) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value,
context.getResources().getDisplayMetrics());
}
}
新提供的tabLayout.setupWithViewPager(viewPager);方法代替了注释中的3个方法了,其实内部做的事都是一样的。TabLayout默认没有提供修改Indicator宽度的函数,需要我们通过反射的方式去设置。
用TabLayout实现底部导航(相对于传统的TabHost,它是可滑动的)
只需要三个步骤:
1.在布局中就把TabLayout放在布局底部
2。去掉底部的indicator,app:tabIndicatorHeight=”0dp”
3.实现自己的效果,自定义的标签布局
代码如下:
for (int i = 0; i < tabLayout.getTabCount(); i++) {
TabLayout.Tab tab = tabLayout.getTabAt(i);
tab.setCustomView(view);
}
CardView
CardView就是一个ViewGroup,里面可以放置子布局
<android.support.v7.widget.CardView
android:layout_width="300dp"
android:layout_height="200dp"
android:layout_margin="16dp"
android:clickable="true"
android:foreground="?attr/selectableItemBackground"
android:stateListAnimator="@drawable/z_translation"
app:cardCornerRadius="10dp"
app:cardElevation="10dp">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/test"/>
</android.support.v7.widget.CardView>
其中,cardElevation是设置高度,高度越高,阴影越明显。foreground属性是设置点击水波纹效果。cardCornerRadius是设置圆角的大小。stateListAnimator是设置点击的动画效果,点击以后,往下压,z_translation如下:
<selector
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:state_pressed="true">
<objectAnimator
android:duration="@android:integer/config_shortAnimTime"
android:propertyName="translationZ"
android:valueTo="-15dp"
android:valueType="floatType"/>
</item>
<item>
<objectAnimator
android:duration="@android:integer/config_shortAnimTime"
android:propertyName="translationZ"
android:valueTo="0dp"
android:valueType="floatType"/>
</item>
</selector>
CardView兼容性开发
创建layout、layout-v21两套布局,根据下面的差别写两份CardView的布局文件。其中尤其注意的是stateListAnimator这个属性,如果最小SDK版本低于21,AS就会警告。
1.阴影的细微差别
5.x系统:边距阴影比较小,需要手动添加边距16dp,android:layout_margin=”16dp”
4.x系统:边距阴影比较大,手动修改边距0dp(原因:兼容包里面设置阴影效果自动设置了margin来处理16dp)
2.圆角效果的细微差别
5.x系统:图片和布局都可以很好的呈现圆角效果,图片也变圆角了,因此5.x上面不需要设置app:contentPadding
4.x系统:图不能变成圆角(图片的直角会顶到CardView的边上),如果要做成5.x一样的效果:通过加载图片的时候自己去处理成圆角(与CardView的圆角大小一样),因此4.x上面不需要设置app:contentPadding,从而尽量好看一些
3.水波纹效果的差别
5.x系统:可以通过
android:foreground=”?attr/selectableItemBackground”实现
4.x系统:需要自己实现
4.点击动画的差别
5.x系统:可以通过android:stateListAnimator=”@drawable/z_translation”设置动画
4.x系统:不能设置上述的动画,因为4.x没有z轴的概念
FloatingActionButton
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_gravity="right|bottom"
android:onClick="rotate"
android:src="@drawable/ic_add_white_24dp"
app:backgroundTint="?attr/colorPrimary"
app:elevation="10dp"
app:fabSize="normal"
app:rippleColor="#f00"
/>
其中:
1.src属性是设置图标
2.backgroundTint是设置背景色(图标是透明背景的)
3.elevation是设置阴影大小
4.fabsize是设置图标的大小,一般为normal(不用设置)
5.rippleColor是设置水波纹的颜色
点击事件如下(旋转):
private boolean reverse = false;
public void rotate(View v) {
float toDegree = reverse ? -180f : 180f;
ObjectAnimator animator = ObjectAnimator
.ofFloat(v, "rotation", 0.0f, toDegree)
.setDuration(400);
animator.start();
reverse = !reverse;
}
FloatingActionButton动画
方案1:列表滑动的时候FloatingActionButton隐藏与显示,通过自定义OnScrollListener实现
public class FabScrollListener extends OnScrollListener {
private static final int THRESHOLD = 20;
private int distance = 0;
private HideScrollListener hideListener;
private boolean visible = true;//是否可见
public FabScrollListener(HideScrollListener hideScrollListener) {
this.hideListener = hideScrollListener;
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
/**
* dy:Y轴方向的增量
* 有正和负
* 当正在执行动画的时候,就不要再执行了
*/
if (distance > THRESHOLD && visible) {
//隐藏动画
visible = false;
hideListener.onHide();
distance = 0;
} else if (distance < -THRESHOLD && !visible) {
//显示动画
visible = true;
hideListener.onShow();
distance = 0;
}
if (visible && dy > 0 || (!visible && dy < 0)) {
distance += dy;
}
}
}
自定义一个OnScrollListener,重写onScrolled方法。判断当前的滚动方向、滚动距离、当前的FloatingActionButton是否显示来进行相应的逻辑处理。
其中HideScrollListener是一个自定义的监听接口:
public interface HideScrollListener {
void onHide();
void onShow();
}
由Activity实现这个接口:
public class FabAnimActivity extends AppCompatActivity implements HideScrollListener {
private RecyclerView recyclerview;
private ImageButton fab;
private Toolbar toolbar;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//省略一些代码
//添加滑动监听
recyclerview.addOnScrollListener(new FabScrollListener(this));
}
@Override
public void onHide() {
// 隐藏动画--属性动画
toolbar.animate().translationY(-toolbar.getHeight()).setInterpolator(new AccelerateInterpolator(3));
RelativeLayout.LayoutParams layoutParams = (LayoutParams) fab.getLayoutParams();
fab.animate().translationY(fab.getHeight() + layoutParams.bottomMargin).setInterpolator(new AccelerateInterpolator(3));
}
@Override
public void onShow() {
// 显示动画--属性动画
toolbar.animate().translationY(0).setInterpolator(new DecelerateInterpolator(3));
fab.animate().translationY(0).setInterpolator(new DecelerateInterpolator(3));
}
}
方案2:自定义FloatingActionButton的Behavior实现
继承FloatingActionButton的Behavior:
public class FabBehavior extends FloatingActionButton.Behavior {
private boolean visible = true;//是否可见
//实例化CoordinatorLayout.LayoutParams时反射生成Behavior实例,这就是为什么自定义behavior需要重写如下的构造函数
public FabBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child, View directTargetChild, View target, int nestedScrollAxes) {
// 当观察的View(RecyclerView)发生滑动的开始的时候回调的
//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);
// 当观察的view滑动的时候回调的
//根据情况执行动画
if (dyConsumed > 0 && visible) {
//show
visible = false;
onHide(child);
} else if (dyConsumed < 0) {
//hide
visible = true;
onShow(child);
}
}
public void onHide(FloatingActionButton fab) {
// 隐藏动画--属性动画
// toolbar.animate().translationY(-toolbar.getHeight()).setInterpolator(new AccelerateInterpolator(3));
CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) fab.getLayoutParams();
// fab.animate().translationY(fab.getHeight()+layoutParams.bottomMargin).setInterpolator(new AccelerateInterpolator(3));
//FAB 缩小
ViewCompat.animate(fab).scaleX(0f).scaleY(0f).start();
}
public void onShow(FloatingActionButton fab) {
// 显示动画--属性动画
// toolbar.animate().translationY(0).setInterpolator(new DecelerateInterpolator(3));
CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) fab.getLayoutParams();
// fab.animate().translationY(0).setInterpolator(new DecelerateInterpolator(3));
//FAB放大
ViewCompat.animate(fab).scaleX(1f).scaleY(1f).start();
}
}
构造方法必须重写,重写onStartNestedScroll返回判断哪个方向的滑动,重写onNestedScroll进行相应的逻辑处理(FloatingActionButton的属性动画显示与隐藏)。
最后在布局文件中使用CoordinatorLayout布局,并且给FloatingActionButton添加自定义的Behavior:
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false"
android:paddingTop="?attr/actionBarSize"
/>
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:title="Fab动画"
app:titleTextColor="#fff"/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="58dp"
android:layout_height="58dp"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:src="@drawable/ic_favorite_outline_white_24dp"
app:layout_behavior="com.nan.advancedui.fab.anim.behavior.FabBehavior"
/>
</android.support.design.widget.CoordinatorLayout>
CoordinatorLayout
CoordinatorLayout是一个继承于ViewGroup的布局容器。CoordinatorLayout监听滑动子控件的滑动通过Behavior反馈到其他子控件并执行一些动画。简单来说,就是通过协调并调度里面的子控件或者布局来实现触摸(一般是指滑动)产生一些相关的动画效果。
其中,view的Behavior是通信的桥梁,我们可以通过设置view的Behavior来实现触摸的动画调度。
注意:滑动控件指的是:RecyclerView/NestedScrollView/ViewPager,意味着ListView、ScrollView不行。
详细使用请参考 《Material Design系列探究之LinearLayoutCompat》
MaterialDesign动画
1.Touch Feedback(触摸反馈)
5.0+的手机是自带的。
通过给控件设置background的属性值即可实现:
<Button
android:id="@+id/btn_test"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:background="?attr/selectableItemBackgroundBorderless"
android:text="测试"/>
其中,selectableItemBackground是有边界的水波纹效果,selectableItemBackgroundBorderless是没有边界的水波纹效果。
可以修改背景颜色和水波纹的颜色,并且最好使用AppcompatActivity:
<item name="colorControlHighlight">@color/colorPrimary_pink</item>
<item name="colorButtonNormal">@color/material_blue_grey_800</item>
如果想改变个别控件的颜色的话,可以通过在外面再嵌套一层布局实现。
2.Reveal Effect(揭露效果)
例子:Activity的揭露出现的效果。主要使用ViewAnimationUtil工具类实现:
//圆形水波纹揭露效果
ViewAnimationUtils.createCircularReveal(
view, //作用在哪个View上面
centerX, centerY, //扩散的中心点
startRadius, //开始扩散初始半径
endRadius)//扩散结束半径
其中,扩散的半径通过勾股定理进行计算,例如:
(float) Math.hypot(view.getWidth() / 2, view.getHeight() / 2)
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
view_root = (LinearLayoutCompat) findViewById(R.id.llc_test);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Animator animator = ViewAnimationUtils.createCircularReveal(view_root, view_root.getWidth() / 2, view_root.getHeight() / 2, 0f, (float) Math.hypot(view_root.getWidth() / 2, view_root.getHeight() / 2));
animator.setDuration(1000);
animator.setInterpolator(new AccelerateInterpolator());
animator.start();
}
}
因为动画播放是依附在window上面的,而在Activity onCreate方法中调用时Window还未初始化完毕,因此需要在onWindowFocusChanged中执行动画。
3.Activity transition(Activity转场动画效果)
两个Activity进行跳转的时候,转场动画。以前我们是通过overridePendingTransition方法实现。
主要使用ActivityOptions类。只支持API21以上的版本。版本判断会比较麻烦,谷歌很贴心 设计了一个兼容类:ActivityOptionsCompat(v4包中),但是此类在低版本上面并没有转场动画效果,只是解决了我们手动去判断版本的问题而已。
使用转换动画前提:需要给两个Activity都设置如下,让其允许使用转场动画。
//方法一:
getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS);
//方法二:
修改主题:<item name="android:windowContentTransitions">true</item>
转场动画可以分为两大类:共享元素转换和普通的转换。
1)共享元素转换
单个元素:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation(MDAnimActivity.this, iv_test, "test");
Intent intent = new Intent(MDAnimActivity.this, MDAnimSceneTransitionActivity.class);
startActivity(intent, options.toBundle());
}
多个元素同时转换:
ActivityOptionsCompat optionsCompat = ActivityOptionsCompat
.makeSceneTransitionAnimation(this, Pair.create((View)iv1, "iv1"),Pair.create((View)bt, "bt"));
Intent intent = new Intent(this, SecondActivity.class);
startActivity(intent, optionsCompat.toBundle());
页面返回的时候系统自动实现了,请看FragmentActivity的onBackPressed方法:
@Override
public void onBackPressed() {
if (!mFragments.getSupportFragmentManager().popBackStackImmediate()) {
super.onBackPressed();
}
}
2.非共享元素的转换
只有API 21才有下面自带效果,因此使用的时候需要判断版本号。
三种系统带的:滑动效果(Slide)、展开效果Explode、渐变显示隐藏效果Fade。下面以Fade为例子介绍:
//最好两个Activity都设置一些,效果会比较好看
Fade fade = new Fade();
fade.setDuration(1000);
getWindow().setExitTransition(fade);//出去的动画
getWindow().setEnterTransition(fade);//进来的动画
//如果有共享元素,可以设置共享元素,那么它就会按照共享元素动画执行,其他的子view就会按照Fade动画执行。
ActivityOptionsCompat optionsCompat = ActivityOptionsCompat.makeSceneTransitionAnimation(MainActivity.this);
Intent intent = new Intent(this, SecondActivity.class);
startActivity(intent, optionsCompat.toBundle());