如图,引导开始,球从上落下,同时淡入文字,然后文字开始轮播,最后一页时停止,点击进入首页。
【资料图】
在来看看效果图。
重力球先不讲,主要欢迎轮播简单实现
首先新建一个类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
文字颜色换行等通过 span 设置,所以需要一个类去配置
data class TextTranslationXGuideBean( val content: String, //内容 val bright: String?, //高亮文本 val brightColor: Int = R.color.bl_black //高亮字体颜色 )
轮播配置成动态的,所以这里使用一个集合去存储
private val guideList = mutableListOf()
/** * 添加单个引导文本 * @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? = nullprivate var mFlickerAnimator: ValueAnimator? = nullinit { 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 }
全部实现
/** 登录引导动画 */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 = mutableListOfTextTranslationXGuideView() 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 //高亮字体颜色 )}