androidx / constraintlayout

ConstraintLayout is an Android layout component which allows you to position and size widgets in a flexible way
Apache License 2.0
1.07k stars 176 forks source link

MotionLayout not recyclable as a child of RecyclerView #472

Open fanjavaid opened 2 years ago

fanjavaid commented 2 years ago

Hi, i try to implement programmatically version of MotionLayout by extending it. And i have a base activity ayout using RecyclerView. However, when i add my motion layout as an item of the RecyclerView, the view is not recycled when i try to scrolling up and down. And it works well when i use as a normal view (act as single view).

Here is the preview:

motion_layout_recycler_view

class SimpleMotionLayout @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : MotionLayout(context, attrs, defStyleAttr) {

    private var _simpleTransition: MotionScene.Transition? = null

    init {

    }

    override fun onFinishInflate() {
        super.onFinishInflate()
        setMotion()
    }

    fun setMotion() {
        initDefaultConstraint(this)

        val motionScene = MotionScene(this)
        _simpleTransition = createPlaceholderTransition(motionScene)

        /**
         * The order matters here.
         * [MotionScene.addTransition] adds the transition to the scene while
         * [MotionScene.setTransition] sets the transition to be the current transition.
         */
        motionScene.addTransition(_simpleTransition)
        motionScene.setTransition(_simpleTransition)

        scene = motionScene

        setTransition(_simpleTransition!!.id)

        setDebugMode(DEBUG_SHOW_PATH)

        animateView()
    }

    private fun initDefaultConstraint(motionLayout: ConstraintLayout) {
        // View
        val squareView = View(context).apply {
            id = R.id.default_button
            setBackgroundColor(Color.BLACK)
        }
        motionLayout.addView(
            squareView,
            LayoutParams(
                fromDp(context, 52),
                fromDp(context, 52)
            )
        )

        val set = ConstraintSet()
        set.clone(motionLayout)

        // Setup constraint set to TOP, LEFT to the Parent
        set.connect(
            squareView.id,
            TOP,
            PARENT_ID,
            TOP
        )
        set.connect(
            squareView.id,
            START,
            PARENT_ID,
            START
        )

        set.applyTo(motionLayout)
    }

    private fun setToEnd() {
        val endSet = getConstraintSet(_simpleTransition?.endConstraintSetId ?: return)
        endSet.clear(R.id.default_button, START)
        endSet.connect(
            R.id.default_button,
            END,
            PARENT_ID,
            END
        )
    }

    fun animateView() {
        setToEnd()
        _simpleTransition?.setOnSwipe(
            OnSwipe().apply {
                dragDirection = DRAG_END
                touchAnchorId = R.id.default_button
                touchAnchorSide = SIDE_START
                onTouchUp = ON_UP_AUTOCOMPLETE_TO_START
                setMaxAcceleration(500)
            }
        )
        setTransition(_simpleTransition!!.id)
    }

    // Placeholder transition??
    private fun createPlaceholderTransition(motionScene: MotionScene): MotionScene.Transition? {
        val startSetId = View.generateViewId()
        val startSet = ConstraintSet()
        startSet.clone(this)

        val endSetId = View.generateViewId()
        val endSet = ConstraintSet()
        endSet.clone(this)

        val transitionId = View.generateViewId()

        return TransitionBuilder.buildTransition(
            motionScene,
            transitionId,
            startSetId, startSet,
            endSetId, endSet
        )
    }

    /**
     * Get px from dp
     */
    private fun fromDp(context: Context, inDp: Int): Int {
        val scale = context.resources.displayMetrics.density
        return (inDp * scale).toInt()
    }
}

Am i missing implementation? Thank you

jafu888 commented 2 years ago

No idea what you mean by "the view is not recycled" The Implementer is responsible for "recycling" as part of onBindViewHolder.

Thought experiment: There can be a infinite list of items held in this recycler view. Each with with a unique Progress value where do the values get stored? You update the state with only holder.setColor(items[position]). You will need to save and restore the stat.

This has nothing to do with MotionLayout you would have the same problem if you used SeekBar.

liuchunguang-git commented 2 years ago

I have the same issue. When I use xml layout and motion scene file, everything works. But when try to do it all in code, it works at at the beginning. But after the cell is recycled and bound to a new item, all the constraints and animations stop working. Here is my testing code.

package com.example.recyclersample.flowerList

import android.content.Context import android.util.AttributeSet import android.view.View import android.widget.ImageView import android.widget.TextView import androidx.constraintlayout.motion.widget.MotionLayout import androidx.constraintlayout.motion.widget.MotionScene import androidx.constraintlayout.motion.widget.TransitionBuilder import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import com.example.recyclersample.data.Flower

class FlowerMotionLayout(context: Context, attr: AttributeSet?) : MotionLayout(context, attr) { private val illustration: ImageView = ImageView(context) private val primaryLeading: TextView = TextView(context) private val primaryLeadingAux: TextView = TextView(context) private val primaryTrailing: TextView = TextView(context) private val primaryTrailingAux: TextView = TextView(context) private val secondaryLeading: TextView = TextView(context) private val seconaryTrailing: TextView = TextView(context) private val tertiary: TextView = TextView(context)

private val bgView = View(context)

init {
    populateLayout()
}

override fun addView(view: View) {
    view.id = View.generateViewId()
    super.addView(view)
}

var big = true;
fun toggle() {
    big = !big
    progress = if (big) 1f else 0f
}

fun bindView(flower: Flower) {
    flower.image?.let {
        illustration.setImageResource(flower.image)
    }

    primaryLeading.text = flower.name
    primaryLeading.textSize = 26f

    primaryLeadingAux.text = ""
    primaryLeadingAux.textSize = 18f

    primaryTrailing.text = flower.id.toString()
    primaryTrailing.textSize = 22f

    secondaryLeading.text = flower.description
    secondaryLeading.textSize = 22f

    seconaryTrailing.text = ""
    seconaryTrailing.textSize = 20f

    tertiary.text = "nice"
    tertiary.textSize = 16f
}

private fun populateLayout() {
    // add all the child views
    addView(illustration)
    addView(primaryLeading)
    addView(primaryLeadingAux)
    addView(primaryTrailing)
    addView(primaryTrailingAux)
    addView(secondaryLeading)
    addView(seconaryTrailing)
    addView(tertiary)
    addView(bgView)
    bgView.layoutParams = LayoutParams(ConstraintLayout.LayoutParams.MATCH_CONSTRAINT, ConstraintLayout.LayoutParams.MATCH_CONSTRAINT)

    val scene = MotionScene(this)

    val startSetId = View.generateViewId()
    val startSet = addStartConstraints()

    val endSetId = View.generateViewId()
    val endSet = addEndConstraints()

    val transitionId = View.generateViewId()
    val transaction = TransitionBuilder.buildTransition(
        scene,
        transitionId,
        startSetId, startSet,
        endSetId, endSet
    )

    transaction.duration = 500

    scene.addTransition(transaction)
    scene.setTransition(transaction)
    setScene(scene)

    setTransition(transitionId)
}

private fun addCommonConstraints(set: ConstraintSet) {
    set.constrainWidth(illustration.id, 100)
    set.constrainHeight(illustration.id, 100)
    set.connect(bgView.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START)
    set.connect(bgView.id, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP)
    set.connect(bgView.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM)
    set.connect(bgView.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)

    set.createVerticalChain(ConstraintSet.PARENT_ID, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM, intArrayOf(primaryLeading.id, secondaryLeading.id, tertiary.id), null, ConstraintSet.CHAIN_PACKED,)
    set.connect(illustration.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START)
    set.connect(illustration.id, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP)
    set.connect(illustration.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM)

    set.connect(primaryLeadingAux.id, ConstraintSet.BASELINE, primaryLeading.id, ConstraintSet.BASELINE)
    set.connect(primaryLeadingAux.id, ConstraintSet.START, primaryLeading.id, ConstraintSet.END)

    set.connect(primaryTrailing.id, ConstraintSet.BASELINE, primaryLeading.id, ConstraintSet.BASELINE)
    set.connect(primaryTrailing.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)

    set.connect(primaryTrailingAux.id, ConstraintSet.BASELINE, primaryLeading.id, ConstraintSet.BASELINE)
    set.connect(primaryTrailingAux.id, ConstraintSet.END, primaryTrailing.id, ConstraintSet.START)

    set.connect(secondaryLeading.id, ConstraintSet.START, primaryLeading.id, ConstraintSet.START)

    set.connect(seconaryTrailing.id, ConstraintSet.BASELINE, secondaryLeading.id, ConstraintSet.BASELINE)
    set.connect(seconaryTrailing.id, ConstraintSet.END, primaryTrailing.id, ConstraintSet.END)

    set.connect(tertiary.id, ConstraintSet.START, primaryLeading.id, ConstraintSet.START)
    set.connect(tertiary.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM)
}

private fun addStartConstraints() : ConstraintSet {
    val startSet = ConstraintSet()
    startSet.clone(this)

    // add start constraints
    addCommonConstraints(startSet)
    startSet.connect(primaryLeading.id, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP)
    startSet.connect(primaryLeading.id, ConstraintSet.START, illustration.id, ConstraintSet.END, 32)
    startSet.setAlpha(bgView.id, 0f)
    startSet.setVisibility(tertiary.id, GONE)

    return startSet
}

private fun addEndConstraints() : ConstraintSet {
    val endSet = ConstraintSet()
    endSet.clone(this)

    // add end constraints
    addCommonConstraints(endSet)
    endSet.connect(illustration.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)
    endSet.clear(illustration.id, ConstraintSet.BOTTOM)
    endSet.connect(primaryLeading.id, ConstraintSet.TOP, illustration.id, ConstraintSet.BOTTOM)
    endSet.connect(primaryLeading.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START, 32)
    endSet.constrainWidth(illustration.id, 250)
    endSet.constrainHeight(illustration.id, 250)
    endSet.setAlpha(bgView.id, 1f)
    return endSet
}

}

I'm not sure what is missing. Thanks!