美文网首页Android知识Android开发经验谈Android技术知识
《一个Android工程的从零开始》阶段总结与修改5-BaseL

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

作者: 半寿翁 | 来源:发表于2018-04-05 23:25 被阅读0次

    先扯两句

    其实按照正常情况,这篇博客应该是在上一篇之前发的,实在是之前公司遇到了一个新需求,要创建一个背景透明的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());如下图标注的位置。

    image

    而这个方法的具体实现如下。

    /**
     * 引用头部布局
     *
     * @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 与布局

    1. 【推荐】灵活使用布局,推荐 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添加布局的时候使用的方法,一般逻辑下,用不到上面两个方法,不过暂时没有删除,后期看情况处理。

    附录

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

    相关文章

      网友评论

        本文标题:《一个Android工程的从零开始》阶段总结与修改5-BaseL

        本文链接:https://www.haomeiwen.com/subject/pvqjhftx.html