高仿京东到家APP引导页炫酷动画效果 前言 功能分析 布局分析 资源分析 代码实现 完整代码地址

高仿京东到家APP引导页炫酷动画效果
前言
功能分析
布局分析
资源分析
代码实现
完整代码地址

京东到家APP的引导页做的可圈可点,插画+动效,简明生动地说明了APP最吸引用户的几个亮点(商品多,价格低,配送快...)。本文主要分析拆解这些动画效果,并完成一个高仿Demo,完整的Demo代码可在文章结尾获取。

先看一下京东到家APP引导页动画效果,如下:

高仿京东到家APP引导页炫酷动画效果
前言
功能分析
布局分析
资源分析
代码实现
完整代码地址

功能分析

分析结果基于对APP进行反编译和我个人的理解(不然还能怎么办呢?我太难了,哈哈哈)

  • 浅浅的背景图+四个引导页面,每一个页面动画播放完成后自动滑动到下一个页面。实现:用ViewPager+4个View实现,每一个页面对应一个View,一个View包含一个LottieAnimationView,在页面中监听Lottie动画的播放,播放完成后自动切换到下一个页面,ViewPager使用的是Alibaba开源的UltraViewPager
背景图片 引导图1 引导图2 引导图3 引导图4
高仿京东到家APP引导页炫酷动画效果
前言
功能分析
布局分析
资源分析
代码实现
完整代码地址 高仿京东到家APP引导页炫酷动画效果
前言
功能分析
布局分析
资源分析
代码实现
完整代码地址 高仿京东到家APP引导页炫酷动画效果
前言
功能分析
布局分析
资源分析
代码实现
完整代码地址 高仿京东到家APP引导页炫酷动画效果
前言
功能分析
布局分析
资源分析
代码实现
完整代码地址 高仿京东到家APP引导页炫酷动画效果
前言
功能分析
布局分析
资源分析
代码实现
完整代码地址
  • 页面滑动切换时有一个旋转动画效果,可自定义ViewPager中的PageTransformer接口实现此效果,Title有透明度渐变效果,用属性动画ObjectAnimator实现

高仿京东到家APP引导页炫酷动画效果
前言
功能分析
布局分析
资源分析
代码实现
完整代码地址

  • 当滑动到最后一个页面时,出现一个带有“立即体验”文本的按钮,出现时从底部向上弹出并且透明度从0到1变化,当从最后一个页面向前滑动时,按钮从底部消失并且透明度从1到0变化,即位移+透明度变化动画,使用属性动画ObjectAnimator可以实现此效果

高仿京东到家APP引导页炫酷动画效果
前言
功能分析
布局分析
资源分析
代码实现
完整代码地址

布局分析

使用uiautomatorviewer查看layout布局文件,如下所示:

高仿京东到家APP引导页炫酷动画效果
前言
功能分析
布局分析
资源分析
代码实现
完整代码地址

  • 根布局为ConstraintLayout,子节点为ViewPager,xml文件如下:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:andro
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="@android:color/white">

    <com.airbnb.lottie.LottieAnimationView
        android:
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent" />

    <com.tmall.ultraviewpager.UltraViewPager
        android:
        android:layout_width="0.0dip"
        android:layout_height="0.0dip"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.airbnb.lottie.LottieAnimationView
        android:
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ImageView
        android:
        android:layout_width="46.0dip"
        android:layout_height="0.0dip"
        android:layout_marginRight="20.0dip"
        android:background="@drawable/pdj_guide_jump"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintDimensionRatio="63:28.5"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.07" />

    <ImageView
        android:
        android:layout_width="104.29999dip"
        android:layout_height="30.669983dip"
        android:background="@drawable/pdj_guide_button"
        android:focusable="true"
        android:visibility="gone"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        />


</androidx.constraintlayout.widget.ConstraintLayout>

  • 引导页总共有四个页面,每一个页面中LottieAnimationView对应一个Lottie文件,xml文件如下:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:andro
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">

    <com.airbnb.lottie.LottieAnimationView
        android:
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

资源分析

  • 资源文件放在了src/main/assets目录下,配置文件为LottieConfig.json,如下:
{
  "radius": 2020,
  "totalPageNum": 4,
  "lottieBgName": "guide_lottie_bg",
  "pageAnimTime": 350,
  "buttonAnimTime": 350,
  "lottieTitle": [
    {
      "lottieName": "title_lottie_0"
    },
    {
      "lottieName": "title_lottie_1"
    },
    {
      "lottieName": "title_lottie_2"
    },
    {
      "lottieName": "title_lottie_3"
    }
  ],
  "lottieMain": [
    {
      "lottieName": "main_lottie_0"
    },
    {
      "lottieName": "main_lottie_1"
    },
    {
      "lottieName": "main_lottie_2"
    },
    {
      "lottieName": "main_lottie_3",
      "repeatInterval": {
        "start": 0.288,
        "end": 1
      }
    }
  ]
}
  • radius --定义PageTransformer时会用到,用于确定最大旋转角度
  • totalPageNum --引导页的数量
  • lottieBgName --背景图片对应的Lottie文件名
  • pageAnimTime --ViewPager各个页面之间切换的滑动时间
  • buttonAnimTime --最后一个页面"立即体验"按钮做位移和透明度动画的时长
  • lottieTitle --每个引导页标题对应的Lottie文件名
  • lottieMain -- 每个引导页内容对应的Lottie文件名

代码实现

基于以上分析,用代码实现就比较简单了,此处贴出部分实现代码

  • 加载本地Lottie zip文件,Lottie支持加载本地和远程json,zip文件,通常我们将Lottie文件放在src/main/assets目录下
 fun loadAssetsLottieZipFile(context: Context, lottieImageView:LottieAnimationView, fileName:String,repeatCount:Int= LottieDrawable.INFINITE,autoPlay:Boolean=true){
        val lottieCompose= if(fileName.endsWith(".zip")){
            LottieCompositionFactory.fromAssetSync(context, fileName).value
         }
        else{
            LottieCompositionFactory.fromAssetSync(context, fileName.plus(".zip")).value
         }
        lottieImageView.progress=0.0f
        lottieImageView.repeatCount=repeatCount
        lottieImageView.setComposition(lottieCompose!!)
        if(autoPlay){
            lottieImageView.playAnimation()
        }

    }
  • 添加圆点指示符
        viewpager.initIndicator()
        viewpager.indicator.setOrientation(UltraViewPager.Orientation.HORIZONTAL)
                .setNormalIcon(drawableToBitmap(ContextCompat.getDrawable(applicationContext, R.drawable.guide_white_dot)!!))
                .setFocusIcon(drawableToBitmap(ContextCompat.getDrawable(applicationContext, R.drawable.guide_dark_dot)!!))
                .setIndicatorPadding(ScreenHelper.dp2px(applicationContext, 5.0F))
                .setMargin(0, 0, 0, ScreenHelper.dp2px(applicationContext, 20.0F))
        viewpager.indicator.setGravity(Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL)
        viewpager.indicator.build()

其中drawableToBitmap对应的代码为:

    /**
     * Drawable转换成Bitmap
     */
    private fun drawableToBitmap(drawable: Drawable): Bitmap {
        val bitmap = Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, Bitmap.Config.ARGB_8888)
        val canvas = Canvas(bitmap)
        drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
        drawable.draw(canvas)
        return bitmap
    }
  • 自定义PageTransformer实现页面切换旋转效果

    • position < -1 时,旋转到最大角度,旋转中心为右下角;
    • -1 < position < 0 时,position 越靠近 0 ,旋转角度越小,旋转中心向下边缘中心靠拢;
    • 0 <= position <= 1 时,position 越靠近 0 ,旋转角度越小,旋转中心向下边缘中心靠拢;
    • position > 1 时,旋转到最大角度,旋转中心为左下角。

代码如下所示:

import android.content.Context
import android.view.View
import androidx.viewpager.widget.ViewPager
import com.kongpf.commonhelper.ScreenHelper
import kotlin.math.atan2

class GuideTransformer(context: Context, private val mRadius: Int) : ViewPager.PageTransformer {
    private var mMaxRotate = 0f

    init {
        if (mRadius > 0) {
            mMaxRotate = (2.0 * Math.toDegrees(atan2((ScreenHelper.getScreenWidth(context) / 2).toDouble(), mRadius.toDouble()))).toFloat()
        }
    }

    override fun transformPage(page: View, position: Float) {
        if (mRadius == 0) {
            return
        }

        when {
            position < -1.0f -> {
                page.rotation = -1.0f * mMaxRotate
                page.pivotX = page.width.toFloat()
                page.pivotY = page.height.toFloat()
            }
            position <= 1.0f -> {
                if (position < 0.0f) {
                    page.pivotX = page.width * (0.5f + 0.5f * -position)
                    page.pivotY = page.height.toFloat()
                    page.rotation = position * mMaxRotate
                } else {
                    page.pivotX = 0.5f * page.width * (1.0f - position)
                    page.pivotY = page.height.toFloat()
                    page.rotation = position * mMaxRotate
                }
            }
            else -> {
                page.rotation = mMaxRotate
                page.pivotX = 0f
                page.pivotY = page.height.toFloat()
            }
        }
    }
}

完整代码地址

https://github.com/kongpf8848/Animation