前言:Google推出Material Design已经很久了,我在很久之前就开始使用Material Design风格编写APP,感觉还是挺好看的,风格比较统一。今天特意写一个简单的Demo来介绍一下Material Design(简称MD)的基本使用,由于Demo很简单,编码有点随意,大佬勿喷!
首先上两张Demo效果图:
首先介绍第一个控件就是DrawerLayout,是Material Design最基础的控件,滑动菜单就是将一些菜单选项隐藏起来,而不是放在主屏幕上,然后可以通过滑动的方式将菜单显示出来。DrawerLayout的用法很简单,首先它是一个布局,允许在布局中放入两个直接子控件,第一个控件是主屏幕中显示的内容,第二个则是隐藏的控件通过滑动来显示内容,布局文件如下:
运行效果如下图所示:
从效果图可以发现,这样看起来不是很明显可以让别人感受到你的应用有隐藏的滑动菜单,所以,我们可以添加一个按钮来提示用户有隐藏的菜单,当点击按钮的时候弹出隐藏菜单来引导用户使用,代码实现如下所示:
public class MainActivity extends AppCompatActivity {
@BindView(R.id.dl_activity_main)
DrawerLayout drawerLayout;
private Unbinder unbinder;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//获取ActionBar对象
ActionBar actionBar = getSupportActionBar();
if(actionBar != null){
//设置按钮
actionBar.setDisplayHomeAsUpEnabled(true);
//更换按钮图标(默认是返回的箭头)
actionBar.setHomeAsUpIndicator(R.drawable.ic_menu);
}
unbinder = ButterKnife.bind(this);
}
//重写该方法,箭头菜单按钮的点击事件
@Override
public boolean onOptionsItemSelected(MenuItem item) {
//该按钮的Id已经在android的R文件中定义,为 :android.R.id.home
if(item.getItemId() == android.R.id.home){
//弹出DrawerLayout菜单,参数为弹出的方式
drawerLayout.openDrawer(GravityCompat.START);
}
return super.onOptionsItemSelected(item);
}
@Override
protected void onDestroy() {
super.onDestroy();
unbinder.unbind();
}
}
运行效果如下图所示:
现在的效果就是,点击按钮,弹出隐藏菜单。
从上面的运行效果中我们可以看出,当我们的滑动菜单弹出的时候,ActionBar是没有任何反应的,依然挡住了顶部的部分显示空间,因此ActionBar十分的不灵活,故使用Toolbar来替换掉ActionBar,这也是Google希望的,Toolbar不仅拥有ActionBar的全部功能,而且还能更好的支持Material Design效果,现在我们更改一下布局文件,替换掉系统的ActionBar,在此之前,我们需要更改应用的默认主题,将主题指定为NoActionBar的,即去掉ActionBar,步骤:1.打开AndroidManifest.xml文件,2.打开AppTheme主题资源文件,3.修改AppTheme文件,文件默认配置如下:
修改为以下格式,指定为NoActionBar,参数解释:colorPrimary标题栏颜色; colorPrimaryDark顶部状态栏颜色;colorAccent界面中其他元素的颜色,该颜色一般与上面的两种颜色形成高反差。
现在ActionBar已经被隐藏了,需要我们手动在布局文件中添加Toolbar,故现在的布局文件为:
运行效果如下图所示:
现在,由于Toolbar过于单调,所以我们现在为Toolbar添加一些功能按钮,简单的分为三个步骤来实现为Toolbar添加菜单按钮,第一步:在res文件夹下创建一个menu的文件夹并在menu文件夹下创建toolbar.xml文件,在toolbar.xml文件中配置按钮图标文字显示方式等信息,配置如下所示:
第二步,将配置好的xml文件加载到toolbar,同样很简单,在Activity中重写onCreateOptionsMenu方法,然后将布局文件加载到menu中即可,代码如下:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
//获取menu的注入器(Inflater)并将我们配置的toolbar文件加载到menu中即可
getMenuInflater().inflate(R.menu.toolbar, menu);
return true;
}
第三步,为菜单中的按钮添加点击事件,在Activity中重写onOptionsItemSelected方法即可,通过ID判断哪个按钮被点击:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
//弹出左侧滑动菜单
if(item.getItemId() == android.R.id.home){
drawerLayout.openDrawer(GravityCompat.START);
//给指定按钮添加点击事件
}else if(item.getItemId() == R.id.backup){
Toast.makeText(MainActivity.this, "正在上传中...", Toast.LENGTH_SHORT).show();
}
return super.onOptionsItemSelected(item);
}
至此,我们就完成了Toolbar的菜单按钮的添加,完成后的效果如下图所示:
目前我们已经可以实现简单的滑动菜单效果,但是效果还比较的简陋,不过我们可以通过Google提供的NavigationView控件来为我们的滑动菜单的菜单布局,将会达到更好的效果,不过在使用NavigationView之前,需要我们将NavigationView控件所在的库添加进来,在app/build.gradle文件中添加如下内容:
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:26.+'
compile 'com.android.support.constraint:constraint-layout:1.0.2'
testCompile 'junit:junit:4.12'
// butterknife
compile 'com.jakewharton:butterknife:8.8.1'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
// circleimageview依赖库(图片圆形化开源项目)
compile 'de.hdodenhof:circleimageview:2.1.0'
// NavigationView依赖库
compile 'com.android.support:design:24.2.1'
}
添加完依赖以后,我们就可以动手了,一般MD风格的滑动菜单栏分为两个部分,上面一部分是头下面一部分为功能菜单,因此我们需要定义两个布局文件在配置头和菜单,我们先来看看如何定义菜单,很简单,在res->menu下新建一个nav_menu.xml文件,在xml文件中定义我们自己所需的菜单项即可,定义如下:
接下来我们来看看头的布局文件,一般头布局中都是一个头像加上一些简单的信息,在此处我们就用头像、姓名和邮箱来布局,布局文件如下所示:
android:layout_height="180dp"
android:background="@color/colorPrimary"
android:padding="10dp">
可以看到,菜单和头布局都非常的简单,准备好菜单和头的布局文件以后我们就需要更改之前的布局文件了,将之前的简单的TextView替换为NavigationView,更改后的布局文件如下:
到这儿,基本的效果已经完成了,但是还需要给菜单设置一下监听事件,所以去到Activity文件中进行修改,Activity中修改如下所示:
@BindView(R.id.dl_activity_main)
DrawerLayout drawerLayout;
@BindView(R.id.nav_activity_main)
NavigationView mNavigationView;
……
//设置默认选中的菜单
mNavigationView.setCheckedItem(R.id.friends);
//给菜单设置监听
mNavigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected( MenuItem item) {
//关闭弹出菜单
drawerLayout.closeDrawers();
return true;
}
});
接下来看看运行效果,如下图所示:
该控件是Design Support库中提供的,可以让我们轻松实现悬浮按钮的效果,该控件的使用非常简单,只需要在布局中添加FloatingActionButton标签即可,xml布局文件如下所示(省略掉未更改的部分):
FloatingActionButton的点击事件的使用和普通的Button是相同的,我们在FloatingActionButton的点击事件中使用Sanckbar来提示用户,Sanckbar的使用和Toast类似,只是除了提示以外,Sanckbar还可以执行一些简单的逻辑,Activity代码如下:
@BindView(R.id.fab_activity_main)
FloatingActionButton mActionButton;
mActionButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//setAction来指定操作 第一个参数是操作名称 第二个参数是点击事件的处理逻辑
Snackbar.make(view, "删除数据", Snackbar.LENGTH_SHORT).setAction("确定",
new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(MainActivity.this, "删除成功", Toast.LENGTH_SHORT).show();
}
}).show();
}
});
接下来看看运行效果,如下图所示:
从上面第四步的截图我们发现弹出的Snackbar遮挡住了我们的浮动按钮,这是一种很不友好的用户体验,所以,Google为我们准备了CoordinatorLayout这个控件来解决这个问题,同样是在Design支持库中,我们只需要将滑动菜单的默认显示页的最外层布局指定为CoordinatorLayout即可,修改后的xml文件如下所示:
那么,为什么CoordinatorLayout可以实现让浮动按钮向上移动的效果呢?那是因为该控件会监听它里面所有控件的状态,接下来看看使用CoordinatorLayout后的运行效果,如下图所示:
到这里,基本可以得到一个通用的结论,那就是在Material Design中,外层的布局方式基本为最外层为DrawerLayout的滑动布局,滑动布局内部的主界面为CoordinatorLayout布局,而滑动的隐藏布局为NavigationView,而用户的主页布局只需在CoordinatorLayout添加布局即可,而隐藏界面的布局则通过menu文件和layout文件来指定给NavigationView即可,基本框架如下xml文件所示:
……
CardView是实现卡片式布局的重要控件,由appcompat-v7库提供,需要在build.gradle文件中声明库的依赖:compile ‘com.android.support:cardview-v7:26.+’, CardView的使用也很简单,我们先来看看简单的使用方法,如下xml布局文件所示:
从布局文件可以看到,CardView的使用很简单,里面就是一个TextView,CardView的特有属性为app:cardCornerRadius和app:cardElevation,我们可以在CardView中嵌套其他ViewGroup来进行更为复杂的布局,接下来将CardView添加到我们的CoordinatorLayout中,来看看效果,修改后的xml文件如下所示:
接下来看看运行效果,如下图所示:
是不是感觉有什么不对,是的,cardview遮挡住了我们的Toolbar,这很正常,因为CoordinatorLayout是FrameLayout的加强版,所以布局方式和FrameLayout是相同的,所以都是一层覆盖一层且是从左上角开始的,故Toolbar被遮挡住了,当然,肯定是有解决方法的,请往下看。
这个控件就是为了解决上面的遮挡问题的,实际上AppBarLayout是一个垂直方向的LinearLayout,但是,在它的内部还做了一些滚动事件的封装,且用了Material Design的设计理念,接下来看看这个控件如何使用,很简单,两个步骤即可,第一步,在我们的Toolbar控件外层嵌套AppBarLayout,修改后的xml布局文件如下所示:
第一步就完成了,仅仅是在Toolbar的外部添加一个AppBarLayout而已,第二步在与Toolbar处于同级的布局文件中添加“app:layout_behavior="@string/appbar_scrolling_view_behavior”该属性即可,当前布局文件中与Toolbar同级的控件为CardView,所以我们只需在CardView中添加该属性即可,修改后的CardView布局如下所示:
经过上面的两个步骤,ToolBar就不会再被后面同级的控件遮挡住,app:layout_behavior="@string/
appbar_scrolling_view_behavior属性由支持库提供。同时我们可以给Toolbar设置app:layout_scrollFlags
="scroll|enterAlways|snap">属性,该属性是在Toolbar属于AppBarLayout的子控件时才会生效,里面的三个参数scroll代表当AppBarLayout下面的控件向上滚动(如:使用RecylerView)的时候,Toolbar会跟着一起向上滚动并隐藏;enterAlways则是向下滚动会随着滚动并重新显示;snap则当Toolbar还没完全隐藏或显示的时候,会根据当前滚动距离,自动选择隐藏还是显示。
接下来我们将CardView替换成为RecylerView来模拟这种效果,并将RecylerView的布局指定为CardView,这是修改后的activity_main.xml的布局文件:
然后我们来编写一下 RecyclerView的布局文件,外层使用CardView,里面嵌套一个TextView,文件名称为layout_item.xml布局文件如下所示:
现在,我们去到Activity中给RecyclerView添加适配器,Activity中和RecyclerView相关代码如下:
List lists = new ArrayList<>();
for(int i = 0; i < 20; i++){
lists.add("模拟数据"+i);
}
//创建适配器 并传入模拟的数据
MyAdapter adapter = new MyAdapter(lists);
//设置显示格式 2列
GridLayoutManager layoutParams = new GridLayoutManager(this, 2);
//将显示格式传给mRecyclerView
mRecyclerView.setLayoutManager(layoutParams);
//设置适配器
mRecyclerView.setAdapter(adapter);
MyAdapter.java文件如下:
public class MyAdapter extends RecyclerView.Adapter{
private List mLists;
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_item,
parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.mTextView.setText(mLists.get(position));
}
@Override
public int getItemCount() {
return mLists.size();
}
static class ViewHolder extends RecyclerView.ViewHolder{
private TextView mTextView;
public ViewHolder(View itemView) {
super(itemView);
mTextView = itemView.findViewById(R.id.tv_layout_item);
}
}
public MyAdapter(List lists){
mLists = lists;
}
}
接下来看看运行效果,如下图所示:
到目前为止,基本的数据已经可以正常展示了,现在,我们将模拟的数据换成新闻数据,然后用Recycler
View来展示新闻,然后实现下拉刷新新闻数据,说到下拉刷新,Google也为我们封装了一个适配MD风格的控件,就是SwipeRefreshLayout,SwipeRefreshLayout的使用很简单,我们只需在之前的RecyclerView控件的外层加上SwipeRefreshLayout就行了,activity_main.xml布局文件修改如下:
现在布局文件已经修改完毕,我们去Activity中添加获取新闻的逻辑和下拉刷新的逻辑,新闻是去知乎拉取的,用了第三方框架OkHttp、Glide和Gson,现在我们先来看看获取数据的逻辑 ( 由于是Demo,代码和随意,未任何优化,实际开发中不建议这样写哦~):
public void initData(){
new Thread() {
@Override
public void run() {
super.run();
OkHttpClient client = new OkHttpClient();
//新闻url
String url = "http://news-at.zhihu.com/api/4/news/latest";
Request request = new Request.Builder().url(url).build();
Call call = client.newCall(request);
//异步获取数据
call.enqueue(new Callback() {
@Override
//获取失败的回调
public void onFailure(Call call, IOException e) {
//关闭刷新提示
mRefreshLayout.setRefreshing(false);
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(MainActivity.this, "无网络", Toast.LENGTH_SHORT).show();
}
});
}
@Override
public void onResponse(Call call, Response response) throws IOException {
try {
String string = response.body().string();
Log.i("felix", string);
//Gson解析获取的数据
Gson gson = new Gson();
ResultBean resultBean = gson.fromJson(string, ResultBean.class);
//重置之前的List引用
lists = null;
//获取Gson解析后的数据
lists = resultBean.getStories();
Log.i("felix", lists.size()+"");
runOnUiThread(new Runnable() {
@Override
public void run() {
//修改List引用对象
adapter.updateLists(lists);
//通知刷新
adapter.notifyDataSetChanged();
//关闭刷新提示
mRefreshLayout.setRefreshing(false);
}
});
} catch (IOException e) {
e.printStackTrace();
mRefreshLayout.setRefreshing(false);
Toast.makeText(MainActivity.this, "无网络", Toast.LENGTH_SHORT).show();
}
}
});
}
}.start();
}
接下来看看用来进行Gson解析的ResultBean的定义:
public class ResultBean {
private String date;//对应json数据中的date键
private List stories; //对应json数据中的stories数组
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
public List getStories() {
return stories;
}
public void setStories(List stories) {
this.stories = stories;
}
public static class StoriesBean{
private List images;
private String type;
private String id;
private String title;
public List getImages() {
return images;
}
public void setImages(List images) {
this.images = images;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
}
Gson小技巧:1、看到JSON结构里面有{ }你就定义一个类,看到[ ]你就定义一个List或数组即可,最后只剩下最简单的如String、int等基本类型直接定义就好。2、内部嵌套的类,请使用public static class className { }。3、类内部的属性名,必须与JSON串里面的Key名称保持一致。
接下来是实现下拉刷新的逻辑,如下所示,在Activity中添加如下代码:
@BindView(R.id.srl_activity_main)
SwipeRefreshLayout mRefreshLayout;
//给SwipeRefreshLayout设置加载进度条的颜色和标题栏相同
mRefreshLayout.setColorSchemeResources(R.color.colorPrimary);
//设置下拉属性监听器
mRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
//调用initData方法获取数据
initData();
}
});
接下来看看运行效果,如下图所示:
新闻展示界面我们已经做好了,现在就来进行新闻详细显示页的布局,用到的控件基本都是前面所学过的控件,不过这里要新学习一个控件就是CollapsingToolbarLayout,该控件可以实现一个可折叠式标题栏的效果,根据控件名我们就能猜出,该控件是作用于Toolbar之上的,而且该控件也是由Design支持库提供的,并且该控件只能嵌套在AppBarLayout中使用,即xml布局文件应该是这种结构:
即,最外层为CoordinatorLayout(防止遮挡,所有的非隐藏的MD风格布局都使用这个控件作为最外层),CoordinatorLayout的下一层为AppBarLayout(也是防止遮挡,不过是防止Toolbar被遮挡,因为CoordinatorLayout是基于FrameLayout的),然后就是我们的可折叠式标题栏CollapsingToolbarLayout,需要注意的是,我们来看看我们给CollapsingToolbarLayout设定的属性theme为深色系,因为这样文字才会显示为白色,app:contentScrim="@color/colorPrimary" 主体颜色和标题栏颜色相同,最后一个属性不是第一次见了,滑动时的反应,scroll表示折叠式标题栏会随着下面的可滚动的内容的滚动而滚动,exitUntilCollapsed 则表示随着滚动CollapsingToolbarLayout折叠完成之后把Toolbar留在顶部。
接下来看看运行效果,我已将应用名改为“简新闻”运行结果如下图所示,第一张为我们点击新闻进去的界面,第二张为向上滑动折叠式标题栏折叠以后的效果:
现在,又发现了新的问题,就是第一张截图的状态栏特别扎眼,所以我们需要修改一下属性来使状态栏透明化,要让状态栏透明化很简单,给CollapsingToolbarLayout及其CollapsingToolbarLayout外层的所有控件加上这个属性就可以使状态栏透明化“android:fitsSystemWindows="true"”然而加上以后发现好像并没有效果,这是因为我们还差一步,需要在主题文件中给状态栏的颜色指定为透明色,现在我们就专门为这个页面编写一个主题文件,在res下新建values-v21文件夹,在文件夹下面新建名为styles.xml文件,在里面编写如下的主题文件信息:
<!-- name:主题名, parent:继承自那个主题 -->
然后我们去AndroidManifast.xml修改新闻详情页的主题为InfoActivityTheme即可,修改后的显示截图如下所示
现在,可折叠式标题栏也完成了,就差显示新闻详情的界面了,在这儿我们用NestedScrollView来作为显示新闻的最外层的布局,里面嵌套一个线性布局(注意:NestedScrollView布局内只能有一个布局,所以我们嵌套线性布局来进行里面的布局),里面的布局也很简单,在线性布局内嵌套CardView,在CardView的里面放一个TextView就ok了,完整的布局文件如下所示,包括上面的可折叠式标题栏的布局:
现在,我们去到InfoActivity中编写获取新闻数据的逻辑,我们在MainActivity的RecyclerView中,当我们点击相应的新闻标题的时候,就把新闻的ID通过Intent传递到InfoActivity中,在InfoActivity中拿到对应的ID去知乎服务器获取详细的新闻数据,然后Gson解析,将解析后的数据展示到相应的位置即可,需要注意的是,服务器返回的详细内容是html格式的,我们需要使用TextView的图文混排功能来展示,完整的InfoActivity代码如下:
public class InfoActivity extends AppCompatActivity {
@BindView(R.id.tv_activity_info)
TextView mTextView;
@BindView(R.id.toolbar_activity_info)
Toolbar mToolbar;
@BindView(R.id.img_activity_info)
ImageView mImageView;
@BindView(R.id.cool_toolbar)
CollapsingToolbarLayout mCTL;
@BindView(R.id.title_activity_info)
TextView mTitle;
ActionBar actionBar;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_info);
ButterKnife.bind(this);
setSupportActionBar(mToolbar);
mTextView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
MyLog.i("textview");
Log.i("felix","1111");
}
});
mTextView.setMovementMethod(LinkMovementMethod.getInstance());
actionBar = getSupportActionBar();
if(actionBar != null){
actionBar.setDisplayHomeAsUpEnabled(true);
}
//让Toolbar的标题固定在上面
mCTL.setTitleEnabled(false);
Intent intent = getIntent();
String id = intent.getStringExtra("id");
String url = "http://news-at.zhihu.com/api/4/news/"+id;
OkHttpClient client = new OkHttpClient();
final Request request = new Request.Builder().url(url).build();
Call call = client.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
final String string = response.body().string();
MyLog.i(string);
Gson gson = new Gson();
final ContentBean bean = gson.fromJson(string, ContentBean.class);
Log.i("felix", "当前线程ID:"+Thread.currentThread().getId());
final Spanned spanned = Html.fromHtml(bean.getBody(), new UrlImageGetter(
InfoActivity.this), null);
runOnUiThread(new Runnable() {
@Override
public void run() {
mTextView.setText(spanned);
Glide.with(InfoActivity.this).load(bean.getImage()).into(mImageView);
mTitle.setText(bean.getTitle());
}
});
}
});
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()){
case android.R.id.home:
finish();
break;
default:
}
return super.onOptionsItemSelected(item);
}
}
下面是UrlImageGetter类的实现:
public class UrlImageGetter implements Html.ImageGetter {
Context mContext;
public UrlImageGetter(Context context) {
mContext = context;
}
@Override
public Drawable getDrawable(String source) {
try {
MyLog.i(source);
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url(source).build();
Call call = client.newCall(request);
Response response = call.execute();
Bitmap bitmap = BitmapFactory.decodeStream(response.body().byteStream());
Drawable drawable = new BitmapDrawable(bitmap);
MyLog.i(drawable.getIntrinsicWidth()+"");
DrawableUtils drawableUtils = new DrawableUtils(mContext);
//调整图片大小
drawable = drawableUtils.utils(drawable);
return drawable;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
下面是DrawableUtils类的实现:
public class DrawableUtils {
Context mContext;
public DrawableUtils(Context context){
mContext = context;
}
public Drawable utils(Drawable drawable){
DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
int width = displayMetrics.widthPixels;
int height = displayMetrics.heightPixels;
int draWidth = drawable.getIntrinsicWidth();
int draHeight = drawable.getIntrinsicHeight();
int resWidth = draWidth, resHeight = draHeight;
if(draWidth < width && draHeight < height){
resWidth = (int) (draWidth * 2.5);
resHeight = (int) (draHeight * 2.5);
}else if(draWidth > width || draHeight > height){
int value = draWidth / width;
resWidth = draWidth / value;
resHeight = draHeight / value;
}
drawable.setBounds(0, 0, resWidth, resHeight);
return drawable;
}
}
下面是新闻的展示效果图:
... ...
可以收藏备用哦,如有错误请指正~