如图,引导开始,球从上落下,同时淡入文字,然后文字开始轮播,最后一页时停止,点击进入首页。
在来看看效果图。
重力球先不讲,主要欢迎轮播简单实现
首先新建一个类 TextTranslationXGuideView,用于动画展示
文本是类似的,最后会有个图片箭头动画,布局很简单,就是一个 TextView 跟 ImageView,直接写 xml 布局里方便了
所以 TextTranslationXGuideView 直接继承 FrameLayout,然后动态添加布局,控制动画
val root = LayoutInflater.from(context) .inflate(R.layout.login_layout_text_translation_x_guide, this, false) root.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) addView(root) mBinding = LoginLayoutTextTranslationXGuideBinding.bind(root)
login_layout_text_translation_x_guide
<?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="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" tools:background="@color/white" tools:layout_marginStart="@dimen/dp_24"> <TextView android:id="@+id/tv_content" android:layout_width="wrap_content" android:layout_height="wrap_content" android:fontFamily="@font/misans_bold" android:lineSpacingExtra="@dimen/dp_20" android:textColor="@color/bl_black" android:textSize="@dimen/sp_36" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:text="欢迎xxx\n111" /> <ImageView android:id="@+id/iv_guide1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="@dimen/dp_30" android:src="@drawable/login_guide_text_right_black" android:visibility="gone" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/tv_content" tools:visibility="visible" /> <ImageView android:id="@+id/iv_guide2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/login_guide_text_right_end" android:visibility="gone" app:layout_constraintBottom_toBottomOf="@+id/iv_guide1" app:layout_constraintStart_toEndOf="@+id/iv_guide1" app:layout_constraintTop_toTopOf="@+id/iv_guide1" app:tint="@color/c_f4f4f4" tools:visibility="visible" /> </androidx.constraintlayout.widget.ConstraintLayout>
文字颜色换行等通过 span 设置,所以需要一个类去配置
data class TextTranslationXGuideBean( val content: String, //内容 val bright: String?, //高亮文本 val brightColor: Int = R.color.bl_black //高亮字体颜色 )
轮播配置成动态的,所以这里使用一个集合去存储
private val guideList = mutableListOf<TextTranslationXGuideBean>()
/** * 添加单个引导文本 * @param content 内容 * @param bright 高亮文本 * @param brightColor 高亮字体颜色 * */ fun addTextGuide( content: String, bright: String? = null, brightColor: Int? = null ): TextTranslationXGuideView { guideList.add(TextTranslationXGuideBean(content, bright, brightColor ?: R.color.bl_black)) return this }
然后在动态设置内容跟图片
/** 设置引导内容 */ private fun setGuideContent(bean: TextTranslationXGuideBean) { mBinding?.tvContent?.text = bean.content val span = SpanUtils.with(mBinding?.tvContent) .append(bean.content) .setForegroundColor(resources.getColor(R.color.bl_black, null)) bean.bright?.let { span.append("\n${bean.bright}") .setForegroundColor(resources.getColor(bean.brightColor, null)) } span.create() }
接下来需要两个动画,一个淡入,一个平移(TextView 自带的跑马灯不好控制,后期如果更换方案改动也大)
private var mTranslationAnimator: ValueAnimator? = null private var mFlickerAnimator: ValueAnimator? = null init { initView() initTranslationAnimation() initGuideRightAnimate() }
平移动画重复执行,轮播显示,通过下标控制,显示 guideList 中的数据,如果轮播到最后一条,展示箭头闪烁动画
private fun initTranslationAnimation() { val point = -ScreenUtils.getScreenWidth().toFloat() mTranslationAnimator = ValueAnimator.ofFloat(0f, point) mTranslationAnimator?.duration = 300 mTranslationAnimator?.interpolator = LinearInterpolator() mTranslationAnimator?.addUpdateListener { animation -> val scrollX = animation.animatedValue as Float translationX = scrollX if (scrollX <= point) { mTranslationAnimator?.cancel() alpha = 0f translationX = 0f nextGuide() } } } private fun initTranslationAnimation() { val point = -ScreenUtils.getScreenWidth().toFloat() mTranslationAnimator = ValueAnimator.ofFloat(0f, point) mTranslationAnimator?.duration = 300 mTranslationAnimator?.interpolator = LinearInterpolator() mTranslationAnimator?.addUpdateListener { animation -> val scrollX = animation.animatedValue as Float translationX = scrollX if (scrollX <= point) { mTranslationAnimator?.cancel() alpha = 0f translationX = 0f nextGuide() } } } /** 开始时调用 */ fun initGuide() { position = 0 if (guideList.size > 0) { guideList.getOrNull(position)?.let { setGuideContent(it) } //渐入 alpha = 0f startAlphaAnimation(1500) { startTranslationAnimator() } } }
结束时清楚缓存跳转首页
fun clear() { guideList.clear() mTranslationAnimator?.cancel() mTranslationAnimator = null mFlickerAnimator?.cancel() mFlickerAnimator = null }
全部实现
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
/** 登录引导动画 */ class TextTranslationXGuideView(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) { private var mBinding: LoginLayoutTextTranslationXGuideBinding? = null private var mTranslationAnimator: ValueAnimator? = null private var mFlickerAnimator: ValueAnimator? = null private val guideList = mutableListOf<TextTranslationXGuideBean>() private var position = 0//当前显示的引导索引 var clickRight: (() -> Unit)? = null init { initView() initTranslationAnimation() initGuideRightAnimate() } private fun initView() { val root = LayoutInflater.from(context) .inflate(R.layout.login_layout_text_translation_x_guide, this, false) root.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) addView(root) mBinding = LoginLayoutTextTranslationXGuideBinding.bind(root) mBinding?.ivGuide1?.setOnThrottledClickListener { clickRight?.invoke() } mBinding?.ivGuide2?.setOnThrottledClickListener { clickRight?.invoke() } } private fun initTranslationAnimation() { val point = -ScreenUtils.getScreenWidth().toFloat() mTranslationAnimator = ValueAnimator.ofFloat(0f, point) mTranslationAnimator?.duration = 300 mTranslationAnimator?.interpolator = LinearInterpolator() mTranslationAnimator?.addUpdateListener { animation -> val scrollX = animation.animatedValue as Float translationX = scrollX if (scrollX <= point) { mTranslationAnimator?.cancel() alpha = 0f translationX = 0f nextGuide() } } } private fun startTranslationAnimator() { mTranslationAnimator?.start() } private fun initGuideRightAnimate() { mFlickerAnimator = ValueAnimator.ofFloat(0f, 1f) mFlickerAnimator?.duration = 600 mFlickerAnimator?.interpolator = LinearInterpolator() mFlickerAnimator?.repeatMode = ValueAnimator.REVERSE mFlickerAnimator?.repeatCount = ValueAnimator.INFINITE mFlickerAnimator?.addUpdateListener { animation -> val alpha = animation.animatedValue as Float mBinding?.ivGuide2?.alpha = alpha } } private fun startGuideRightAnimator() { mBinding?.ivGuide2?.visibility = View.VISIBLE mBinding?.ivGuide2?.alpha = 0f mFlickerAnimator?.start() } /** 开始时调用 */ fun initGuide() { position = 0 if (guideList.size > 0) { guideList.getOrNull(position)?.let { setGuideContent(it) } //渐入 alpha = 0f startAlphaAnimation(1500) { startTranslationAnimator() } } } /** 下一个引导 */ private fun nextGuide() { position += 1 //是否为最后一条数据 val isEndGuide = position == guideList.size - 1 //第一个图标需要先展示 mBinding?.ivGuide1?.visibility = if (isEndGuide) View.VISIBLE else View.GONE guideList.getOrNull(position)?.let { setGuideContent(it) startAlphaAnimation { if (position < guideList.size - 1) { //如果有,循环执行下一个引导 startTranslationAnimator() } else { //最后一个,执行渐变闪烁动画 startGuideRightAnimator() } } } } private fun startAlphaAnimation(duration: Long = 1000L, endListener: (() -> Unit)) { animate().setDuration(duration).alpha(1f) .setListener(object : Animator.AnimatorListener { override fun onAnimationStart(p0: Animator?) {} override fun onAnimationEnd(p0: Animator?) { endListener.invoke() } override fun onAnimationCancel(p0: Animator?) {} override fun onAnimationRepeat(p0: Animator?) {} }) } /** 设置引导内容 */ private fun setGuideContent(bean: TextTranslationXGuideBean) { mBinding?.tvContent?.text = bean.content val span = SpanUtils.with(mBinding?.tvContent) .append(bean.content) .setForegroundColor(resources.getColor(R.color.bl_black, null)) bean.bright?.let { span.append("\n${bean.bright}") .setForegroundColor(resources.getColor(bean.brightColor, null)) } span.create() } /** * 添加单个引导文本 * @param content 内容 * @param bright 高亮文本 * @param brightColor 高亮字体颜色 * */ fun addTextGuide( content: String, bright: String? = null, brightColor: Int? = null ): TextTranslationXGuideView { guideList.add(TextTranslationXGuideBean(content, bright, brightColor ?: R.color.bl_black)) return this } fun clear() { guideList.clear() mTranslationAnimator?.cancel() mTranslationAnimator = null mFlickerAnimator?.cancel() mFlickerAnimator = null } data class TextTranslationXGuideBean( val content: String, //内容 val bright: String?, //高亮文本 val brightColor: Int = R.color.bl_black //高亮字体颜色 ) }