《一个Android工程的从零开始》阶段总结与修改1-base

先扯两句

最近在开发一个项目,算算时间也有一周没有继续写《一个android工程的从零开始》了,先跟大家道个歉。当然,这段时间真正实际操作中,也发现了自己的Base封装中有一些Bug,正好在这次开发过程中找出来,并且予以改正,特此发一篇博客说明一下,之前博客对应的部分会给予提示,部分内容就不予以修改了,算是为大家也为自己在出错的时候提供一个可查找对照的方向。错误的部分为大家使用过程中带来的不便十分抱歉。
闲言少叙,老规矩还是先上我的Git库,然后开始正文。
MyBaseApplication (https://github.com/BanShouWeng/MyBaseApplication)

正文

这部分主要分为BaseActivity封装修改、BaseFragment封装修改、Retrofit header动态添加封装三部分。

BaseActivity封装修改
BaseActivity布局

BaseActivity中,布局修改的部分是ScrollView布局的部分,当然,这部分使用不会报错,只是无法实现我们要求,具体会出现什么问题,我创建了一个测试Activity,布局文件相当简单:

<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:gravity="center_horizontal"
android:orientation="vertical"
tools:context="com.iyubatestapplication.ui.TestActivity">
<TextView
android:id="@+id/textView"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:text="大家注意看标题"
android:textSize="100sp" />
</LinearLayout>

而java部分也是简单异常:

public class TestActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setBaseConten tView(R.layout.activity_test);
setTitle("Test");
}
}

那么我们来看看效果是什么样的:

《一个Android工程的从零开始》阶段总结与修改1-base - 阿里云
这里写图片描述

大家可以看到,Title部分竟然也随着滑动了,这明显不是工程中我们想要的效果。
至于造成这个现象的原因,其实很简单,那就是之前集成的时候,我将ScrollView放在了最外层,也就是放在了Title的外面,那么ScrollView内的都会滑动,自然也就会出现Title随之滑动的现象了,既然好的了问题,那么解决起来自然就建简单了,只要将ScrollView放到Title下面一层不就好了嘛。不过真的这么简单吗?

《一个Android工程的从零开始》阶段总结与修改1-base - 阿里云
这里写图片描述

这是京东的布局,如果套用我们刚刚的设想,那么大家思考一下,下方的工具栏在我们滑动的过程中会出现什么效果?没错,就如同上面的Title一样,会出现随着滑动的情况。当然,这是首页所特有的,想必大多数APP只会有一个首页,而且就功能而言,也就是“我的”版块才会用到ScrollView,而其他版块基本都会有RecyclerView(或者ListView、GridView、VLayout等),所以说这个部分我们可以单独搭建。
可是,如果当前的Activity中需要创建带有Title或者底部固定位置布局的Fragment,那么前面Title的问题就会再次出现,作为一个一直将偷懒作为人生准则的人,当然想要弄一个都考虑到的情况,所以这部分的布局代码就被我修改成了这样一副样子:

<?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.BaseActivity">
<include
android:id="@+id/base_title_layout"
layout="@layout/title_layout"></include>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dip"
android:layout_weight="1"
android:orientation="vertical">
<LinearLayout
android:id="@+id/base_main_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:visibility="gone"></LinearLayout>
<ScrollView
android:id="@+id/base_scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"></ScrollView>
</FrameLayout>
</LinearLayout>

将LinearLayout与ScrollView放在一起,那么就可以想用哪里选哪里,当然,我们的setBaseContentView方法也要做一定的调整,成为了两个方法,setBaseContentView与setBaseScrollContentView,分别对应没有ScrollView和有ScrollView两种情况:

/**
* 引用头部布局
*
* @param layoutId 布局id
*/
public void setBaseContentView(int layoutId) {
LinearLayout layout = (LinearLayout) findViewById(R.id.base_main_layout);
//获取布局,并在BaseActivity基础上显示
final View view = getLayoutInflater().inflate(layoutId, null);
//关闭键盘
hideKeyBoard();
//给EditText的父控件设置焦点,防止键盘自动弹出
view.setFocusable(true);
view.setFocusableInTouchMode(true);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);
layout.addView(view, params);
layout.setVisibility(View.VISIBLE);
}
/**
* 引用头部布局且当前页面基于ScrollView
*
* @param layoutId 布局id
*/
public void setBaseScrollContentView(int layoutId) {
ScrollView layout = (ScrollView) findViewById(R.id.base_scroll_view);
//当子布局高度值不足ScrollView时,用这个方法可以充满ScrollView,防止布局无法显示
layout.setFillViewport(true);
//获取布局,并在BaseActivity基础上显示
final View view = getLayoutInflater().inflate(layoutId, null);
//关闭键盘
hideKeyBoard();
//给EditText的父控件设置焦点,防止键盘自动弹出
view.setFocusable(true);
view.setFocusableInTouchMode(true);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);
layout.addView(view, params);
layout.setVisibility(View.VISIBLE);
}

大家可以看得出来,我这里给方法的注释是“引用头部布局”也就是说,以上的所有讨论都是基于会引用我们的Title的情况下的,毕竟如果不使用Title的时候,我们可以直接使用setContentView方法进行布局初始化,当然,看过我之前博客的也会知道,我创建了一个hideTitle方法,用于处理Title的显隐。不过hideTitle + setBaseContentView就是setContentView,只有这种情况下,就建议大家直接使用setContentView,而hideTitle + setBaseScrollContentView则相当于在在setContentView的基础上嵌套了一层ScrollView,如果真的有需要,这个组合还是可以使用的。
当然,如果我们的布局中嵌套了ScrollView的情况下,在使用ListView与GridView的时候,会出现显示不全的情况等,这个部分想必大家都已经清楚了,而解决方法就是需要我们自定义一个自己的ListView,一般都为其命名为MyListView,而重写的方法也很简单,只需要继承ListView,并重写以下方法即可:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
try {
int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,
MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, expandSpec);
} catch (Exception e) {
}
}

不过好在我最近使用RecyclerView的时候,故意没有重写一下,简单做了个测试,并没有发现ListView想同的问题,所以暂时就直接用了,如果大家对这部分有什么其他的发现,欢迎一起沟通。

ps:在工程开发过程中,尤其是电商APP的产品浏览,经常会上设置一键返回头部的按钮,这个按钮就很显然就不适合在ScrollView布局的内层,所以我们可以考虑将其集成在BaseActivity与Title一层,当然记得将父布局改成RelativeLayout,并在下方FrameLayout中添加属性android:layout_below=“@+id/base_title_layout”

ps的ps:以上建议是在本框架基础上进行的操作,当然也可以脱离框架自行编写一个布局,而且商品浏览一把都是有列表布局的,因此也不需要使用ScrollView,非要在上下添加其他布局的情况,RecyclerView可以判断onCreateViewHolder中的viewType做对应适配,ListView、GridView通用的方法就是添加Header,至于RecyclerView添加header的方法,大家可以看看张鸿洋大神的Android 优雅的为RecyclerView添加HeaderView和FooterView,或许会有帮助。

BaseActivity方法修改####

前面说了布局,是会影响到我们使用的,下面说的方法,呃,一部分也影响到我们使用,先上方法吧。

/**
* 最右侧文本功能键设置方法
*
* @param text 文本信息
* @param clickListener 点击事件
* @return 将当前TextView返回方便进一步处理
*/
public TextView setBaseRightText(String text, View.OnClickListener clickListener) {
TextView baseRightText = (TextView) findViewById(R.id.base_right_text);
baseRightText.setText(text);
baseRightText.setVisibility(View.VISIBLE);
baseRightText.setOnClickListener(clickListener);
return baseRightText;
}

上面这个方法看过我前面博客的应该比较熟悉,那就是我在最右侧设置了一个文本功能键的使用方法,包括设置文本、设置点击时间等,不过若是细看的话,很简单就会发现与之前的不同。

第一:创建的baseRightText从全局变量变成了如今的局部变量,也就是说当我们不使用这个功能键的时候,虽然解析布局的时候依然会解析base_right_text对应id的TextView,不过至少(TextView) findViewById方法可以偷懒不执行了。

第二:TextView baseRightText = (TextView) findViewById(R.id.base_right_text),很显然这并不是ButterKnife的控件绑定方法,至于为什么这么写,主要还是因为我们无法在BaseActivity和集成它的Activity中同时添加ButterKnife.bind(this);,不然会出现如下错误:

java.lang.RuntimeException: Unable to start activity ComponentInfo{com.banshouweng.mybaseapplication/com.banshouweng.mybaseapplication.ui.activity.MainActivity}: java.lang.IllegalStateException: Required view ‘address_list’ with ID 2131427418 for field ‘addressList’ was not found. If this view is optional add ‘@Nullable’ (fields) or ‘@Optional’ (methods) annotation.

Caused by: java.lang.IllegalStateException: Required view ‘address_list’ with ID 2131427418 for field ‘addressList’ was not found. If this view is optional add ‘@Nullable’ (fields) or ‘@Optional’ (methods) annotation.

原因就是在集成BaseActivity的Activity(以MainActivity为例)的onCreate,会有super.onCreate(savedInstanceState);方法,也就是说MainActivity的onCreate执行之前我们会先执行BaseActivity的onCreate方法,而BaseActivity中有ButtKnife.bind(this);方法,看过我之前的博客的会知道,我在BaseActivity中有这么一段代码:

if (!(this instanceof MainActivity)) {
activities.add(this);
}

先把我查的this是什么贴出来:

this确实是当前activity的指针,它可以传给Context是因为Activity是Context的一个子类

也就是说当MainActivity继承BaseActivity时,this就是MainActivity,所以绑定控件的时候,就会以BaseActivity的布局文件作为参照,而这部分自然是没有MainActivity中对应的布局文件的,自然就会报上述错误。
反之,如果是在MainActivity中添加ButtKnife.bind(this);方法,而不再BaseActivity中添加,我们可以在下图位置查找到MainActivity_ViewBinding类:

《一个Android工程的从零开始》阶段总结与修改1-base - 阿里云
这里写图片描述

在其中可以看到:

public class MainActivity_ViewBinding implements Unbinder {
private MainActivity target;
@UiThread
public MainActivity_ViewBinding(MainActivity target) {
this(target, target.getWindow().getDecorView());
}
@UiThread
public MainActivity_ViewBinding(MainActivity target, View source) {
this.target = target;
target.addressList = Utils.findRequiredViewAsType(source, R.id.address_list, "field ‘addressList’", RecyclerView.class);
}
@Override
@CallSuper
public void unbind() {
MainActivity target = this.target;
if (target == null) throw new IllegalStateException("Bindings already cleared.");
this.target = null;
target.addressList = null;
}
}

也就是说,其中只有我在MainActivity中的addressList控件,并没有BaseActivity中的控件,所以很遗憾,这样依然会报上述错误,因此,在BaseActivity中只能使用TextView baseRightText = (TextView) findViewById(R.id.base_right_text);关联控件与布局文件。

其三:@return 将当前TextView返回方便进一步处理
这个部分也比较好理解,有很多情况下,我们需要做界面的复用,也就是说右上角的图片功能键、文本功能键都不是固定的,需要修改资源,或者隐藏掉已经显示出来的功能键,而如果每次都创建一个对应的方法就得不偿失了,所以这里我们便将对应的功能键控件返回到调用的Activity中,需要处理的时候,处理对应返回的控件即可。

其四:baseRightText.setOnClickListener(clickListener);
这里不再创建对应的新接口,而是统一使用OnClickListener,当然,作为懒人,之前我一直懒得记每个对应控件的Id,不过第三点已经说了,我们将对应的控件返回了回来,所以只需要调用对应控件的getId()方法就可以很优雅的解决掉我们的偷懒问题。

BaseFragment封装修改

前面BaseActivity写了那么多,作为一个懒人,大家就别指望我在这里也会写那么多了,这里我只总结了一句话,那就是参见“BaseActivity封装修改”,对应做修改即可——和谐社会,不能打人的。

Retrofit header动态添加封装

这个部分呢,是最近有这方面需求,所以我就查看了一下,首先还是先感谢zhuhai__yizhi的Retrofit添加header参数的几种方法。帮了个大忙。
大家可以通过zhuhai__yizhi了解一下对应的三种方法,而作为一个懒人的我,实在是太懒了,而且是集成的Retrofit2.3.0的我,就稍微尝试了一下,结果发现了一个更好的偷懒方法:

//Post
public interface RetrofitPostService {
@FormUrlEncoded
@POST("{action}")
Observable<ResponseBody> postResult(@Path("action") String action, @HeaderMap Map<String, String> headerParams, @FieldMap Map<String, String> params);
}
//Get
public interface RetrofitGetService {
@GET("{action}")
Observable<ResponseBody> getResult(@Path("action") String action, @HeaderMap Map<String, String> headerParams, @QueryMap Map<String, String> params);
}

大家应该看出来了,那就是@HeaderMap Map<String, String> headerParams参数,用法就是Key是字段,Value是我们要传递的值,当然现阶段我接触到的,需要传递Header的还是比较少的,但是多了解一下总归是没有坏处的嘛。

附录

《一个Android工程的从零开始》- 目录