如何将 TextView 宽度从 Visible 设置为 Gone 状态?

How to animate TextView width from Visible to Gone state?

我想在更改 TextView 的可见性时设置 TextView 宽度的动画。我不想实现通用的“淡入淡出 in/out”效果,但我想从侧面折叠 TextView 到 0 宽度。

这是我的功能:

fun fadeInTextViewSize(){
    val parentWidth = (buttonText.parent as View).measuredWidth
    val widthAnimator = ValueAnimator.ofInt(buttonText.width, parentWidth)
    widthAnimator.duration = 500
    widthAnimator.interpolator = DecelerateInterpolator()
    widthAnimator.addUpdateListener { animation ->
        buttonText.layoutParams.width = animation.animatedValue as Int
        buttonText.requestLayout()
    }
    widthAnimator.start()
}

fun fadeOutTextViewSize(){
    val widthAnimator = ValueAnimator.ofInt(buttonText.width, 0)
    widthAnimator.duration = 500
    widthAnimator.interpolator = DecelerateInterpolator()
    widthAnimator.addUpdateListener { animation ->
        buttonText.layoutParams.width = animation.animatedValue as Int
        buttonText.requestLayout()
    }
    widthAnimator.start()
}

问题是使用此功能时,我的 TextView 身高出于某种原因设置为 MATCH_PARENT

我假设您需要 TextView 看起来就像从两端均等地擦除一样。这是一种可以做到这一点的技术:

private lateinit var buttonText: TextView
private var viewWidth = 0

fun fadeOutTextViewSize() {
    with(buttonText) {
        // Enable scrolling for the view since we will need to scroll horizontally to center text.
        setMovementMethod(ScrollingMovementMethod())
        setHorizontallyScrolling(true)

        // Lock in the starting width and height.
        viewWidth = buttonText.width
        layoutParams.width = buttonText.width
        layoutParams.height = buttonText.height

        visibility = View.VISIBLE
    }

    with(ValueAnimator.ofInt(viewWidth, 0)) {
        duration = 1000
        interpolator = DecelerateInterpolator()

        addUpdateListener { animation ->
            val newWidth = animation.animatedValue as Int
            buttonText.layoutParams.width = newWidth

            // Shift text left so it stays centered in the initial bounds.
            val scrollX = (viewWidth - newWidth) / 2
            buttonText.scrollTo(scrollX, 0)
            buttonText.requestLayout()
        }
        doOnEnd {
            // Make the view invisible to seal its disappeared status. This could also be
            // "GONE" depending on the desired effect.
            buttonText.visibility = View.INVISIBLE
        }
        start()
    }
}

技术是将 TextView 的大小和高度锁定为其初始大小。从该初始大小开始,动画将视图的宽度从初始大小缩小为零。由于布局会将文本的开头定位到视图的开头,因此文本会向左滚动以在宽度缩小时保持其在屏幕上的位置。

红线只是用来标记 TextView 的中心,不需要。

使视图重新出现与此处的代码主要相反。

测试布局:

<androidx.constraintlayout.widget.ConstraintLayout 
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/holo_blue_dark"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/buttonText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@android:color/holo_blue_light"
        android:text="Hello World!"
        android:textSize="48sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <View
        android:layout_width="1dp"
        android:layout_height="0dp"
        android:background="@android:color/holo_red_light"
        app:layout_constraintBottom_toBottomOf="@id/buttonText"
        app:layout_constraintEnd_toEndOf="@id/buttonText"
        app:layout_constraintStart_toStartOf="@id/buttonText"
        app:layout_constraintTop_toTopOf="@id/buttonText" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text="Click Here"
        app:layout_constraintEnd_toEndOf="@+id/buttonText"
        app:layout_constraintStart_toStartOf="@id/buttonText"
        app:layout_constraintTop_toBottomOf="@id/buttonText" />
</androidx.constraintlayout.widget.ConstraintLayout>


还有另一种方法可以实现此目的,即使用自定义 TextView 剪辑其 canvas 以使视图缩小和展开。这种方法有以下优点:

  • 它可能更有效,因为它不需要 TextView 的额外布局。
  • 如果 TextViewConstraintLayout 内,并且有其他视图被限制在 and/or 的开始 and/or 结束TextView,这些视图不会移动,因为 TextView 的边界保持不变。
private lateinit var buttonText: ClippedTextView 
fun fadeOutTextViewSize() {
    with(ValueAnimator.ofInt(buttonText.width, 0)) {
        duration = 1000
        interpolator = DecelerateInterpolator()

        addUpdateListener { animation ->
            val newWidth = animation.animatedValue as Int
            buttonText.setClippedWidth(newWidth)
            buttonText.invalidate()
        }
        start()
    }
}

ClippedTextView.kt

class ClippedTextView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null
) : androidx.appcompat.widget.AppCompatTextView(context, attrs) {

    private var mClipWidth = 0

    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        super.onLayout(changed, left, top, right, bottom)
        mClipWidth = right - left
    }

    override fun draw(canvas: Canvas) {
        val sideClipWidth = (width - mClipWidth) / 2
        canvas.withClip(sideClipWidth, 0, width - sideClipWidth, height) {
            super.draw(this)
        }
    }

    fun setClippedWidth(clipWidth: Int) {
        mClipWidth = clipWidth
    }
}


我们也可以把所有的逻辑都放到自定义的TextView中,如下:

ClippedTextView.kt

class ClippedTextView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null
) : androidx.appcompat.widget.AppCompatTextView(context, attrs) {

    private var mClipWidth = 0
    private val mAnimator: ValueAnimator by lazy {
        ValueAnimator().apply {
            duration = 1000
            interpolator = DecelerateInterpolator()
            addUpdateListener { animation ->
                setClippedWidth(animation.animatedValue as Int)
                invalidate()
            }
        }
    }

    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        super.onLayout(changed, left, top, right, bottom)
        setClippedWidth(right - left)
    }

    override fun draw(canvas: Canvas) {
        val sideClipWidth = (width - mClipWidth) / 2
        canvas.withClip(sideClipWidth, 0, width - sideClipWidth, height) {
            super.draw(this)
        }
    }

    fun expandView() {
        doWidthAnimation(mClipWidth, 0)
    }

    fun shrinkView() {
        doWidthAnimation(mClipWidth, width)
    }

    private fun doWidthAnimation(startWidth: Int, endWidth: Int) {
        animation?.cancel()
        with(mAnimator) {
            setIntValues(startWidth, endWidth)
            start()
        }
    }

    private fun setClippedWidth(clipWidth: Int) {
        mClipWidth = clipWidth
    }
}