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

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

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

因为代码简单,就没放到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 {private PointF mControlPoint;public BezierEvaluator(PointF controlPoint) {this.mControlPoint=controlPoint;}@Overridepublic PointF evaluate(float v, PointF pointF, PointF t1) {return BezierUtil.getPointFromQuadBezier(v,pointF,mControlPoint,t1);}
}

5、布局



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 = trueoverride 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 = 0fprivate 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.2val mx: Float = mScreenW * 0.2f//缩小动画结束后,控件的高度 = 屏幕高度*(0.2f - 0.05f)。为什么要减去 0.05 ?看下面的注释,或搜索:为什么多减0.05val mh: Float = mScreenH * (0.2f - 0.05f)val moveCardLp: ConstraintLayout.LayoutParams =ka_cha_move_view.layoutParams as ConstraintLayout.LayoutParamsmoveCardLp.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 = 600kaChaToSmallAnim.addUpdateListener(object :ValueAnimator.AnimatorUpdateListener {override fun onAnimationUpdate(animation: ValueAnimator?) {if (animation != null) {val scale: Float = animation.animatedValue as Floatka_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 = 500kaChaToAlphaAnimator.addListener(object : Animator.AnimatorListener {override fun onAnimationStart(animator: Animator) {ka_cha_ycv?.visibility = View.VISIBLEka_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.INVISIBLEka_cha_move_tv?.visibility = View.VISIBLEstartKaChaMoveAnim()}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/113529034val sX: Float = w / 2 - offsetXval sY: Float = h / 2 - offsetYval eX: Float = w - UiUtils.dip2px(this, 50f) - offsetXval eY: Float = h - UiUtils.dip2px(this, 280f) - offsetYval 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 = 800kaChaMoveWeiZhiChangeAnim.addUpdateListener(object : ValueAnimator.AnimatorUpdateListener {override fun onAnimationUpdate(animation: ValueAnimator?) {try {if (animation != null) {val currentPoint: PointF = animation.animatedValue as PointFka_cha_move_view?.x = currentPoint.xka_cha_move_view?.y = currentPoint.y}} catch (e: Exception) {e.printStackTrace()}}})//准备变小动画val kaChaMoveToSmallAnim: ValueAnimator = ValueAnimator.ofFloat(1f, 0.2f)kaChaMoveToSmallAnim.interpolator = LinearInterpolator()kaChaMoveToSmallAnim.duration = 800kaChaMoveToSmallAnim.addUpdateListener(object :ValueAnimator.AnimatorUpdateListener {override fun onAnimationUpdate(animation: ValueAnimator?) {if (animation != null) {val scale: Float = animation.animatedValue as Floatka_cha_move_view?.scaleX = scaleka_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.VISIBLEka_cha_ycv?.visibility = View.GONEka_cha_ll?.visibility = View.GONEka_cha_ycv?.scaleX = 1fka_cha_ycv?.scaleY = 1f}override fun onAnimationEnd(animator: Animator) {ka_cha_move_view?.visibility = View.GONEisKaChaAnimFinish = 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.GONEkaChaMoveAnimSet.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。这个时候做抛物线动画,缩小后的控件,瞬间就跑到界面的右下角了,然后做位移动画。如果去掉缩小动画,把动画时间边长,就能看出来,做抛物线的时候,处于起始点位置的,是原始大小的截屏控件的左上角。为了抛物线动画的正常进行,只能用一个新的替换变小的截屏控件


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部