基于属性动画,实现 咔嚓截屏(收藏)动画
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实现不定高度元素的折叠/合并,展开/收缩的动画效果