动态布局 GridLayout(Kotlin)

前言: 最近项目中需求到一个功能,就是根据后台传过来的数据,在 Android 上进行一个自定义分屏。分屏区域是不定的,分屏区域的大小更是不定的,唯一定的分屏区域的形状肯定是长方形。那这里就想到如同 Excel 合并单元格的模式来进行动态布局。而由于最近 Kotlin 语言大火,本次直接使用到 Kotlin 语言进行实际开发。

动态GridLayout的具体实现方式

一般实现 GridLayout 都是在 xml 文件中进行配置,然后在 GridLayout 对里面的子控件一个一个的写。如果用这个来实现一个简单的计算器布局,估计很累。同样在具体需求下面,这样的每个都进行布局重写根本不符合我们所需要的功能。因此博主就想到,能不能让 GridLayout 子布局动态大小,而且还要为每个子布局设置属性等等,想想就头大不是么,相当复杂了可以说。

这里就想到一个好办法,就是把 GridLayout 看做一个田字格画布,比如说 3x4 的画布,那这个画布上面就有 12 个格子。跟表格类似。就跟在 Excel 上合并单元格一样,只要能合并单元格的区域就可以单独作为一个子区域。那这个单独的子区域上面可以防止任何组件,如:TextView,Button,Fragment….只要你能想到的控件,基本都可以放进去,然后实现深一步的业务。

Kotlin 简介

今天的 Google 大会上,已经正式提出将 Kotlin 语言作为 Android 的开发语言。可以说又是一个 Google 的亲儿子,以后肯定会成为 Android 开发的主流语言。其对于传统语言 Java 的优势这里我就不多累赘了,具体可以看知乎上大神些的回答:Kotlin 作为 Android 开发语言相比传统 Java 有什么优势?

总得来说,用 Kotlin 来开发就会变的很爽。但是毕竟是一个新出语言,市场上,还没有公司正式用该语言进行具体的开发步骤,基本都是大家进行一些个人开发。毕竟很多国内关于 Kotlin 的社区并不是很完善。

本编将在实际操作的过程中顺便介绍 Kotlin 的简单知识,希望对你有所帮助。如遇到不理解的地方,欢迎 Google ,基本上一个 Stackoverflow 社区可以解决你的一些问题(要是访问慢的话试试科学上网吧)。

GridLayout 具体实现步骤

1.动态布局设置

首先我们在布局文件中放入一个 GridLayout 作为主布局,然后在代码中动态设置 GridLayout 的横向纵向数,以及每个格子的控件。

1
2
3
4
5
<GridLayout
android:id="@+id/grid_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>

然后通过代码获取屏幕的长宽,方便后面动态布局设置长宽属性。

1
2
3
4
private var height = 0 //屏幕高度
private var width = 0 //屏幕宽度
private var mainX = 3 //x轴格子数
private var mainY = 4 //Y轴格子数
1
2
3
4
5
6
7
//获取手机屏幕高宽度
val wm: WindowManager = this.windowManager
height = wm.defaultDisplay.height
width = wm.defaultDisplay.width
grid_layout.columnCount = mainX //设置gridlayout 横向格子
grid_layout.rowCount = mainY //设置gridlayout 竖向格子

这里我为了操作方便,是直接把 X 和 Y 轴格子数写死了,就是横向 3 个,竖向 4 个,也就说一共是一个 3 x 4 = 12 的表格。如下图:

img

这里我们可以根据坐标,已经占用的横纵两向的格子数来合并这些格子。

在手机上,坐标跟普通坐标系是有点反着的,既手机的左上角是坐标原点处,往下是 Y 轴延伸方向,往左是 X 轴延伸方向。请忽略我这个灵魂画手。

手机屏幕坐标图

那么现在我们只需要传入 4 个参数,既坐标点 x,y 以及各占用的格子数 xn,xn,就可以在这个 12 表格的 GridLayout 上面将布局画出来。

每次都单独取一个布局元素

1
val par = GridLayout.LayoutParams() //每个子布局单独获取,并分配区域

分配好这个布局所占用的控件位置,以及占用表格大小

1
2
3
4
5
par.rowSpec = GridLayout.spec(y as Int, yn as Int)
par.columnSpec = GridLayout.spec(x as Int, xn as Int)
//根据每个格子占用高宽来计算这个子布局的高宽
par.height = height / mainY * yn
par.width = width / mainX * xn

如果有要在格子间加入线条的需求,可以设置各个子布局间的距离,然后统一设置整个布局背景,就是线条颜色,也可以在子布局中详细设置,这个就见仁见智了。具体需求具体操作。

因为有些格子是靠边的,所以并不需要靠边的一侧空出间隔,所以要进行一个简单的判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
//判断子布局的上下左右是否为边界,是,就不留间隔.
if (0 != y) {
par.topMargin = 1
}
if (0 != x) {
par.leftMargin = 1
}
if (mainX != (x + xn)) {
par.rightMargin = 1
}
if (mainY != (y + yn)) {
par.rightMargin = 1
}

接着把设置的布局元素设置为指定的控件,我这里因为功能需要,都统一成 FrameLayout,然后每个 FrameLayout 都设置一个单独的 Fragment ,方便在独立的 Fragment 中进行逻辑处理。

1
2
3
t.layoutParams = par
grid.addView(t, par)

这里我们把这个设置子布局的内容,单独设立一个方法,方便之后的使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* 设置Fragment组件 跨多少行 跨多少列
* 这里不一样的要设置成 Fragment,其他各种组件都可以自己搭配
*
* @param grid grid控件
* @param t fragment布局
* @param x 组件横向轴开始的索引
* @param y 组件纵向轴开始的索引
* @param xn 组件横向拉升位置 就是占几列
* @param yn 组件列向拉升位置
*/
fun setSpanRowCol(grid: GridLayout, t: FrameLayout, x: Int?, y: Int?, xn: Int?, yn: Int?) {
val par = GridLayout.LayoutParams() //每个子布局单独获取,并分配区域
par.rowSpec = GridLayout.spec(y as Int, yn as Int)
par.columnSpec = GridLayout.spec(x as Int, xn as Int)
par.height = height / mainY * yn
par.width = width / mainX * xn
//判断子布局的上下左右是否为边界,是,就不留间隔.
if (0 != y) {
par.topMargin = 1
}
if (0 != x) {
par.leftMargin = 1
}
if (mainX != (x + xn)) {
par.rightMargin = 1
}
if (mainY != (y + yn)) {
par.rightMargin = 1
}
t.layoutParams = par
grid.addView(t, par)
}

到这里基本就是这个动态设置的思路,需要注意的是,每次添加布局,子布局的大小以及位置不能有冲突,不然子布局会重叠,这个需要添加之前就计算好。

我这边自己手动添加了 2 个集合用来 for循环添加子布局。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
listone = ArrayList()
listone?.add(Data.Single.getInstance(0, 0, 3, 2))
listone?.add(Data.Single.getInstance(0, 2, 3, 2))
listtwo = ArrayList()
listtwo?.add(Data.Single.getInstance(0, 0, 3, 2))
listtwo?.add(Data.Single.getInstance(0, 2, 1, 2))
listtwo?.add(Data.Single.getInstance(1, 2, 2, 2))
for (i in listone!!.indices) {
val data: Data = listone!!.get(i)
val fragmentlayout: FrameLayout = FrameLayout(this)
fragmentLayouts?.add(fragmentlayout)
fragmentlayout.id = View.generateViewId()
setSpanRowCol(grid_layout, fragmentlayout, data.x, data.y, data.xn, data.yn)
addFragment(EasyFragment.Single.getInstance(i.toString()), fragmentlayout.id)
}

基本到这里整个动态 GridLayout 的添加就算完成了,下面主要介绍下本编中用到的 Kotlin 语言的相关知识。

2.Kotlin 简单语法介绍
  1. Kotlin 变量,常量

    变量 var,常量 val 表示,这点比 Java 中简洁多了,跟 JavaScript 类似。

  2. Kotlin 数据初始化

    Kotlin 中的数据类型跟 java 中类似,基础数据类型都包含有。

    而 Kotlin 对于数据或者对象的初始化可以直接通过 :来代替数据类型,也可以直接用 = 直接表明类型,该类型会直接饮用 = 后面的数据类型。

    1
    2
    private height : Int? = null
    private var height = 0 //屏幕高度
  3. Kotlin 空指针

    在Kotlin中空指针异常得到了很好的解决。

    • 在类型上的处理,即在类型后面加上?,即表示这个变量或参数以及返回值可以为null,否则不允许为变量参数赋值为null或者返回null
    • 对于一个可能是null的变量或者参数,在调用对象方法或者属性之前,需要加上?,否则编译无法通过。

    如下面的代码就是Kotlin实现空指针安全的一个例子,而且相对Java实现而言,简直是一行代码搞定的。

    这里不单独对空指针问题进行介绍了,毕竟这个属于学习 Kotlin 中的一个复杂点,关于空指针安全原理,可以参考这篇文章研究学习 Kotlin 的一些方法

  4. Kotlin 的类型转换

    在实际项目中会经常遇到数据类型转换问题,在 Kotlin 中,会经常遇到比如 Int? 转化为 Int ,就可以直接用 as 直接转化

    1
    y as Int

    这一串代码直接将 y 重新赋值成 Int 类型,相当于java中的

    1
    y = Integer.valueOf(y)

    简洁明了。当然 Kotlin 也支持多种转化方式,比如toInt,toString等等。

  5. Kotlin 方法类别

    代码中直接用 fun 就能表面是一个方法,甚至都不需要 function。

    而且直接设置该方法的返回类型,也只需要用 :来表面,比如

    1
    2
    3
    fun parse(url: String): String {
    retrun url
    }

    那这里就必须返回一个 String 类型。

  6. Kotlin 方法扩展

    很多时候,Framework提供给我们的API往往都时比较原子的,调用时需要我们进行组合处理,因为就会产生了一些Util类,一个简单的例子,我们想要更快捷的展示Toast信息,在Java中我们可以这样做。

    1
    2
    3
    4
    >public static void longToast(Context context, String message) {
    > Toast.makeText(context, message, Toast.LENGTH_LONG).show();
    >}
    >

    >

    但是Kotlin的实现却让人惊奇,我们只需要重写扩展方法就可以了,比如这个longToast方法扩展到所有的Context对象中,如果不去追根溯源,可能无法区分是Framework提供的还是自行扩展的。

    1
    2
    3
    4
    5
    >fun Context.longToast(message: String) {
    > Toast.makeText(this, message, Toast.LENGTH_LONG).show()
    >}
    >applicationContext.longToast("hello world")
    >

    >

    注意:Kotlin的方法扩展并不是真正修改了对应的类文件,而是在编译器和IDE方面做得处理。使我们看起来像是扩展了方法。

    本次项目中就使用到了该方法,通常在 BaseActivity 中,扩展一些方法比如正常的添加 fragment ,可以直接类似重写一般的扩展:

    1
    2
    3
    4
    5
    6
    7
    8
    //Fragment 添加
    inline fun FragmentManager.inTransaction(func: FragmentTransaction.() -> FragmentTransaction) {
    beginTransaction().func().commit()
    }
    fun AppCompatActivity.addFragment(fragment: Fragment, frameId: Int) {
    supportFragmentManager.inTransaction { add(frameId, fragment) }
    }
    1
    2
    fun AppCompatActivity.replaceFragment(fragment: Fragment, frameId: Int) {
    supportFragmentManager.inTransaction { replace(frameId, fragment) }

​ 那我们在之后继承了 BaseActivity 的 Activity 中都可以直接调用 addFragment 和 replaceFragment 方法:

1
addFragment(EasyFragment.Single.getInstance(i.toString()), fragmentlayout.id)
  1. Kotlin 单例模式

    我们经常要在一些 class 中使用到单例模式,一般好的 java 中都是这么调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    private static MyPicasso myPicasso;
    private synchronized static void createInstance() {
    if (myPicasso == null)
    myPicasso = new MyPicasso();
    }
    public static MyPicasso getInstance() {
    if (myPicasso == null)
    createInstance();
    return myPicasso;
    }

    通过一个 synchronized 进行一个异步创建单例,但是这样代码很多了就,而且不方便。但是在 Kotlin 中可以直接一个 Object 解决问题。并且可以在这个 object中进行一些具体操作,很方便。我在一个 Fragment 实现单例,并且可以直接传入参数。相当简洁。

    1
    2
    3
    4
    5
    6
    7
    object Single {
    fun getInstance(str: String): EasyFragment {
    val instance: EasyFragment = EasyFragment()
    instance.mStr = str
    return instance
    }
    }
  2. Kotlin 数据序列化

    在 java 中一般都要继承 2 个接口 Serializable 或者 Parcelable ,虽然现在有了工具不需要手写 get 和 set,但是代码量在那边摆着呢,显得很麻烦。

    而在 Kotlin 中,直接一个 data class 就直接搞定,Kotlin 中自动包含了setter 和 getter。

    1
    2
    3
    4
    /**
    * 数据Model
    */
    data class Data(val x: Int, val y: Int, val xn: Int, val yn: Int) {}
总结

通过本次实际程序,基本可以对 Kotlin 的用法有个基本了解了,有不会的地方大可以直接 Google,国外对于该方面的资料还是比国内多很多。欢迎各位的学习讨论。

源码地址

废话不多说,直接上源码:源码地址