Android ConstraintLayout 约束布局 2.0

12,657 阅读6分钟

一 约束布局2.x新特性简介

What's New in ConstraintLayout (Google I/O'19)

2020.06.28更新: Flow使用详解

约束布局2.0未使用过约束布局的,可先查看上一篇文章ConstraintLayout 约束布局1.x

implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta3'

约束布局是一个允许你灵活定义view位置和大小的ViewGroup,具有多种辅助工具,如GuideLine、Barrier、Group等。在灵活地放置各种各样的view时,并不会增加Layout层级。2.0版本出了优化布局性能外,还增加了一些新特性,使得开发过程更加方便:

  • ConstraintHelper辅助工具的增加:Layer,flow
  • ConstraintHelper的自定义开放
  • ConstraintLayoutStates 界面状态切换控制
  • API使用优化
  • **MotionLayout **构建一个动态的布局

开发实际发现,到现在竟然还有些人看不上约束布局,Google对于约束布局的信心和野心,难道还不足以引起重视?? img-092dc569072ff195402cad0b6341b62a.jpg

二 ConstraintHelper辅助工具

2.1 Layer

Layer功能上可以理解为包含它所引用的view的一个父布局viewGroup,但并不会增加layout的层级。这点是非常好用的,在Layer之前,想往自己view统一加个背景限制,一般都是另外加个view来做纯背景展示。

像开发过程中这种带图标的dialog,可以用layer方便圈出背景。 image.png

另外,Layer支持对里面的 view 一起做变换,可看待成一个平常的父布局,一起做变换,设置visibility等(Layer本身也是继承自view)。

  ...

	<androidx.constraintlayout.helper.widget.Layer
        android:id="@+id/mLayer"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@drawable/shape_pet_white_with_corner"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toBottomOf="@id/mRecyclerView"
        app:constraint_referenced_ids="mTvTitle,mRecyclerView,guide_line"
        />

	...

2.2 自定义 ConstraintHelper

自定义ConstraintHelper(简称Helper),可以用来封装针对ui的一些固定行为,方便以后复用。而且,一个view又可以同时被多个helper所引用,可以很便捷地组合出多种效果。

注:Helper本身也是继承了view的

  1. Helper提供了getViews()方法获取所引用的所有view
  2. Helper提供了view的onLayout()/onMeasure()等流程方法执行前后的回调,如:updatePostLayout(container: ConstraintLayout?),onLayout()后 ;updatePreLayout(container: ConstraintLayout?) ,onLayout()前; 可利用这些方法和获得的view,封装一些通用操作
  3. 再应用到布局文件中,声明要包含的view的id:app:constraint_referenced_ids="xxx,xxx"
class EnterAnimationHelper @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ConstraintHelper(context, attrs, defStyleAttr) {

    override fun updatePostLayout(container: ConstraintLayout?) {
        super.updatePostLayout(container)
        val views = getViews(container)
        views.forEach {
            startEnterAnimation(it)
        }
    }

    private fun startEnterAnimation(view: View) {
        val translationY = -100f


        view.translationY = translationY
        val translationYHolder = PropertyValuesHolder.ofFloat(
            View.TRANSLATION_Y,
            translationY,
            0f
        )

        val keyFrame1 =
            Keyframe.ofFloat(0f, -6f)
        val keyFrame2 =
            Keyframe.ofFloat(0.6f, 25f)
        val keyFrame3 =
            Keyframe.ofFloat(1f, 0f)
        val rotateHolder =
            PropertyValuesHolder.ofKeyframe(View.ROTATION, keyFrame1, keyFrame2, keyFrame3)

        ObjectAnimator.ofPropertyValuesHolder(
            view,
            translationYHolder,
            rotateHolder
        )
            .apply { interpolator = AnticipateOvershootInterpolator() }
            .setDuration(600L)
            .start()
    }
}
...

 <com.cyq.x622.constraintlayoutsample.widget.EnterAnimationHelper
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:constraint_referenced_ids="xxx,xxx"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
    />

...

效果如下(此处是封装了一个,上下位移和左右旋转的动画效果): helper.gif

三 ConstraintLayoutStates

ConstraintLayoutStates可以创建具有不同状态的布局并在它们之间轻松切换。通常,一个界面包含有加载状态,加载成功状态以及加载失败状态。利用ConstraintLayoutStates,可以很方便地在已定义好的状态之间相互切换。

3.1 创建不同状态的布局文件

根据需要,创建不同状态下的布局文件。每一个文件必须有相同的view,只有属性值,如visibility,和定位方式等可以不相同(简单起见,demo就定义了加载状态和成功状态,都是只含有一个ProgressBar和一个TextView,id都相同,只有visibility不同)

image.png

3.2 创建状态声明XML文件

在xml资源文件夹中,创建一份xml文件,定义了layout可拥有的所有状态constraint_set_test.xml:

<?xml version="1.0" encoding="utf-8"?>
<ConstraintLayoutStates xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <State
        android:id="@+id/loading"
        app:constraints="@layout/activity_state_start"/>

    <State
        android:id="@+id/success"
        app:constraints="@layout/activity_state_end"/>
</ConstraintLayoutStates>

3.3 加载声明的状态文件并在不同状态之间切换

在activity/fragment中,在要应用状态的ConstraintLayout上 使用loadLayoutDescription(),加载定义好的状态xml文件声明。然后便可以直接调用constraintLayout.setState()来切换状态.

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_state_loading)

        mStateConstraintLayout.loadLayoutDescription(R.xml.constraint_set_test)

        //简单延时,模拟io操作
        mStateConstraintLayout.postDelayed({
            mTv.text = "加载完成"
            mStateConstraintLayout.setState(R.id.success,0,0) //xml中定义的id
        },4_000L)

    }

效果如下: state.gif

四 流式API

2.0以后,对属性的修改提供了流式API。

        ConstraintProperties(mBtnLayer)
            .alpha(0.5f)
            .margin(ConstraintSet.TOP, 100)
            .apply()

这部分比较简单,可以直接查看官方文档ConstraintProperties

五 简易demo

附件:constraintLayout.zip

六 Flow

Flow是一个特别强大的布局辅助工具,支持多种布局模式,可以快速构造多样性布局。

Flow部分的demo及文中结构大部分翻译自于Medium:Awesomeness of ConstraintLayout Flow

尽管约束布局已经特别强大,可以快速建立约束关系展示布局,如下面两个水平关系view 1_IwFYf6yetL5KkD_t8sUN2Q.png

两个水平view

但如果要构建8个不同行的view,且相互之间有约束关系: 1_splakOAa-xL4wGu6evuRJg.png

每行之间间隔均分

当然,使用三条约束链便可以实现,但写法上太过于繁琐,尤其是当垂直方向上还要设置对应关系时,更加繁琐。

这时候Flow就可以派上用场了。

Flow可以看成一个具有多种约束功能的流式布局,当空间不足时,能按设定的方式自动换行对齐。

Flow在使用上,有多个属性可以控制布局约束关系:

  • orientation: horizontal  或者 vertical
  • WrapMode
  • Gap
  • Styles
  • Bias
  • Alignment

6.1 orientation

布局方向:水平horizonta l或 垂直vertical

setOrientation(int orientation) android:orientation="horizontal|vertical"

image.png

水平布局

image.png

垂直布局

6.2 WrapMode

WrapMode属性决定了Flow将如何控制所引用的views的布局方法:NONECHAINALIGN

app:flow_wrapMode = " none | chain | aligned "

flow.setWrapMode(  Flow.WRAP_NONE | Flow.WRAP_CHAIN | Flow.WRAP_ALIGNED )

NONE:默认值,空间不够情况下Views不会被自动换到另一行/列,直接超出屏幕范围。 1_hxvRedZkqouW6uWQH8ej4Q.png

Wrap Mode = NONE

CHAIN:该模式与约束布局的链式chain布局相似,不仅可以实现相同的效果,还会额外的自动换行/换列处理

【注】:与约束布局链式布局一样,CHAIN模式也有链式style:SPREAD(默认值),PACKEDSPREAD_INSIDE

1_splakOAa-xL4wGu6evuRJg.png

Wrap Mode = CHIAN 且 style = SPREAD

ALIGNED:该模式与上面的CHAIN相同,但额外增加了对齐方式

【注】:与CHAIN模式一样也有链式style:SPREAD(默认值),PACKEDSPREAD_INSIDE 1_jt4vaw3aJFEIl0RiO9JPOg.png

Wrap Mode = ALIGNED 且 style = SPREAD

6.3 Gap

Gap是放置views时的水平和垂直间隔。 app:flow_horizontalGap  app:flow_verticalGap flow.setVerticalGap flow.setHorizontalGap 因为比较简单,就不放图了,手动试试就知道了。

6.4 Styles

当wrapMode为chain或ALIGNED时生效。Flow的styles跟约束布局之前的基础链式布局style是一个概念,有SPREAD(默认值),PACKEDSPREAD_INSIDE。不了解的可先查看上一篇:约束布局的链式布局

app:flow_horizontalStyle = “ spread | spread_inside | packed ” app:flow_verticalStyle = “ spread | spread_inside | packed ”

应用到Flow中,效果如下: 1_splakOAa-xL4wGu6evuRJg (1).png

spread

1_C9sSFE0H8P9rtvHfneJiGw.png

Spread Inside

1_Ds-oG-rUkhZTPZvKNI2gDw.png

packed

6.5 Bias

flow的bias偏移,只在style为packed时生效,因为当style为spread或者spread_inside时,views是均匀分布的,bias无法起到作用。float值,范围为 0-1

app:flow_horizontalBias = “ float "

app:flow_verticalBias = “ float "

flow.setHorizontalBias( float)

flow.setVerticalBias( float)

这里只取两个端点值,0和1,方便理解。

image.png

bias为0,贴到最左边

image.png

bias为1,贴到最右边

6.6 Alignment

Alignment对齐方式,同样也有水平和垂直。Alignment的对齐方向,与flow的方向必须是相反的才能生效。比如当flow的方向是水平时,Alignment只有设为垂直才有效。views是水平放置,对齐是view与view之间在垂直方向上的对齐方式。关于这个属性,可以运行demo多试几遍理解理解。

app:flow_verticalAlignment = “ top | center | bottom | baseline ”

app:flow_horizontalAlignment = “ start| end ”

flow.setVerticalAlignment(
Flow.VERTICAL_ALIGN_TOP | Flow.VERTICAL_ALIGN_CENTER | Flow.VERTICAL_ALIGN_BOTTOM | Flow.VERTICAL_ALIGN_BASELINE )

flow.setHorizontalAlignment(
Flow.HORIZONTAL_ALIGN_START | Flow.HORIZONTAL_ALIGN_END )

1_k0u9hxx6pTbmlycxXtLwsg.png

Vertical Alignment = Bottom

6.7 Flow demo

flow的demo并非远程,可参考ConstraintFlowPlayground

后期计划MotionLayout

MotionLayout是2.x版本的一个重要的更新,尤其是MotionLayout构建动态布局。下期再计划编写了