先扯两句
其实按照正常情况,这篇博客应该是在上一篇之前发的,实在是之前公司遇到了一个新需求,要创建一个背景透明的Activity做提示遮罩用,结果现有的BaseActivity布局的封装实在不支持这种情况,所以只能创建了一个不是基于BaseActivity的Activity已达成需求,虽然任务完成了,可是程序员探索的脚步不能停,于是痛定思痛之下,终于下定决心,对BaseActivity进行拆解,原本的BaseActivity保留所有的方法逻辑,而将与布局相关的方法与参数迁移到当前的BaseLayoutActivity中。
好了,闲言少叙,老规矩还是先上我的Git,然后开始正文吧。 MyBaseApplication (https://github.com/BanShouWeng/MyBaseApplication)
正文
前面翻过android开发相关——include、merge和ViewStub的布局优化中已经阐述了ViewStub相关的内容,没有看到的朋友可以去看一下,而这里,我本次封装的引用就从include替换到了ViewStub。当然,这里也有一些需要提前说明,那就是一般而言,ViewStub使用的环境还是那些不常使用到的,例如引导页之类的功能,而title的使用频率还是蛮高的。而ViewStub解析所消耗的资源与无用的include占用的资源哪个更多,以我当前的水准还无法得到一个准确的答案,所以具体是使用ViewStub还是使用include还是看大家自己的理解了。当然,如果谁有更具体的数据,也欢迎分享,在此感激不尽。
BaseActivity的底层封装
首先,这里先看一下BaseActivity的布局xml:
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.banshouweng.mybaseapplication.base.activity.BaseActivity">
<ViewStub
android:id="@+id/base_title_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/title_include" />
<FrameLayout
android:id="@+id/base_main_layout"
android:layout_width="match_parent"
android:layout_height="0dip"
android:layout_weight="1"/>
</LinearLayout>
其中只有两个控件:其一为在前面写的说过的ViewStub控件,在这里的主要作用就是封装的title布局文件;其二则是一个FrameLayout控件,其作用嘛,就是用以展示BaseActivity子类中重写getLayout()方法时传来id对应的布局文件。 实现方法,在《一个Android工程的从零开始》阶段总结与修改3-BaseActivity上(抽象处理)中,我们将BaseActivity做了抽象处理,其中使用到了一个方法在onCreate方法中调用了setBaseContentView(getLayoutId());如下图标注的位置。
而这个方法的具体实现如下。
/**
* 引用头部布局
*
* @param layoutId 布局id
*/
private void setBaseContentView(int layoutId) {
FrameLayout layout = getView(R.id.base_main_layout);
//获取布局,并在BaseActivity基础上显示
final View view = getLayoutInflater().inflate(layoutId, null);
//关闭键盘
hideKeyBoard();
//给EditText的父控件设置焦点,防止键盘自动弹出
view.setFocusable(true);
view.setFocusableInTouchMode(true);
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);
layout.addView(view, params);
}
可以看到,这里就是将传递来的子布局添加到FrameLayout中,而看过我之前博客,或者下载我封装的MyBaseApplication 的朋友会知道,当时使用的是LinearLayout而不是现在的FrameLayout。这真不是我闲的蛋疼,没事非要乱改,毕竟我没事就以懒汉自居,这是看过我博客的朋友都知道的,之所以改了名字完全是因为阿里没事非要发个什么《阿里巴巴Android开发手册》,为了生存嘛,肯定要向大公司开发规范靠拢喽。
四、UI 与布局
- 【推荐】灵活使用布局,推荐 Merge、ViewStub 来优化布局,尽可能多的减少 UI 布局层级,推荐使用 FrameLayout,LinearLayout、RelativeLayout 次之。
想必对于像我这种菜鸟而言,在使用布局的时候第一反应,还是使用的LinearLayout,等慢慢熟悉了之后忽然发现,很多情况下RelativeLayout比LinearLayout更好用,而现在又出来了一个ConstraintLayout。但是却很少有使用到FrameLayout的,或许大家在培训的时候知道,在创建Fragment的时候使用FrameLayout去占位,可一般老师也不说为什么。 之所以很少使用FrameLayout,并不是FrameLayout太难了,而是它太简单了,没办法完成我们所需求的那么多功能,而也正因为它的简单,才是我们这里使用它的原因,因为使用它可以减少不必要的资源浪费。通俗举例(但不一定完全正确):RelativeLayout的相对关系处理在这里就用不上,所以多加载了这些功能,就是浪费了资源。 其中除了加载布局就是在防止键盘弹出,当然,也不是去掉这几行代码就一定会弹出输入法,只是针对个别会自动弹出的机型做了一下统一,去掉也可以。 在子类中调用的时候,只需要重新抽象方法getLayoutId()即可:
@Override
protected int getLayoutId() {
return R.layout.activity_main;
}
如图,上面就是在activity_base.xml中自定义的title,而下面的则是activity_main.xml的布局。
title封装
这里的title就是上面ViewStub对应的布局文件,title布局文件title_include.xml如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="@dimen/title_height">
<RelativeLayout
android:id="@+id/base_bg"
android:layout_width="match_parent"
android:layout_height="@dimen/title_height"
android:background="@color/blue">
<ImageView
android:id="@+id/base_back"
android:layout_width="50dp"
android:layout_height="50dp"
android:padding="@dimen/size_13"
android:src="@mipmap/back"
android:tint="@android:color/white" />
<ImageView
android:id="@+id/base_close"
android:layout_width="50dp"
android:layout_height="50dp"
android:padding="@dimen/size_13"
android:src="@mipmap/close"
android:visibility="gone"
android:layout_toRightOf="@+id/base_back"
android:tint="@android:color/white" />
<TextView
android:id="@+id/base_title"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:gravity="center"
android:text="@string/title"
android:textColor="@android:color/white"
android:textSize="@dimen/size_20" />
<ImageView
android:id="@+id/base_right_icon2"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_toLeftOf="@+id/base_right_icon1"
android:contentDescription="@string/second_function_key"
android:padding="@dimen/size_13"
android:src="@mipmap/add"
android:tint="@android:color/white"
android:visibility="gone" />
<ImageView
android:id="@+id/base_right_icon1"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_alignParentRight="true"
android:contentDescription="@string/first_function_key"
android:padding="@dimen/size_13"
android:src="@mipmap/more"
android:tint="@android:color/white"
android:visibility="gone" />
<TextView
android:id="@+id/base_right_text"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_alignParentRight="true"
android:gravity="center"
android:text="@string/make_sure"
android:textColor="@android:color/white"
android:textSize="@dimen/size_17"
android:visibility="gone" />
</RelativeLayout>
</RelativeLayout>
ViewStub的运用
开篇中已经提到了,我前面写的写的布局优化的内容,而关于ViewStub的运用,实际在其中已经阐述了,这里之所以还要拿出来说一下,不是有新内容,也不是水字数(毕竟也不是写网文),只是单纯的怕大家还需要去前面写的博客查代码比较麻烦而已,当然这里就不多废话了,直接把代码贴在这里就好了。
/**
* Title ViewStub
*/
private ViewStub titleStub;
/**
* 控件初始化
*/
protected void initBaseView() {
if (titleStub == null) {
titleStub = getView(R.id.base_title_layout);
titleStub.inflate();
}
}
关闭按钮
或许有人看到过我《一个Android工程的从零开始》-6、base(五) BaseFragment封装中,对于title的封装,其实若对比下来的话,实际上还是那些内容,只是比当初封装的时候多出了一个控件而已,就是:
<ImageView
android:id="@+id/base_close"
android:layout_width="50dp"
android:layout_height="50dp"
android:padding="@dimen/size_13"
android:src="@mipmap/close"
android:visibility="gone"
android:layout_toRightOf="@+id/base_back"
android:tint="@android:color/white" />
而这个控件就是下图中的“X”,在返回键的右侧,具体功能就是关闭:
关闭按钮的作用
可能会有人问,既然已经有了返回键,为什么还要添加一个关闭键,这样是不是有些多此一举。当然,在大多数情况下,这个关闭确实是用不到的,所以之前我也没设置这个按钮,可随着开发的项目越来越复杂,功能逐渐完善,就会发现,其实这个“X”的存在还是很有价值的,下面我就举两个例子:
1.在多级菜单一次关闭的情况下,我在京东上查笔记本电脑,目录如下:电脑、办公>电脑整机>笔记本>小米(MI) >小米Air,结果发现买不起,想要买一箱啤酒借酒消愁的时候,还需要一级一级返回到“电脑、办公”的上一级重新搜索(别说可以直接搜寻想要购买的商品,这里只是在说明目录结构复杂时的情况),而如果有了这个“关闭键”则可以直接关闭掉这一系列页面,直接跳转到首页去进行后续操作。
2.当使用WebView访问的时候,一般我们会在用户点击返回键(自定义返回键、物理返回键、或者虚拟返回键)的时候做如下代码处理:
if(webView.canGoBack()){
webView.goBack();
return true;
}else{
finish();
return false;
}
其目的就是当我们在H5页面中访问到如下目录时:电脑、办公>电脑整机>笔记本>小米(MI) >小米Air时,想要再查看其它小米产品,就可以直接点击返回键,而不是返回键直接退出了当前的WebView页面,不然如果我是用户,退出后做的第一件事就是把这个APP卸载了。可这样就会造成一个问题,当用户想退出WebView执行APP其他操作的时候,却需要将之前自己访问的页面都返回一遍,这样的用户体验会不会再次有一批想要卸载APP的呢?而这个时候,在我们无法智能到读取用户真实想法的情况下,就只能采取设置两个按钮的方法,一个是返回上一级,一个是关闭当前WebView页。
关闭按钮的实现
根据上面的分析,我们知道了关闭按钮常用的两种情况,而若说实现起来,也是两种情况。第一种情况下,点击关闭按钮的时候进行的是批量关闭Activity的操作;而第二种情况则只是简单的关闭当前WebView所在的Activity而已。
1.批量关闭Activity
/**
* 关闭按钮
*/
private ImageView baseClose;
/**
* 设置关闭部分页面
*
* @param targetActivity 关闭后所要返回的Activity对应的class
*/
public void setBaseClose(final Class<?> targetActivity) {
initBaseView();
baseClose = getView(R.id.base_close);
baseClose.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
backTo(targetActivity);
}
});
}
首先initBaseView()方法自然是解析的title布局,随后是获取baseClose控件,并添点击事件。其中的backTo(targetActivity),是批量关闭Activity的方法,back的具体实现方法会在下一篇博客中详细说明。
2.关闭当前的Activity
/**
* 设置关闭点击事件
*/
public void setBaseClose() {
initBaseView();
baseClose = getView(R.id.base_close);
baseClose.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
finish();
}
});
}
看到这个方法打击一定会发现,这不就是上面的方法吗,若说区别就是一个传递了所要返回的Activity对应的class这个参数,并调用了backTo(targetActivity),另一个是直接finish的。或许可以做个判断如果传入的targetActivity为null则调用finish岂不是更好,何必重载一遍方法。以上的方法自然可以的,至于是否使用则需要看我们所开发的项目有多复杂。 不过从我封装BaseActivity的角度出发,关闭当前的Activity使用的却不是上面的这个方法,而是重载了下面的三个方法:
1.在使用的时候,考虑都方法或者控件的复用效果,虽然我们的设计初衷是“关闭按钮”,可是真正的开发过程中,谁也不知道产品会开个什么脑洞。而且即便不是产品故意为难我们,有一些页面在关闭的时候,难免要求弹窗提示用户“是否放弃当前页面的操作”,所以点击事件就不是一成不变的了,所以这里添加了一个boolean值,去做这个判断。也就是说,当我们需要重置点击事件的时候传入true即可,而不需要重置的时候,则传入false,就会调用finish方法了。
/**
* 设置关闭点击事件
*
* @param isResetClose 是否重置关闭按钮点击事件
*/
public void setBaseClose(boolean isResetClose) {
initBaseView();
baseClose = getView(R.id.base_close);
if (isResetClose) {
baseClose.setOnClickListener(this);
} else {
baseClose.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
finish();
}
});
}
}
baseClose.setOnClickListener(this);是因为BaseActivity是实现了View.OnClickListener的抽象类 2.当前的重载方法同样是从重载的角度出发的,因为点击事件如果被重新定义的话,图标也可能会有所改变,这里只需要将对应图标的资源id传来即可。当然,既然重新设置图标,我就当你肯定会重新定义点击事件了,大家如果想完善个只换图标,点击事件不变的,可以自行添加(如果整个项目都统一换,请直接替换默认图片资源)。
/**
* 设置关闭点击事件
*
* @param resId 重置关闭按钮图标
*/
public void setBaseClose(int resId) {
initBaseView();
baseClose = getView(R.id.base_close);
baseClose.setImageResource(resId);
baseClose.setOnClickListener(this);
}
3.不过当下单纯的使用关闭按钮的情况比较多,所以这里添加了一个重载方法,一般而言,我们直接调用这个方法即可,毕竟对于我这种懒汉来说,多写个false还是很麻烦的。
/**
* 设置关闭点击事件
*/
public void setBaseClose() {
setBaseClose(false);
}
或许有人会问,为什么批量关闭Activity的方法就没有这么多情况分析,难倒批量关闭Activity的时候就不存在自定义吗?答案自然是有的。至于为什么没有分析,难倒调用setBaseClose(true);不能重新定义批量关闭Activity的点击事件吗?
其实上述的方法就已经很全面了,不过下面这段也不算是狗尾续貂吧,只能说是被产品摧残多了以后的一种自觉的反应:
/**
* 隐藏关闭按钮
*/
public void hideBaseClose() {
if (null != baseClose){
baseClose.setVisibility(View.GONE);
} else {
Logger.e(getName(),"baseClose is not exist");
}
}
/**
* 显示关闭按钮
*/
public void showBaseClose() {
if (null != baseClose){
baseClose.setVisibility(View.VISIBLE);
} else {
Logger.e(getName(),"baseClose is not exist");
}
}
上面两个方法分别是关闭按钮的隐藏和显示方法,是在为了动态处理关闭按钮而添加的,一般情况下也使用不到,除非是产品说:当WebView中,刚进入H5页面时不需要显示关闭按钮,当经过操作,重新返回到首页时,由于返回键与关闭键功能重复,需要将关闭键隐藏的时候。至于为什么在baseClose不存在的时候,只是报错(Logger是我封装的Log)而没做其他法处理,实在是控件为空的原因,还需要特殊提醒吗?
title设置
之所以把上面这张图又贴出来,主要是想要说明一下,这里的title不是上图title的整体,主要说的是中间“MyTitle”位置的文本设置,当然,这里不过是有一个TextView而已,设置的方法很简单,不过是调用一个setText罢了,不过在调用setText的时候,大家有没有遇到过如下图的错误:
翻译过来就是我们想要查找的资源不存在,而这个资源是什么呢?
private int count = 0;
@SuppressLint("SetTextI18n")
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.merge_btn:
view.setText(count++);
break;
}
}
也就是说,当我们设置的参数为数字的时候,这个数字会自动被当做资源id,所以说,在设置数字的时候,我们都需要将数字转换为字符串才能正常显示。不过,正是因为这个,所以上面我设置的“MyTitle”的时候,可以有两种方式:
// 字符串
title.setText("MyTitle");
<resources>
<string name="title">MyTitle</string>
...
</resources>
<--资源id 在res-->values-->strings.xml文件中-->
// 资源id
title.setText(R.string.title);
所以这里封装的setTitle方法也是有两部分:
/**
* 设置标题
*
* @param title 标题的文本
*/
public void setTitle(String title) {
initBaseView();
((TextView) getView(R.id.base_title)).setText(title);
}
/**
* 设置标题
*
* @param titleId 标题的文本
*/
public void setTitle(int titleId) {
initBaseView();
((TextView) getView(R.id.base_title)).setText(titleId);
}
返回键
返回键基本上是title的标配了,使用不到返回键的地方基本也就是首页罢了(启动页、引导页、登录页等页面不需要返回键的同时,也不需要title),所以这里返回键的初始化的部分就不单独列举方法了,而是与上面的setTitle融合在了一起,添加一个boolean值,用于设置是否需要展示返回键。
/**
* 设置标题
*
* @param title 标题的文本
* @param showBack 是否显示返回键
*/
public void setTitle(String title, boolean showBack) {
initBaseView();
((TextView) getView(R.id.base_title)).setText(title);
if (showBack) {
baseBack = getView(R.id.base_back);
baseBack.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
}
}
/**
* 设置标题
*
* @param titleId 标题的文本
* @param showBack 是否显示返回键(一般只用于首页)
*/
public void setTitle(int titleId, boolean showBack) {
initBaseView();
((TextView) getView(R.id.base_title)).setText(titleId);
if (showBack) {
baseBack = getView(R.id.base_back);
baseBack.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
}
}
当然,由于不显示返回键的情况过少,所以这里在外层又封装了两个方法,至于原因,自然是多写个true麻烦喽:
/**
* 设置标题
*
* @param title 标题的文本
*/
public void setTitle(String title) {
setTitle(title, true);
}
/**
* 设置标题
*
* @param titleId 标题的文本
*/
public void setTitle(int titleId) {
setTitle(titleId, true);
}
除此之外,返回键的图标也可能会做出修改,所以也定义了修改的方法:
/**
* 重置返回点击事件
*
* @param resId 重置返回按钮图标
*/
public void setBaseBack(int resId) {
initBaseView();
baseBack = getView(R.id.base_back);
baseBack.setImageResource(resId);
baseBack.setOnClickListener(this);
}
再往复杂了考虑,就是在使用的时候,我们之前使用的时候是直接返回,可当满足一定条件,会重置返回键,这里也添加了重置的方法:
/**
* 重置返回点击事件
*/
public void resetBaseBack() {
if (null != baseBack) {
baseBack.setOnClickListener(this);
} else {
Logger.e(getName(), "baseBack is not exist!");
}
}
右侧功能键
这部分,在之前的博客《一个Android工程的从零开始》-2、base(一) BaseActivity布局中有所介绍,而本次也没有做出太多调整具体的可以去这篇博客中看,这里就只贴出代码效果图,以及对调整的地方做出说明了,先是在前面的布局优化博客中截出的效果图:
实现的方法如下:
/**
* 最右侧图片功能键设置方法
*
* @param resId 图片id
* @param alertText 语音辅助提示读取信息
* @return 将当前ImageView返回方便进一步处理
*/
public ImageView setBaseRightIcon1(int resId, String alertText) {
initBaseView();
baseRightIcon1 = getView(R.id.base_right_icon1);
baseRightIcon1.setImageResource(resId);
baseRightIcon1.setVisibility(View.VISIBLE);
//语音辅助提示的时候读取的信息
baseRightIcon1.setContentDescription(alertText);
baseRightIcon1.setOnClickListener(this);
return baseRightIcon1;
}
/**
* 右数第二个图片功能键设置方法
*
* @param resId 图片id
* @param alertText 语音辅助提示读取信息
* @return 将当前ImageView返回方便进一步处理
*/
public ImageView setBaseRightIcon2(int resId, String alertText) {
ImageView baseRightIcon2 = getView(R.id.base_right_icon2);
if (null != baseRightIcon1) {
baseRightIcon2.setImageResource(resId);
baseRightIcon2.setVisibility(View.VISIBLE);
//语音辅助提示的时候读取的信息
baseRightIcon2.setContentDescription(alertText);
baseRightIcon2.setOnClickListener(this);
} else {
Logger.e(getName(),"You must inflate the baseRightIcon1 before use baseRigh
}
return baseRightIcon2;
}
/**
* 最右侧文本功能键设置方法
*
* @param text 文本信息
* @return 将当前TextView返回方便进一步处理
*/
public TextView setBaseRightText(String text) {
initBaseView();
TextView baseRightText = getView(R.id.base_right_text);
baseRightText.setText(text);
baseRightText.setVisibility(View.VISIBLE);
baseRightText.setOnClickListener(this);
return baseRightText;
}
/**
* 最右侧文本功能键设置方法
*
* @param textId 文本信息id
* @return 将当前TextView返回方便进一步处理
*/
public TextView setBaseRightText(int textId) {
initBaseView();
TextView baseRightText = getView(R.id.base_right_text);
baseRightText.setText(textId);
baseRightText.setVisibility(View.VISIBLE);
baseRightText.setOnClickListener(this);
return baseRightText;
}
这里与之前不同的地方只有setBaseRightIcon2,因为在布局中,有下图所示的关系:
也就是说,BaseRightIcon2需要在BaseRightIcon1的基础上确定位置,所以在没有调用setBaseRightIcon1便直接调用setBaseRightIcon2给出错误提示。
其他
其他还有两个方法:
/**
* 隐藏头布局
*/
public void hideTitle() {
if (titleStub != null) {
titleStub.setVisibility(View.GONE);
}
}
/**
* 隐藏返回键
*/
public void hideBack() {
if (null != baseBack) {
baseBack.setVisibility(View.GONE);
} else {
Logger.e(getName(), "baseBack is not exist!");
}
}
这两个方法是之前使用include添加布局的时候使用的方法,一般逻辑下,用不到上面两个方法,不过暂时没有删除,后期看情况处理。