开发应用的过程中,首页的控件越来越多,布局文件的代码已经到了爆表的程度,而且不同状态下首页各个控件的 Visibility 不同,每次新增状态都是一件头疼的事情,时常遗漏控件导致出错,和 YYY 大佬交流讨论后他给出了一种巧妙的方案,特此学习记录一下!
此处的多状态布局是指一个约束布局中,有很多的子布局和控件(Demo 中仅使用控件,嵌套子布局效果也是一样的),这些布局和控件根据首页状态的不同,各自的显示隐藏状态也不同,形成了不同的布局呈现。

| 位数 | 控件 | 二进制码 | 十进制值 |
|---|---|---|---|
| 低位第一位 | tv_author_name | 0000 0000 0000 0000 0000 0000 0000 0001 | 1 |
| 低位第二位 | tv_author_introduction | 0000 0000 0000 0000 0000 0000 0000 0010 | 2 |
| 低位第三位 | tv_tool_box | 0000 0000 0000 0000 0000 0000 0000 0100 | 4 |
| 低位第四位 | tv_folder | 0000 0000 0000 0000 0000 0000 0000 1000 | 8 |
| 低位第五位 | iv_zoom_in | 0000 0000 0000 0000 0000 0000 0001 0000 | 16 |
| 低位第六位 | iv_zoom_out | 0000 0000 0000 0000 0000 0000 0010 0000 | 32 |
| 低位第七位 | iv_close | 0000 0000 0000 0000 0000 0000 0100 0000 | 64 |
| 低位第八位 | iv_android | 0000 0000 0000 0000 0000 0000 1000 0000 | 128 |
| 低位第九位 | tv_tab_first | 0000 0000 0000 0000 0000 0001 0000 0000 | 256 |
| 低位第十位 | tv_tab_second | 0000 0000 0000 0000 0000 0010 0000 0000 | 512 |
| 低位第十一位 | tv_tab_third | 0000 0000 0000 0000 0000 0100 0000 0000 | 1024 |
观察发现,其实就是第一位的 1 不断的向左移动,这就让人想起了位运算中的左移运算[1],比起直接使用十进制数来赋值表示要准确明了许多
private const val INDEX = 1
const val INDEX_VIEW_AUTHOR_NAME :Int = INDEX shl 0
const val INDEX_VIEW_AUTHOR_INTRODUCTION :Int = INDEX shl 1
const val INDEX_VIEW_TOOL_BOX :Int = INDEX shl 2
const val INDEX_VIEW_FOLDER :Int = INDEX shl 3
const val INDEX_VIEW_ZOOM_IN :Int = INDEX shl 4
const val INDEX_VIEW_ZOOM_OUT :Int = INDEX shl 5
const val INDEX_VIEW_CLOSE :Int = INDEX shl 6
const val INDEX_VIEW_ANDROID :Int = INDEX shl 7
const val INDEX_VIEW_TAB_FIRST :Int = INDEX shl 8
const val INDEX_VIEW_TAB_SECOND :Int = INDEX shl 9
const val INDEX_VIEW_TAB_THIRD :Int = INDEX shl 10
// 全屏模式
const val INDEX_FULL_SCREEN = INDEX_VIEW_AUTHOR_NAME or INDEX_VIEW_AUTHOR_INTRODUCTION
// 简洁模式
const val INDEX_CONCISE_MODE =
INDEX_VIEW_AUTHOR_NAME or
INDEX_VIEW_AUTHOR_INTRODUCTION or
INDEX_VIEW_TAB_FIRST or
INDEX_VIEW_TAB_SECOND or
INDEX_VIEW_TAB_THIRD
现在可以通过控件显示或者隐藏来决定当前布局的状态,那么反过来当拿到布局状态,如何确定这个状态下,各个控件的可见性情况?
答案是使用位运算中的与运算[3],将需要确定的控件的单独显示状态对应的 Int 值与表示当前首页状态的 Int 值做与运算,如果和这个控件单独显示的状态值相同表示这个控件是显示的,不同则表示它是隐藏的,
比如说要确定全屏模式下,作者姓名是否展示,可以这样做:
INDEX_FULL_SCREEN and INDEX_VIEW_AUTHOR_NAME
0000 0000 0000 0000 0000 0000 0000 0011 and 0000 0000 0000 0000 0000 0000 0000 0001
结果是 0000 0000 0000 0000 0000 0000 0000 0001 表示作者姓名是显示的
现在可以表示不同状态下的首页布局的情况了,那么还需要考虑的就是不同状态的切换了
1.)两种状态差异过大,直接切换,这种情况就可以直接根据不同状态的值进行控件的显示与隐藏操作
2.)比当前状态多或者少展示一个控件
这种情况下当然可以根据不同状态的值进行显示与隐藏操作,但是状态粒度太小对于我们来说后期维护会非常吃力,布局的状态会成指数增加,
所以当两种状态变化不大,或者是某个控件在多种状态下都有可能显示或者隐藏,我们采取另外的策略,即在当前状态下补充进去或者筛减出来。
如何补充呢?根据上面第二点布局的状态表示,我们可以知道当前布局状态就是使用或运算将仅显示单个控件的状态组合在一起,那么补充进来一个控件就是在现有的基础上与目标控件进行或运算。
例如:在简洁模式的基础上,显示关闭按钮:
INDEX_CONCISE_MODE or INDEX_VIEW_CLOSE
0000 0000 0000 0000 0000 0111 0000 0011 or 0000 0000 0000 0000 0000 0000 0100 0000
=》0000 0000 0000 0000 0000 0111 0100 0011
如何筛减呢? 本着相同为 0 不同为 1 的原则,想要排除一个显示控件,需要将当前状态和目标控件的单独显示状态做位运算中的异或运算[4]
例如:在简洁模式且显示关闭按钮的基础上,隐藏关闭按钮:
(INDEX_CONCISE_MODE or INDEX_VIEW_CLOSE) xor INDEX_VIEW_CLOSE
0000 0000 0000 0000 0000 0111 0100 0011 or 0000 0000 0000 0000 0000 0000 0100 0000
=》0000 0000 0000 0000 0000 0111 0000 0011
到目前为止,情况基本上都考虑完善了,接下来就是实现上需要注意的地方:
1.)我们可以使用 Map 来收集控件对象的实例,Key 就是单个控件展示的状态值,Value 就是控件对象实例
2.)要预先写好几种状态的值,如初始状态,全屏模式,简洁模式等
3.)使用一个类来统一管理首页的状态和展示
GitHub 代码 https://github.com/NicholasHzf/LayerVisibility
<androidx.constraintlayout.widget.ConstraintLayout 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"
tools:context=".MainActivity">
<TextView
android:id="@+id/tv_change_state"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="状态切换"
android:textColor="@color/black"
android:textSize="18sp"
android:background="#8E8F8D"
android:padding="4dp"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/tv_add_close"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_add_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="添加「关闭」按钮"
android:textColor="@color/black"
android:textSize="18sp"
android:background="#8E8F8D"
android:padding="4dp"
app:layout_constraintStart_toEndOf="@id/tv_change_state"
app:layout_constraintEnd_toStartOf="@id/tv_reduce_close"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_reduce_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="移除「关闭」按钮"
android:textColor="@color/black"
android:textSize="18sp"
android:background="#8E8F8D"
android:padding="4dp"
app:layout_constraintStart_toEndOf="@id/tv_add_close"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_author_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Nicholas.Hzf"
android:textColor="@color/black"
android:textSize="18sp"
android:background="#0088ff"
android:padding="14dp"
android:layout_marginTop="10dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_change_state" />
<TextView
android:id="@+id/tv_author_introduction"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="每天进步一点点"
android:textColor="#d1d1d1"
android:textSize="14sp"
android:paddingVertical="7dp"
android:paddingHorizontal="14dp"
android:background="#0088ff"
android:layout_marginTop="10dp"
app:layout_constraintStart_toStartOf="@id/tv_author_name"
app:layout_constraintTop_toBottomOf="@id/tv_author_name" />
<TextView
android:id="@+id/tv_tool_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="工具箱"
android:textColor="@color/white"
android:textStyle="bold"
android:textSize="16sp"
android:padding="14dp"
android:background="@color/black"
android:layout_marginTop="10dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_reduce_close" />
<TextView
android:id="@+id/tv_folder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="文件夹"
android:textColor="@color/white"
android:textStyle="bold"
android:textSize="16sp"
android:padding="14dp"
android:background="@color/black"
android:layout_marginEnd="7dp"
android:layout_marginTop="10dp"
app:layout_constraintTop_toBottomOf="@id/tv_reduce_close"
app:layout_constraintEnd_toStartOf="@id/tv_tool_box"/>
<ImageView
android:id="@+id/iv_zoom_in"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/ic_baseline_add_24"
android:background="@color/black"
android:layout_marginBottom="10dp"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintBottom_toTopOf="@id/iv_zoom_out"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/iv_zoom_out"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/ic_baseline_reduce_24"
android:background="@color/black"
app:layout_constraintTop_toBottomOf="@id/iv_zoom_in"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<ImageView
android:id="@+id/iv_close"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="16dp"
android:background="@color/black"
android:src="@drawable/ic_baseline_close_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.8" />
<ImageView
android:id="@+id/iv_android"
android:layout_width="84dp"
android:layout_height="84dp"
android:src="@drawable/ic_baseline_android_24"
android:background="#0088ff"
android:padding="14dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_tab_first"
android:layout_width="0dp"
android:layout_height="56dp"
android:text="TAB1"
android:textColor="@color/black"
android:textSize="18sp"
android:background="#8BC34A"
android:gravity="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/tv_tab_second"/>
<TextView
android:id="@+id/tv_tab_second"
android:layout_width="0dp"
android:layout_height="56dp"
android:text="TAB2"
android:textColor="@color/black"
android:textSize="18sp"
android:background="#8BC34A"
android:gravity="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/tv_tab_first"
app:layout_constraintEnd_toStartOf="@id/tv_tab_third"/>
<TextView
android:id="@+id/tv_tab_third"
android:layout_width="0dp"
android:layout_height="56dp"
android:text="TAB3"
android:textColor="@color/black"
android:textSize="18sp"
android:background="#8BC34A"
android:gravity="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/tv_tab_second" />
androidx.constraintlayout.widget.ConstraintLayout>
package com.hzf.layerproject
import android.view.View
import androidx.core.view.isVisible
import kotlin.random.Random
/**
* @ClassName: IndexStateManager
* @Description: 首页状态管理类
* @Author: Nicholas.hzf
* @Date: 2022/8/13 18:01 Created
*/
object IndexStateManager {
private val mViewMap by lazy {
HashMap<Int, View>()
}
private const val VIEW_SIZE = 11
private const val INDEX = 1
const val INDEX_VIEW_AUTHOR_NAME :Int = INDEX shl 0
const val INDEX_VIEW_AUTHOR_INTRODUCTION :Int = INDEX shl 1
const val INDEX_VIEW_TOOL_BOX :Int = INDEX shl 2
const val INDEX_VIEW_FOLDER :Int = INDEX shl 3
const val INDEX_VIEW_ZOOM_IN :Int = INDEX shl 4
const val INDEX_VIEW_ZOOM_OUT :Int = INDEX shl 5
const val INDEX_VIEW_CLOSE :Int = INDEX shl 6
const val INDEX_VIEW_ANDROID :Int = INDEX shl 7
const val INDEX_VIEW_TAB_FIRST :Int = INDEX shl 8
const val INDEX_VIEW_TAB_SECOND :Int = INDEX shl 9
const val INDEX_VIEW_TAB_THIRD :Int = INDEX shl 10
const val PRIMARY_STATE =
INDEX_VIEW_AUTHOR_NAME or
INDEX_VIEW_AUTHOR_INTRODUCTION or
INDEX_VIEW_TOOL_BOX or
INDEX_VIEW_FOLDER or
INDEX_VIEW_ZOOM_IN or
INDEX_VIEW_ZOOM_OUT or
INDEX_VIEW_ANDROID or
INDEX_VIEW_TAB_FIRST or
INDEX_VIEW_TAB_SECOND or
INDEX_VIEW_TAB_THIRD
const val INDEX_FULL_SCREEN =
INDEX_VIEW_AUTHOR_NAME or INDEX_VIEW_AUTHOR_INTRODUCTION
const val INDEX_CONCISE_MODE =
INDEX_VIEW_AUTHOR_NAME or
INDEX_VIEW_AUTHOR_INTRODUCTION or
INDEX_VIEW_TAB_FIRST or
INDEX_VIEW_TAB_SECOND or
INDEX_VIEW_TAB_THIRD
private var CURRENT_STATE = PRIMARY_STATE
fun initViewMap(viewList: List<View>){
if (viewList.size != VIEW_SIZE){
throw Exception("View 数量错误")
}
mViewMap.clear()
CURRENT_STATE = PRIMARY_STATE
mViewMap[INDEX_VIEW_AUTHOR_NAME] = viewList[0]
mViewMap[INDEX_VIEW_AUTHOR_INTRODUCTION] = viewList[1]
mViewMap[INDEX_VIEW_TOOL_BOX] = viewList[2]
mViewMap[INDEX_VIEW_FOLDER] = viewList[3]
mViewMap[INDEX_VIEW_ZOOM_IN] = viewList[4]
mViewMap[INDEX_VIEW_ZOOM_OUT] = viewList[5]
mViewMap[INDEX_VIEW_CLOSE] = viewList[6]
mViewMap[INDEX_VIEW_ANDROID] = viewList[7]
mViewMap[INDEX_VIEW_TAB_FIRST] = viewList[8]
mViewMap[INDEX_VIEW_TAB_SECOND] = viewList[9]
mViewMap[INDEX_VIEW_TAB_THIRD] = viewList[10]
updateViews()
}
fun updateViews(){
mViewMap.keys.forEach { key ->
mViewMap[key]?.isVisible = (key and CURRENT_STATE) == key
}
}
fun destroyViews(){
mViewMap.clear()
CURRENT_STATE = 0
}
fun showView(view: Int){
CURRENT_STATE = CURRENT_STATE or view
updateViews()
}
fun hideView(view: Int){
CURRENT_STATE = CURRENT_STATE xor view
updateViews()
}
fun changeState(state: Int){
CURRENT_STATE = state
updateViews()
}
fun changeStateRandom(){
val random = Random.nextInt(3)
CURRENT_STATE = when(random){
0 -> PRIMARY_STATE
1 -> INDEX_FULL_SCREEN
2 -> INDEX_CONCISE_MODE
else -> PRIMARY_STATE
}
updateViews()
}
fun getCurrentState() = CURRENT_STATE
}
package com.hzf.layerproject
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.hzf.layerproject.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
IndexStateManager.initViewMap(mutableListOf(
binding.tvAuthorName,binding.tvAuthorIntroduction,
binding.tvToolBox,binding.tvFolder,
binding.ivZoomIn,binding.ivZoomOut,
binding.ivClose,binding.ivAndroid,
binding.tvTabFirst,binding.tvTabSecond,binding.tvTabThird
))
binding.tvChangeState.setOnClickListener {
IndexStateManager.changeStateRandom()
}
binding.tvAddClose.setOnClickListener {
IndexStateManager.showView(IndexStateManager.INDEX_VIEW_CLOSE)
}
binding.tvReduceClose.setOnClickListener {
IndexStateManager.hideView(IndexStateManager.INDEX_VIEW_CLOSE)
}
}
}
【1】左移运算:将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0)
例如:INDEX shl 10
0000 0000 0000 0000 0000 0000 0000 0001 左移 10 位,得到
0000 0000 0000 0000 0000 01000 0000 0000
【2】或运算:相同位进行比较,有1则对应位的结果为1,否则为0
例如:INDEX_CONCISE_MODE or INDEX_VIEW_CLOSE
0000 0000 0000 0000 0000 0111 0000 0011 or 0000 0000 0000 0000 0000 0000 0100 0000,得到
0000 0000 0000 0000 0000 0111 0100 0011
【3】与运算:相同位进行比较,两位同时为 1,结果才为 1,否则为 0
0000 0000 0000 0000 0000 0000 0000 0011 and 0000 0000 0000 0000 0000 0000 0000 0001,得到
0000 0000 0000 0000 0000 0000 0000 0001
【4】异或运算:相同位进行比较,相同为 0 不同为 1
例如:(INDEX_CONCISE_MODE or INDEX_VIEW_CLOSE) xor INDEX_VIEW_CLOSE
0000 0000 0000 0000 0000 0111 0100 0011 or 0000 0000 0000 0000 0000 0000 0100 0000,得到
0000 0000 0000 0000 0000 0111 0000 0011