Android 学习之多状态布局的一种实现方案

开发应用的过程中,首页的控件越来越多,布局文件的代码已经到了爆表的程度,而且不同状态下首页各个控件的 Visibility 不同,每次新增状态都是一件头疼的事情,时常遗漏控件导致出错,和 YYY 大佬交流讨论后他给出了一种巧妙的方案,特此学习记录一下!

一、多状态布局

此处的多状态布局是指一个约束布局中,有很多的子布局和控件(Demo 中仅使用控件,嵌套子布局效果也是一样的),这些布局和控件根据首页状态的不同,各自的显示隐藏状态也不同,形成了不同的布局呈现。
布局展示

二、实现思路

(一)方案简述

  1. 需求是根据首页的状态不一样,触发不同的控件的隐藏显示状态的改变。
  2. 可以考虑使用 0 和 1 来表示某个控件的隐藏显示,那么一个控件就可以使用一个 bit 来控制,0 表示隐藏 1 表示显示,多个控件的状态组合在一起成为了一串 0/1 二进制码。
  3. 考虑到首页控件的数量 Demo 使用 Int 类型(Int 类型在 kotlin 中是 32 位,可以表示 32 个控件的可见性状态),如果控件数量过多则可以考虑使用 Long 类型。
  4. 不同的 Int 值表示不同的首页状态,状态改变时更新当前状态的 Int 值,首页布局随之发生改变。

(二)具体设计

  1. 按照顺序指定 Int 的每一位代表的首页控件,先考虑某一个控件单独显示,其他控件均隐藏的情况
位数控件二进制码十进制值
低位第一位tv_author_name0000 0000 0000 0000 0000 0000 0000 00011
低位第二位tv_author_introduction0000 0000 0000 0000 0000 0000 0000 00102
低位第三位tv_tool_box0000 0000 0000 0000 0000 0000 0000 01004
低位第四位tv_folder0000 0000 0000 0000 0000 0000 0000 10008
低位第五位iv_zoom_in0000 0000 0000 0000 0000 0000 0001 000016
低位第六位iv_zoom_out0000 0000 0000 0000 0000 0000 0010 000032
低位第七位iv_close0000 0000 0000 0000 0000 0000 0100 000064
低位第八位iv_android0000 0000 0000 0000 0000 0000 1000 0000128
低位第九位tv_tab_first0000 0000 0000 0000 0000 0001 0000 0000256
低位第十位tv_tab_second0000 0000 0000 0000 0000 0010 0000 0000512
低位第十一位tv_tab_third0000 0000 0000 0000 0000 0100 0000 00001024

观察发现,其实就是第一位的 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
  1. 首页各个控件单独的可见状态我们已经表示完毕,那首页的不同状态该如何表示?
    假设,我们现在要求首页进入全屏状态,但是希望能够保留作者姓名和作者简介,那么我们的页面状态是 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
  1. 现在可以通过控件显示或者隐藏来决定当前布局的状态,那么反过来当拿到布局状态,如何确定这个状态下,各个控件的可见性情况?
    答案是使用位运算中的与运算[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 表示作者姓名是显示的

  2. 现在可以表示不同状态下的首页布局的情况了,那么还需要考虑的就是不同状态的切换了
    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

  3. 到目前为止,情况基本上都考虑完善了,接下来就是实现上需要注意的地方:
    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

/**
 * @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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值