开发应用的过程中,首页的控件越来越多,布局文件的代码已经到了爆表的程度,而且不同状态下首页各个控件的 Visibility 不同,每次新增状态都是一件头疼的事情,时常遗漏控件导致出错,和 YYY 大佬交流讨论后他给出了一种巧妙的方案,特此学习记录一下!
一、多状态布局
此处的多状态布局是指一个约束布局中,有很多的子布局和控件(Demo 中仅使用控件,嵌套子布局效果也是一样的),这些布局和控件根据首页状态的不同,各自的显示隐藏状态也不同,形成了不同的布局呈现。
二、实现思路
(一)方案简述
- 需求是根据首页的状态不一样,触发不同的控件的隐藏显示状态的改变。
- 可以考虑使用 0 和 1 来表示某个控件的隐藏显示,那么一个控件就可以使用一个 bit 来控制,0 表示隐藏 1 表示显示,多个控件的状态组合在一起成为了一串 0/1 二进制码。
- 考虑到首页控件的数量 Demo 使用 Int 类型(Int 类型在 kotlin 中是 32 位,可以表示 32 个控件的可见性状态),如果控件数量过多则可以考虑使用 Long 类型。
- 不同的 Int 值表示不同的首页状态,状态改变时更新当前状态的 Int 值,首页布局随之发生改变。
(二)具体设计
- 按照顺序指定 Int 的每一位代表的首页控件,先考虑某一个控件单独显示,其他控件均隐藏的情况
位数 | 控件 | 二进制码 | 十进制值 |
---|
低位第一位 | 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
- 首页各个控件单独的可见状态我们已经表示完毕,那首页的不同状态该如何表示?
假设,我们现在要求首页进入全屏状态,但是希望能够保留作者姓名和作者简介,那么我们的页面状态是 0000 0000 0000 0000 0000 0000 0000 0011 如果要求点击作者姓名进入简洁模式,即显示作者姓名、作者简介、Tab 1、Tab 2 和 Tab 3,隐藏关闭按钮,文件夹按钮,工具箱按钮,放大缩小按钮和中心安卓图标,那么我们的页面状态是 0000 0000 0000 0000 0000 0111 0000 0011 观察发现,其实就是需要展示的控件彼此之间做一下位运算中的或运算[2]
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.)使用一个类来统一管理首页的状态和展示
三、Demo 代码
GitHub 代码 https://github.com/NicholasHzf/LayerVisibility
(一)布局文件
<?xml version="1.0" encoding="utf-8"?>
<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
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
|