ViewStub的奇淫巧技
昨天写了个约束布局的博客,写了很久,虽然文章写得还是不尽人意,但真的感觉到对约束布局的理解更加清晰了。
众所周知,include可以复用布局,merge可以减少层级,这俩个标签不是主角,要是想学习其用法,请自行搜索,不多赘述。
ViewStub可以干什么?
我认为ViewStub可以减少初始化UI时的性能消耗。使用ViewStub标记的View或ViewGroup在UI初始化时并不会消耗性能,它需要在被设置为可见的时候,或是调用了inflate()的时候,才会实例化标记的View或ViewGroup。在之前我了解过布局优化,也知道抽象布局(include,merge,viewstub的总称),但只是用过include和merge标签。虽然都是优化布局的标签,但是觉得ViewStub很鸡肋。直到我看了 ViewStub标签的使用 这个视频,突然灵光一闪,觉得这个标签适用场景还是很多的,具体看一下我的例子吧。
在这个界面里面我用了俩个ViewStub标签,黄色方框和红色方框框起来的部分。就拿黄色方框中的三个button来说,按照一般思路,我们在布局时先将它们放置在相应位置上,然后在初始化布局时,将“清空”和“计算”隐藏起来,在再合适的时机将“立即计算”隐藏起来,让它们的可见性为可见。
但不管是隐藏还是显示,初始化组件时总要获取其实例来绘制,因而消耗性能。ViewStub却可以减少这部分隐藏组件在UI初始化时消耗的性能,当隐藏的是布局的时候那减少的消耗会更多。具体是怎样实现的呢?让我们先看一下,下面的UI布局时的图。
从上图中,我们可以理解ViewStub为占位符,它只占着那个位置却不会被绘制。也只有当ViewStub被设置为可见的时候,或是调用了ViewStub.inflate()的时候,ViewStub所向的布局就会被Inflate和实例化,然后ViewStub的布局属性都会传给它所指向的布局。(而且看到没有约束布局下零层级!!!)
ViewStub的缺点:ViewStub只在UI初始化后inflate一次,这也是它无法代替View可见性的重要原因。
但我们对于不需要多次显示与隐藏的view或viewGroup,ViewStub就是首选啦!
接下来看看实现的代码吧。
<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种写法