ViewStub的奇淫巧技

昨天写了个约束布局的博客,写了很久,虽然文章写得还是不尽人意,但真的感觉到对约束布局的理解更加清晰了。

众所周知,include可以复用布局,merge可以减少层级,这俩个标签不是主角,要是想学习其用法,请自行搜索,不多赘述。

ViewStub可以干什么?

我认为ViewStub可以减少初始化UI时的性能消耗。使用ViewStub标记的View或ViewGroup在UI初始化时并不会消耗性能,它需要在被设置为可见的时候,或是调用了inflate()的时候,才会实例化标记的View或ViewGroup。在之前我了解过布局优化,也知道抽象布局(include,merge,viewstub的总称),但只是用过include和merge标签。虽然都是优化布局的标签,但是觉得ViewStub很鸡肋。直到我看了 ViewStub标签的使用 这个视频,突然灵光一闪,觉得这个标签适用场景还是很多的,具体看一下我的例子吧。

ViewStub示例图

在这个界面里面我用了俩个ViewStub标签,黄色方框和红色方框框起来的部分。就拿黄色方框中的三个button来说,按照一般思路,我们在布局时先将它们放置在相应位置上,然后在初始化布局时,将“清空”和“计算”隐藏起来,在再合适的时机将“立即计算”隐藏起来,让它们的可见性为可见。

但不管是隐藏还是显示,初始化组件时总要获取其实例来绘制,因而消耗性能。ViewStub却可以减少这部分隐藏组件在UI初始化时消耗的性能,当隐藏的是布局的时候那减少的消耗会更多。具体是怎样实现的呢?让我们先看一下,下面的UI布局时的图。

UI布局图

从上图中,我们可以理解ViewStub为占位符,它只占着那个位置却不会被绘制。也只有当ViewStub被设置为可见的时候,或是调用了ViewStub.inflate()的时候,ViewStub所向的布局就会被Inflate和实例化,然后ViewStub的布局属性都会传给它所指向的布局。(而且看到没有约束布局下零层级!!!)

ViewStub的缺点:ViewStub只在UI初始化后inflate一次,这也是它无法代替View可见性的重要原因。

但我们对于不需要多次显示与隐藏的view或viewGroup,ViewStub就是首选啦!

扫描二维码关注公众号,回复: 4959763 查看本文章

接下来看看实现的代码吧。

<ViewStub
        android:id="@+id/calculator_view_stub"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout="@layout/calculator_viewstub_btn"
        app:layout_constraintEnd_toEndOf="@id/gl_right"
        app:layout_constraintTop_toBottomOf="@id/view"
        />
    <ViewStub
        android:id="@+id/scroll_text_view_stub"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout="@layout/info_scroll"
        android:layout_marginTop="73dp"
        android:layout_marginRight="@dimen/margin_s"
        android:layout_marginLeft="@dimen/margin_s"
        android:layout_marginBottom="@dimen/margin_s"
        app:layout_constraintEnd_toStartOf="@+id/gl_right"
        app:layout_constraintStart_toStartOf="@+id/gl_left"
        app:layout_constraintTop_toBottomOf="@+id/view"
        app:layout_constraintBottom_toTopOf="@id/btn_generating_bill"
        />

在主布局下要注意的应该就只有  android:layout="@layout/calculator_viewstub_btn"  这个了,很多同学在原有布局下进行优化布局,往往会直接把include标签改为ViewStub标签,导致出现了下列报错:

java.lang.IllegalArgumentException: ViewStub must have a valid layoutResource

这是因为include标签下引用布局用的是  layout="@layout/calculator_viewstub_btn" ,而ViewStub 用的是android:layout="@layout/calculator_viewstub_btn" 。

这俩个ViewStub指向的布局很简单,但我还是给展示一下吧。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_height="wrap_content"
    android:layout_width="wrap_content"
    android:orientation="horizontal"
    xmlns:tools="http://schemas.android.com/tools"
    tools:showIn="@layout/calculator_activity">
    <Button
        android:id="@+id/btn_refresh"
        android:layout_width="57dp"
        android:layout_height="43dp"
        android:layout_marginEnd="@dimen/margin_m"
        android:layout_marginTop="@dimen/margin_l"
        android:background="@drawable/btn_small"
        android:text="清空"
        android:onClick="refreshClick"
         />

    <Button
        android:id="@+id/btn_goon"
        android:layout_width="57dp"
        android:layout_height="43dp"
        android:layout_marginTop="@dimen/margin_l"
        android:background="@drawable/btn_small"
        android:text="计算"
        android:onClick="goonClick"
         />
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.NestedScrollView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/corners_text_bg">
    <TextView
        android:id="@+id/tv_aggregate"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="@dimen/text_size_big" />
</android.support.v4.widget.NestedScrollView>

可以看出以上俩个都是ViewGroup,若是按照一般的写法,难免会出现层级嵌套,如此做法就可以保证在UI初始化时零层级的布局了。

以上的东西还是简单的,当ViewStub指向的布局里面的组件需要进行交互时,就有几个不常见的坑了。先上代码吧。

package com.example.roy.recycleviewtest.activity;

import android.content.Intent;
import android.os.Bundle;
import android.support.constraint.Guideline;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewStub;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

import com.bigkoo.pickerview.builder.TimePickerBuilder;
import com.bigkoo.pickerview.listener.OnTimeSelectListener;
import com.bigkoo.pickerview.view.TimePickerView;
import com.example.roy.recycleviewtest.R;
import com.example.roy.recycleviewtest.base.BaseActivity;
import com.example.roy.recycleviewtest.util.ToastUtils;

import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

import butterknife.BindView;
import butterknife.ButterKnife;

public class CalculatorActivity extends BaseActivity implements View.OnClickListener {


    @BindView(R.id.view)
    View view;
    @BindView(R.id.toolBar)
    Toolbar toolBar;
    @BindView(R.id.tv_hint1)
    TextView tvHint1;
    @BindView(R.id.et_loan)
    EditText etLoan;
    @BindView(R.id.tv_hint2)
    TextView tvHint2;
    @BindView(R.id.et_interest)
    EditText etInterest;
    @BindView(R.id.tv_hint3)
    TextView tvHint3;
    @BindView(R.id.et_start_time)
    EditText etStartTime;
    @BindView(R.id.btn_start_time)
    Button btnStartTime;
    @BindView(R.id.et_end_time)
    EditText etEndTime;
    @BindView(R.id.btn_end_time)
    Button btnEndTime;
    @BindView(R.id.btn_calculate)
    Button btnCalculate;
    @BindView(R.id.btn_generating_bill)
    Button btnGeneratingBill;
    @BindView(R.id.calculator_view_stub)
    ViewStub calculatorViewStub;
    @BindView(R.id.gl_left)
    Guideline glLeft;
    @BindView(R.id.gl_right)
    Guideline glRight;
    @BindView(R.id.gl_between)
    Guideline glBetween;
    @BindView(R.id.scroll_text_view_stub)
    ViewStub scrollViewStub;
    
    TextView tvAggregate;
    int oneYearDays = 365;
    long s1 = 0;
    long s2 = 0;
    int day = 0;
    double aggregate = 0;
    double count;
    String s;
    StringBuffer sbAggregate;
    DecimalFormat df = new DecimalFormat("0.00");//格式化小数



    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.btn_start_time:
                hideSoftKeyBoard(this);
                showStartDate();
                break;
            case R.id.btn_end_time:
                hideSoftKeyBoard(this);
                showEndDate();
                break;
            case R.id.btn_calculate:
                if (etLoan.getText().toString().trim().isEmpty())
                    ToastUtils.showShort(this, "填好必填项!");
                else if (etInterest.getText().toString().trim().isEmpty())
                    ToastUtils.showShort(this, "填好必填项!");
                else if (etStartTime.getText().toString().trim().isEmpty())
                    ToastUtils.showShort(this, "填好必填项!");
                else if (etEndTime.getText().toString().trim().isEmpty())
                    ToastUtils.showShort(this, "填好必填项!");
                else {
                    scrollViewStub.inflate();
                    tvAggregate=this.findViewById(R.id.tv_aggregate);
                    day = (int) ((s2 - s1) / 1000 / 60 / 60 / 24);
                    aggregate = compoundInterest(
                            Float.parseFloat(etLoan.getText().toString()),
                            Float.parseFloat(etInterest.getText().toString()),
                            day);
                    sbAggregate = new StringBuffer();
                    sbAggregate.append(df.format(aggregate));//返回的是String类型
                    tvAggregate.setText(sbAggregate);
                    btnCalculate.setVisibility(View.GONE);
                    calculatorViewStub.inflate();
                }
                break;
            case R.id.btn_generating_bill:
                break;
        }
    }


    @Override
    protected void setContentView() {
        setContentView(R.layout.calculator_activity);
    }

    @Override
    protected void initData() {
        Intent intent = getIntent();
        s = intent.getStringExtra("tool_title");
        SimpleDateFormat format = new SimpleDateFormat("yyyy");
        int thisYear = Integer.parseInt(format.format(new Date()));
        if (thisYear % 4 == 0) {
            oneYearDays = 366;
        }
    }

    @Override
    protected void initView() {
        setSupportActionBar(toolBar);
        toolBar.setTitle(s);
//        getSupportActionBar().setDisplayShowTitleEnabled(false);
        etStartTime.setFocusable(false);
        etEndTime.setFocusable(false);
        //可以通过Acivity的findViewById方法获取TextView对象
//        btnGoon=this.findViewById(R.id.btn_goon);
//        btnRefresh=this.findViewById(R.id.btn_refresh);
    }

    @Override
    protected void initEvent() {
        btnStartTime.setOnClickListener(this);
        btnEndTime.setOnClickListener(this);
        btnCalculate.setOnClickListener(this);
        btnGeneratingBill.setOnClickListener(this);
    }

    @Override
    protected void onResumeFragments() {
        super.onResumeFragments();
    }

    /**
     *
     * @param presentValue 初始金额
     * @param interest     日化利息,利率或折现率
     * @param number       计息次数(按天计算)
     */
    private double compoundInterest(float presentValue, double interest, int number) {
        double f;//终值
        double i = interest / oneYearDays / 100;
        f = presentValue * Math.pow(1 + i, number);
        return twoDecimal(f);
    }

    private double twoDecimal(double td) {
        BigDecimal bd = new BigDecimal(td);
        return bd.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue();
    }

    public void goonClick(View view) {
        if (etLoan.getText().toString().trim().isEmpty())
            ToastUtils.showShort(CalculatorActivity.this, "填好必填项!");
        else if (etInterest.getText().toString().trim().isEmpty())
            ToastUtils.showShort(CalculatorActivity.this, "填好必填项!");
        else if (etStartTime.getText().toString().trim().isEmpty())
            ToastUtils.showShort(CalculatorActivity.this, "填好必填项!");
        else if (etEndTime.getText().toString().trim().isEmpty())
            ToastUtils.showShort(CalculatorActivity.this, "填好必填项!");
        else {
            day = (int) ((s2 - s1) / 1000 / 60 / 60 / 24);
            count = compoundInterest(
                    Float.parseFloat(etLoan.getText().toString()),
                    Float.parseFloat(etInterest.getText().toString()),
                    day);
            sbAggregate.append("+" + df.format(count));
            aggregate = aggregate + count;
            //返回的是String类型
            sbAggregate.append("\n" + df.format(aggregate));
            tvAggregate.setText(sbAggregate);
        }
    }

    public void refreshClick(View view) {
        finish();
        CalculatorActivity.actionStart(CalculatorActivity.this, CalculatorActivity.class);
    }

}

首先从获取实例来讲,因为我用的是ButterKnife,俩个ViewStub的实例也获取了,然而那几个指向布局里面的组件并未获取。它们的获取方式也不一般,需要通过Acivity的findViewById方法获取View的对象,也就是下面这种方式。

tvAggregate=this.findViewById(R.id.tv_aggregate); 

并且得在包含它的布局调用inflate()之后。这理解起来很简单,先将布局实例化,再将组件实例化。我在示例中获取TextView实例是在触发“立即计算”中,但你们仔细看过代码之后会发现在java文件中并未实例化俩个button组件。

XML文件中的button下可以看到下面这样的一条属性。

android:onClick="refreshClick"

其实button的点击事件大致可以分为三种(写法细分为五种),一是匿名内部类写法,但整个界面的按钮只有一个时这样写,极为简便;二是在Activity.java类中实现View.OnClickListener接口,这样方便管理多个点击事件;第三种就是我们用的这种方法了,直接在XML文件中定义onclick事件。然而,因为这种方法不适合低耦合的理念,所以几乎在项目中无法看到这种用法。

ViewStub要是只能用来一次显示组件,这将大大减小了它的适用场景,当ViewStub指向的布局里面的组件可以进行交互,那么Viewstub还是有学习的必要的。第三种方法正好适用于我的项目中,前俩种写法对于这种场景的代码不是太友好,如下:

case R.id.btn_calculate:
                if (etLoan.getText().toString().trim().isEmpty())
                    ToastUtils.showShort(this, "填好必填项!");
                else if (etInterest.getText().toString().trim().isEmpty())
                    ToastUtils.showShort(this, "填好必填项!");
                else if (etStartTime.getText().toString().trim().isEmpty())
                    ToastUtils.showShort(this, "填好必填项!");
                else if (etEndTime.getText().toString().trim().isEmpty())
                    ToastUtils.showShort(this, "填好必填项!");
                else {

                    scrollViewStub.inflate();
                    tvAggregate=this.findViewById(R.id.tv_aggregate);
                    day = (int) ((s2 - s1) / 1000 / 60 / 60 / 24);
                    aggregate = compoundInterest(
                            Float.parseFloat(etLoan.getText().toString()),
                            Float.parseFloat(etInterest.getText().toString()),
                            day);
                    sbAggregate = new StringBuffer();
                    sbAggregate.append(df.format(aggregate));//返回的是String类型
                    tvAggregate.setText(sbAggregate);
                    btnCalculate.setVisibility(View.GONE);
//                    calculatorViewStub.inflate();
                    calculatorViewStub.inflate();
                    btGoon=this.findViewById(R.id.btn_goon);
                    btRefresh=this.findViewById(R.id.btn_refresh);
                    btGoon.setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View view) {
                            goonClick();
                        }
                    });
                    btRefresh.setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View view) {
                            refreshClick();
                        }
                    });
                }
                break;

 以上代码简直不忍直视。。。

再来说说Viewstub的局限性吧,在activity的生命周期里面inflate()方法只能被调用一次(只测试了activity生命周期内的情况),再将重复组件隐藏的话,就需要用设置view的可见性来实现了。

由示例可以看出ViewStub的适用范围还是挺大的,在初始化UI时不用加载指向的View,在需要加载布局与组件时候去加载,可以优化UI初始化时的性能。ViewStub使我的小项目中真正地实现了零层级,也希望我的示例可以给你的性能优化带来灵感。

笔者小白一枚,望指正,谢谢!

参考:ViewStub基本用法

           android_button onclick点击事件的5种写法

           Android XML中onClick实现原理

 

猜你喜欢

转载自blog.csdn.net/NULL_thing/article/details/82929717
今日推荐