《一个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__yizhiRetrofit添加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工程的从零开始》- 目录

    原文作者:半寿翁
    原文地址: https://www.jianshu.com/p/f80377514521
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞