基于属性动画,实现 咔嚓截屏(收藏)动画

Posted 夜尽天明89

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于属性动画,实现 咔嚓截屏(收藏)动画相关的知识,希望对你有一定的参考价值。

写在前面:这里的截屏,其实不是真正意义上的截屏。而是把数据先设置到一些控件上,让控件做透明度、缩小、位移等动画。只是看起来像截屏。为了方便描述,下面还是采用截屏这种说法。

需求:做一个咔嚓截屏收藏功能。点击“按钮”后,把屏幕截屏,然后把截下来的内容,做缩小、透明度、位移等动画,飞到按钮上。

因为代码简单,就没放到GitHub上,直接全部放出来了。复制了就能用。

代码中已经有一些注释了,但是有一些地方因为简单的文字描述不清除,需要在博客中详细说。

先来2张简单的效果图

注意最后的说明。
注意最后的说明。
注意最后的说明。

上代码:
1、引入三方CardView,因为项目用需要带阴影,且阴影要自由指定颜色,就找了个三方的

//可以指定阴影颜色的卡片view
implementation 'com.zyp.cardview:cardview:1.0.1'

2、UiUtils

import android.content.Context;
import android.content.res.Resources;

public class UiUtils 

    public static int dip2px(Context context, float dpValue) 
        final float scale = getResources(context).getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    

    private static Resources getResources(Context context) 
        return context.getResources();
    

    public static int getScreenWidth(Context context) 
        return getResources(context).getDisplayMetrics().widthPixels;
    
    
    public static int getScreenHeight(Context context) 
        return getResources(context).getDisplayMetrics().heightPixels;
    


3、因为要做抛物线,需要用到贝塞尔曲线。BezierUtil

import android.graphics.PointF;

public class BezierUtil 

    /**
     * B(t) = (1 - t)^2 * P0 + 2t * (1 - t) * P1 + t^2 * P2, t ∈ [0,1]
     *
     * @param t  力度步长
     * @param p0 起始点
     * @param p1 控制点
     * @param p2 终止点
     * @return t对应的点
     */
    public static PointF getPointFromQuadBezier(float t, PointF p0, PointF p1, PointF p2) 
        PointF point = new PointF();
        float temp = 1 - t;
        point.x = temp * temp * p0.x + 2 * t * temp * p1.x + t * t * p2.x;
        point.y = temp * temp * p0.y + 2 * t * temp * p1.y + t * t * p2.y;
        return point;
    

    /**
     * B(t) = P0 * (1-t)^3 + 3 * P1 * t * (1-t)^2 + 3 * P2 * t^2 * (1-t) + P3 * t^3, t ∈ [0,1]
     *
     * @param t  力度步长
     * @param p0 起始点
     * @param p1 控制点1
     * @param p2 控制点2
     * @param p3 终止点
     * @return t对应的点
     */
    public static PointF getPointFromCubicBezier(float t, PointF p0, PointF p1, PointF p2, PointF p3) 
        PointF point = new PointF();
        float temp = 1 - t;
        point.x = p0.x * temp * temp * temp + 3 * p1.x * t * temp * temp + 3 * p2.x * t * t * temp + p3.x * t * t * t;
        point.y = p0.y * temp * temp * temp + 3 * p1.y * t * temp * temp + 3 * p2.y * t * t * temp + p3.y * t * t * t;
        return point;
    

4、BezierEvaluator

import android.animation.TypeEvaluator;
import android.graphics.PointF;

public class BezierEvaluator implements TypeEvaluator<PointF> 

    private PointF mControlPoint;

    public BezierEvaluator(PointF controlPoint) 
        this.mControlPoint=controlPoint;
    

    @Override
    public PointF evaluate(float v, PointF pointF, PointF t1) 
        return BezierUtil.getPointFromQuadBezier(v,pointF,mControlPoint,t1);
    

5、布局

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/tv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:lineSpacingExtra="2dp"
        android:textColor="#000000"
        android:textSize="30dp" />


    <!--收藏按钮-->
    <View
        android:id="@+id/to_move"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_marginBottom="200dp"
        android:background="#00ff00"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />


    <!--咔嚓截屏-->
    <com.zyp.cardview.YcCardView
        android:id="@+id/ka_cha_ycv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#ffffff"
        android:visibility="gone"
        app:ycCardElevation="10dp"
        app:ycCardPreventCornerOverlap="false"
        app:ycStartShadowColor="#0000ff">

        <LinearLayout
            android:id="@+id/ka_cha_ll"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#ffffff"
            android:orientation="vertical"
            android:visibility="gone">

            <TextView
                android:id="@+id/ka_cha_tv"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:textColor="#252525"
                android:textSize="20dp" />

        </LinearLayout>

    </com.zyp.cardview.YcCardView>

    <com.zyp.cardview.YcCardView
        android:id="@+id/ka_cha_move_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:background="#ffffff"
        android:visibility="gone"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:ycCardElevation="2dp"
        app:ycCardPreventCornerOverlap="false"
        app:ycStartShadowColor="#0000ff" />

    <TextView
        android:id="@+id/ka_cha_move_tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:background="#80000000"
        android:gravity="center"
        android:paddingStart="5dp"
        android:paddingTop="3dp"
        android:paddingEnd="5dp"
        android:paddingBottom="3dp"
        android:text="提示文案"
        android:textColor="#ffffff"
        android:textSize="12dp"
        android:visibility="gone"
        app:layout_constraintEnd_toEndOf="@id/ka_cha_move_view"
        app:layout_constraintStart_toStartOf="@id/ka_cha_move_view"
        app:layout_constraintTop_toBottomOf="@id/ka_cha_move_view" />


    <!--辅助线-->
    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="#ff0000"
        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="match_parent"
        android:background="#ff0000"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

6、界面代码

import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.app.Activity
import android.graphics.PointF
import android.os.Bundle
import android.view.View
import android.view.animation.LinearInterpolator
import androidx.constraintlayout.widget.ConstraintLayout
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : Activity() 

    private var isKaChaAnimFinish: Boolean = true

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

        //设置内容
        tv.text =
            "默认内容,默认内容,默认内容,默认内容,默认内容,默认内容,默认内容,默认内容,默认内容,默认内容,默认内容,默认内容,默认内容,默认内容,默认内容,默认内容,默认内容,默认内容,默认内容,默认内容,默认内容,默认内容,默认内容,默认内容,默认内容,默认内容,默认内容,默认内容,默认内容,默认内容,默认内容,默认内容,默认内容,默认内容,默认内容,默认内容,默认内容,默认内容,默认内容,默认内容,默认内容,默认内容,默认内容,默认内容,默认内容"


        //给动画设置内容
        ka_cha_tv?.text = "这是咔嚓后展示的内容,这是咔嚓后展示的内容,这是咔嚓后展示的内容,这是咔嚓后展示的内容,这是咔嚓后展示的内容,这是咔嚓后展示的内容"


        //设置点击事件
        to_move?.setOnClickListener 

            if (isKaChaAnimFinish) 
                startKaChaAnim()
            

        

    

    private var offsetX: Float = 0f
    private var offsetY: Float = 0f
    /**
     * 动画的整体思路
     * 1、点击按钮,开始截屏(截屏只是一种说法,不是真的截屏。其实就是让一些控件 设置了一些内容,然后这些控件做动画。下面还是保留截屏的说法)
     * 2、截屏后,内容view 从透明到不透明渐变出现;
     * 3、截屏内容view 变小
     * 4、停一下
     * 5、做抛物线动画
     * 6、抛的过程中,再次变小,从透明到透明度50%
     *
     * 开始咔嚓动画
     */
    private fun startKaChaAnim() 

        isKaChaAnimFinish = false

        //屏幕宽度
        val mScreenW = UiUtils.getScreenWidth(this)
        //屏幕高度。这里的高度,是去掉顶部状态栏(流量、电池、信号等所在行)、底部操作栏(返回键、home键、菜单键)
        val mScreenH = UiUtils.getScreenHeight(this)

        //缩小动画结束后,控件的宽度 = 屏幕宽度*0.2
        val mx: Float = mScreenW * 0.2f
        //缩小动画结束后,控件的高度 = 屏幕高度*(0.2f - 0.05f)。为什么要减去 0.05 ?看下面的注释,或搜索:为什么多减0.05
        val mh: Float = mScreenH * (0.2f - 0.05f)
        val moveCardLp: ConstraintLayout.LayoutParams =
            ka_cha_move_view.layoutParams as ConstraintLayout.LayoutParams
        moveCardLp.width = mx.toInt()
        moveCardLp.height = mh.toInt()
        ka_cha_move_view.layoutParams = moveCardLp

        //X轴方向的偏移值
        offsetX = mx / 2
        //Y轴方向的偏移值
        offsetY = mh / 2

        //准备变小动画
        val kaChaToSmallAnim: ValueAnimator = ValueAnimator.ofFloat(1f, 0.2f)
        kaChaToSmallAnim.interpolator = LinearInterpolator()

        kaChaToSmallAnim.duration = 600

        kaChaToSmallAnim.addUpdateListener(object :
            ValueAnimator.AnimatorUpdateListener 
            override fun onAnimationUpdate(animation: ValueAnimator?) 

                if (animation != null) 
                    val scale: Float = animation.animatedValue as Float

                    ka_cha_ycv?.scaleX = scale

                    /**
                     * 为什么多减0.05
                     *
                     * 正常情况下缩小,最后的结果view,宽高比和 mScreenW、mScreenH 的比是一样的。
                     * 可能会不好看,显的控件瘦长,这里多减去 0.05,是为了让控件看着比例好看些,可以不减
                     *
                     * 需要注意,不管这里减去还是不减去,上面计算的地方,也要做统一处理
                     */
                    ka_cha_ycv?.scaleY = scale - 0.05f
                
            
        )

        //准备透明度动画
        val kaChaToAlphaAnimator: ObjectAnimator = ObjectAnimator.ofFloat(
            ka_cha_ll,
            "alpha",
            0f, 1f
        )
        kaChaToAlphaAnimator.interpolator = LinearInterpolator()

        kaChaToAlphaAnimator.duration = 500

        kaChaToAlphaAnimator.addListener(object : Animator.AnimatorListener 
            override fun onAnimationStart(animator: Animator) 
                ka_cha_ycv?.visibility = View.VISIBLE
                ka_cha_ll?.visibility = View.VISIBLE
            

            override fun onAnimationEnd(animator: Animator) 

            

            override fun onAnimationCancel(animator: Animator) 

            

            override fun onAnimationRepeat(animator: Animator) 

            
        )

        //设置组合动画
        val kaChaAnimSet: AnimatorSet = AnimatorSet()
        //设置组合动画的属性
        kaChaAnimSet.interpolator = LinearInterpolator()

        kaChaAnimSet.addListener(object : Animator.AnimatorListener 
            override fun onAnimationStart(animator: Animator) 

            

            override fun onAnimationEnd(animator: Animator) 

                ka_cha_move_view?.visibility = View.INVISIBLE
                ka_cha_move_tv?.visibility = View.VISIBLE

                startKaChaMoveAnim()

            

            override fun onAnimationCancel(animator: Animator) 

            

            override fun onAnimationRepeat(animator: Animator) 

            
        )

        //先渐变,再缩小
        kaChaAnimSet.play(kaChaToSmallAnim)?.after(kaChaToAlphaAnimator)
        //开始做动画
        kaChaAnimSet.start()

    

    //开始做移动动画
    private fun startKaChaMoveAnim() 

        val w: Float = UiUtils.getScreenWidth(this).toFloat()
        val h: Float = UiUtils.getScreenHeight(this).toFloat()

        //下面的计算有点绕,具体的解释,见博客 https://blog.csdn.net/u014620028/article/details/113529034
        val sX: Float = w / 2 - offsetX
        val sY: Float = h / 2 - offsetY

        val eX: Float = w - UiUtils.dip2px(this, 50f) - offsetX
        val eY: Float = h - UiUtils.dip2px(this, 280f) - offsetY

        val pointControl: PointF = PointF(sX + 200, sY - 200)
        val pointStart: PointF = PointF(sX, sY)
        val pointEnd: PointF = PointF(eX, eY)

        val bezierEvaluator: BezierEvaluator = BezierEvaluator(pointControl)

        //位置变换动画
        val kaChaMoveWeiZhiChangeAnim: ValueAnimator =
            ValueAnimator.ofObject(bezierEvaluator, pointStart, pointEnd)

        kaChaMoveWeiZhiChangeAnim.interpolator = LinearInterpolator()

        kaChaMoveWeiZhiChangeAnim.duration = 800
        kaChaMoveWeiZhiChangeAnim.addUpdateListener(object : ValueAnimator.AnimatorUpdateListener 
            override fun onAnimationUpdate(animation: ValueAnimator?) 

                try 

                    if (animation != null) 

                        val currentPoint: PointF = animation.animatedValue as PointF

                        ka_cha_move_view?.x = currentPoint.x
                        ka_cha_move_view?.y = currentPoint.y

                    

                 catch (e: Exception) 
                    e.printStackTrace()
                

            
        )

        //准备变小动画
        val kaChaMoveToSmallAnim: ValueAnimator = ValueAnimator.ofFloat(1f, 0.2f)
        kaChaMoveToSmallAnim.interpolator = LinearInterpolator()

        kaChaMoveToSmallAnim.duration = 800

        kaChaMoveToSmallAnim.addUpdateListener(object :
            ValueAnimator.AnimatorUpdateListener 
            override fun onAnimationUpdate(animation: ValueAnimator?) 

                if (animation != null) 
                    val scale: Float = animation.animatedValue as Float

                    ka_cha_move_view?.scaleX = scale
                    ka_cha_move_view?.scaleY = scale
                
            
        )


        //准备透明度动画
        val kaChaMoveToAlphaAnimator: ObjectAnimator = ObjectAnimator.ofFloat(
            ka_cha_move_view,
            "alpha",
            1f, 0.5f
        )
        kaChaMoveToAlphaAnimator.interpolator = LinearInterpolator()

        kaChaMoveToAlphaAnimator.duration = 500

        //设置组合动画
        val kaChaMoveAnimSet: AnimatorSet = AnimatorSet()
        //设置组合动画的属性
        kaChaMoveAnimSet.interpolator = LinearInterpolator()

        kaChaMoveAnimSet.addListener(object : Animator.AnimatorListener 
            override fun onAnimationStart(animator: Animator) 

                ka_cha_move_view?.visibility = View.VISIBLE
                ka_cha_ycv?.visibility = View.GONE
                ka_cha_ll?.visibility = View.GONE

                ka_cha_ycv?.scaleX = 1f
                ka_cha_ycv?.scaleY = 1f

            

            override fun onAnimationEnd(animator: Animator) 

                ka_cha_move_view?.visibility = View.GONE

                isKaChaAnimFinish = true

            

            override fun onAnimationCancel(animator: Animator) 

            

            override fun onAnimationRepeat(animator: Animator) 

            
        )


        //位移、变小、透明度动画 一起执行
        kaChaMoveAnimSet.playTogether(
            kaChaMoveWeiZhiChangeAnim,
            kaChaMoveToSmallAnim,
            kaChaMoveToAlphaAnimator
        )

        ka_cha_move_view?.postDelayed(
            //开始做动画

            ka_cha_move_tv?.visibility = View.GONE

            kaChaMoveAnimSet.start()
        , 300)

    



复制了上面的代码,就能跑起来了,可以先试试。

说明:
1、offsetX 是截屏缩小到指定的最小比例后,控件的宽度的一半。offsetY 同理

2、startKaChaMoveAnim (开始做位移动画)中的一些计算说明:
sX:startX的缩写,意思是,动画的起点的X轴方向坐标。sX = w / 2 - offsetX = 屏幕宽度一半 - X偏移。
sY:同sX。sY = h / 2 - offsetY。
总的解释:(w / 2,h / 2 )是屏幕的中心点,也是截屏布局缩小后的中心点,但是view的移动,是按照控件是左上角算的,所以,就把移动的起点,设置为 (sX, sY)

eX:动画的结束点(终点)的X轴坐标。eX = w - 50dp - offsetX 。从布局文件可以看出,按钮的宽是100dp,因为控件最后的位置,是在按钮的中间,所以,是减去按钮宽度的一半,offsetX 的用处同上。
eY:动画的结束点(终点)的Y轴坐标。eY = h - 280dp - offsetY。offsetY不多说了。280 = 200+50+30。根据布局文件,200是按钮距离屏幕底部的距离;50是按钮高度的一般。30是我单独加的,我想让控件最后结束的位置略微靠上。这里的30,是自己定的,可以随便改,只要最后效果符合自己的要求就行

pointControl: PointF = PointF(sX + 200, sY - 200):控制点。因为要做抛物线运动,就是曲线,就是贝塞尔曲线。我的项目中要求,做抛物线的控件略微向上抛一点点就好,所以我这样设置了 控制点。具体的,根据自己的项目要求来

3、上面的代码可以看出来,当前界面截屏缩小后,做抛物线动画前,切换了控件。即:做截屏、缩小动画的是一个控件,做抛物线的,是另一个。
这是因为:控件缩小后,做抛物线运动的时候,是按控件的原始大小在移动(但是视觉上是缩小的)。这里比较不好描述,我尽量说明,如果还不明白,就把上面的代码改改,自己看下效果。

尽力的描述:初始的时候,截屏控件宽高是屏幕的宽高(宽高设为1,1)。做完缩小动画后,宽高变成了 宽:0.2,高:0.15。这个时候做抛物线动画,缩小后的控件,瞬间就跑到界面的右下角了,然后做位移动画。如果去掉缩小动画,把动画时间边长,就能看出来,做抛物线的时候,处于起始点位置的,是原始大小的截屏控件的左上角。为了抛物线动画的正常进行,只能用一个新的替换变小的截屏控件

以上是关于基于属性动画,实现 咔嚓截屏(收藏)动画的主要内容,如果未能解决你的问题,请参考以下文章

基于max-height实现不定高度元素的折叠/合并,展开/收缩的动画效果

基于 React 实现一个 Transition 过渡动画组件

UIBezierPath 动画 - 对象固定在左上角

android中的动画之属性动画

这个 HTML5 截屏动画是如何创建的?

markdown OS X截屏到动画GIF