Using Flow, Layer, ConstraintHelper, and ImageFilterView in Android ConstraintLayout
This article explains how to use Android ConstraintLayout helper widgets such as Flow, Layer, ConstraintHelper, and ImageFilterView, covering their properties, wrap modes, XML configuration, Kotlin animation code, and runtime adjustments with SeekBar to create dynamic UI layouts.
Flow is a virtual layout helper that arranges referenced views in a chain horizontally or vertically without adding them to the layout hierarchy. It supports three wrap modes: none , chain , and aligned .
<androidx.constraintlayout.helper.widget.Flow
android:layout_width="match_parent"
android:layout_height="0dp"
app:constraint_referenced_ids="tvZi,tvChou,tvYin,tvMao,tvChen,tvSi,tvWu,tvWei"
app:flow_horizontalGap="8dp"
app:flow_verticalGap="10dp"
app:flow_wrapMode="aligned" />Layer is another helper that creates a virtual layer to apply the same transformation (rotate, translate, scale) to multiple views simultaneously. It does not perform layout but groups views for collective animation.
<androidx.constraintlayout.helper.widget.Layer
android:id="@+id/layer"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/corner_white"
app:constraint_referenced_ids="tvZi,tvChou,tvYin,tvMao"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />The following Kotlin code demonstrates a simple translation animation applied to the Layer helper.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_layer)
layerDemo()
}
/** Execute layer animation */
private fun layerDemo() {
val layer = findViewById<Layer>(R.id.layer)
val valueAnimator = ValueAnimator.ofFloat(0f, 60f, -30f, 0f, -20f, 0f, -8f, 0f)
valueAnimator.addUpdateListener(AnimatorUpdateListener { animation ->
if (animation.animatedValue == null) return@AnimatorUpdateListener
val animatedValue = animation.animatedValue.toString().toFloat()
layer.translationX = animatedValue
})
valueAnimator.duration = 800
valueAnimator.start()
}Both Flow and Layer inherit from ConstraintHelper , which holds references to the specified views and provides callbacks before and after layout.
Using ConstraintHelper , a custom CircularAnimationHelper can be created to apply circular reveal animations to a group of views.
class CircularAnimationHelper @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ConstraintHelper(context, attrs, defStyleAttr) {
// Called after onLayout; run the animation here
override fun updatePostLayout(container: ConstraintLayout) {
super.updatePostLayout(container)
val views = getViews(container)
views.forEach {
val createCircularReveal = ViewAnimationUtils.createCircularReveal(
it, it.width / 2, it.height / 2, 0f,
hypot((it.height / 2).toDouble(), (it.width / 2).toDouble()).toFloat()
)
createCircularReveal.duration = 1000 + Random.nextLong(500) * Random.nextInt(5)
createCircularReveal.start()
}
}
}ImageFilterView, a subclass of AppCompatImageView , can display, combine, and filter images. It offers attributes such as altSrc , saturation , brightness , warmth , contrast , crossfade , round , roundPercent , and overlay to control visual effects.
Attribute
Meaning
altSrc
Alternative image for fade‑in/out transitions
saturation
Image saturation (0 = grayscale, 1 = original, 2 = oversaturated)
brightness
Image brightness (0 = black, 1 = original, 2 = double brightness)
warmth
Apparent color temperature (1 = neutral, 2 = warm, 0.5 = cool)
contrast
Contrast level (1 = unchanged, 0 = gray, 2 = high contrast)
crossfade
Blend between src and altSrc (0 = src, 1 = altSrc)
round
Corner radius in dimension units
roundPercent
Corner radius as a percentage (0‑1, 1 = circle)
overlay
Whether the alternate image overlays (true) or cross‑fades (false)
The following XML places two ImageFilterView instances and a SeekBar that drives their properties at runtime.
// activity_image_filter.xml
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/iv_1"
android:layout_width="120dp"
android:layout_height="120dp"
app:srcCompat="@mipmap/cat" />
<androidx.constraintlayout.utils.widget.ImageFilterView
android:id="@+id/iv_7"
android:layout_width="120dp"
android:layout_height="120dp"
app:altSrc="@drawable/tiger"
app:srcCompat="@drawable/cat" />
<androidx.appcompat.widget.AppCompatSeekBar
android:id="@+id/seekbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:max="100"
android:progress="0"
app:layout_constraintBottom_toBottomOf="parent" />The activity code listens to SeekBar changes and updates the ImageFilterView attributes accordingly.
class ImageFilterActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_image_filter)
val iv1: ImageFilterView = findViewById(R.id.iv_1)
// ... other ImageFilterView references omitted ...
val seekBar: SeekBar = findViewById(R.id.seekbar)
seekBar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
if (fromUser) {
val realProgress = (progress / 100.0).toFloat()
iv1.saturation = realProgress * 10
iv2.brightness = 1 - realProgress
iv3.warmth = realProgress * 20
iv4.contrast = realProgress * 2
iv5.round = realProgress * 100
iv6.roundPercent = realProgress
iv7.crossfade = realProgress
}
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
})
}
}These helpers enable developers to build complex, responsive, and animated UI layouts in Android with concise XML and Kotlin code.
New Oriental Technology
Practical internet development experience, tech sharing, knowledge consolidation, and forward-thinking insights.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.