Android 过场效果--列表页到详情页

用过TapTap的APP发现在排行榜的列表页点击单项会有一个进入详情页的过场效果,觉得很不错, 小米的系统相册也有类似的过场效果,个人对这个效果很有兴趣,便决定自己也实现下这个效果。虽说做完Demo后了解到android 5.0以上的sdk有共享元素动画的方式去实现,但是,这里并不采用该方式。按照自己的思路来实现,记录一下实现的过程。Demo是基于kotlin写的。效果图如下:

目录

 

目录

效果分析

一些要点

1.Activity转场动画

2.跳转

3.View信息

4.根布局

5.坐标

代码

1.布局文件

2.页面代码



效果分析

从上面的GIF看到 在列表页点击一个列表项的图片,图片大小发生变化并且移动到详情页中的某一个位置,详情页返回时图片会回到列表页中的原位置。详情页中布局有一个不可见 INVISIBLE 状态的ImageView,当动画完成后,才将可见状态设置为可见。从思路上就是,复制一个ImageView添加到跳转页中,跳转页中有一个不可见的ImageView,复制的ImageView经过动画变化到达与目标ImageView的位置大小状态一致,结束。

这里实现用到的知识点有 Activity过场动画,属性动画,View的坐标以及View在Window中的坐标。以下是详细分析

1.点击列表项的图片时,传递当前图片View的信息到到详情页,使用startActivityForResult,在列表页的onStop生命周期方法(防止出现闪烁情况)中将当前项的图片隐藏,列表页根布局透明度设为0.记录当前项图片的位置,宽高信息。

2.详情页初始状态根布局的透明度为0,且有一个INVISIBLE 状态的ImageView,根据从详情页传递过来的View信息,构建一个复制的ImageView添加到根布局中,计算出复制ImageView和目标ImageView的坐标,宽高差异后,执行复制ImageView坐标,宽高动画,并同时进行详情页根布局的透明度变化,动画完成后,将目标ImageView设为可见,从根布局移除添加的复制ImageView。此时,图片从列表页过渡到列表页中的过程结束。

3.详情页返回列表页的过程也是类似,在此demo中是监听手机的返回键,将详情页的目标ImageView的信息传递给列表页,在列表页中复制一个ImageView添加到根布局中,在第1步的时候我们已经记录了被点击图片的位置,宽高信息,所以复制的ImageView需要回到被点击图片的状态中,动画和第2步一样(反过来),执行完后,在第1步被隐藏的图片设为可见,同样移除复制的ImageView。

一些要点

1.Activity转场动画

Activity默认的转场效果是从右往左,从gif的效果图上看是进场Activity直接从上覆盖到原Activity,所以这里可以用以下代码来实现,可以看成是透明度进场时长为0的动画效果,也可以定义动画xml文件来控制时长。

overridePendingTransition(0,0)

2.跳转

从列表页跳转到详情页需要传递列表页当前项图片的View信息,从详情页回到列表页也需要传递信息,所以跳转这里用startActivityForResult方法来实现

3.View信息

  这里我们定义一个类来记录页面传的View信息,主要记录坐标,宽高信息,定义如下:

  (由于在本Demo中图片是在资源包res里面的,一般情况下可以多加个url属性,通过一些图片缓存框架如Glide可以很快加载)

package com.example.zyb.tapdemo.Bean

import java.io.Serializable

/**
 * 复制的View的信息类  宽高和坐标
 * Created by ZYB on 2018/8/3 0003.
 */
class ViewInfo(var width:Int,var height:Int,var x:Int,var y:Int):Serializable{

}

4.根布局

根布局采用的是FrameLayout帧布局,新添加的ImageView处于最上的图层,通过坐标方便的定位到ImageView在根布局所处的位置(只要是继承于ViewGroup都可以)

5.坐标

View的属性中有x,y,但是x,y坐标是相对于父布局的位置坐标,所以在列表中的图片ImageView的x,y坐标不是我们想要的,我们应该获取的是列表中ImageView相对于列表页根布局的坐标。在这里,我们使用的是屏幕Window坐标,屏幕坐标,是View相对于Window窗口的绝对坐标,通过屏幕坐标可以间接得出View在根布局中的坐标,使得复制的View的位置和原View一样。

如果存在状态栏的情况下,列表项ImageView相对于根布局的Y坐标=屏幕Y坐标-状态栏高度

不存在的情况下,列表项ImageView相对于根布局的Y坐标=屏幕Y坐标

列表项ImageView相对于根布局的X坐标=屏幕X坐标

这样一来 我们就得到了列表项相对于根布局的X,Y坐标,由此我们可以在详情页复制一个位置和列表项位置一样的View出来

   获取View的屏幕window坐标  View类的getLocationInWindow方法

//定义一个长度为2的数组
var item_loc = IntArray(2)
//调用View的getLocationInWindow方法
//item_loc[0]即为View相对于屏幕的X坐标,item_loc[1]即为View相对于屏幕的Y坐标,
View.getLocationInWindow(item_loc)

   获取状态栏高度

/**
 * 获取状态栏高度
 */
fun getStateBarHeight(context:Context):Int{
    var height = 0;
    var resid = context.resources.getIdentifier("status_bar_height", "dimen", "android");
    if (resid > 0)
    {
        height = context.resources.getDimensionPixelOffset(resid)
    }
    return height;
}

代码

1.布局文件

    列表页布局

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/frame_root"
    tools:context="com.example.zyb.tapdemo.ListInfoActivity">
    <android.support.v7.widget.RecyclerView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/rcv_tap"
        />
</FrameLayout>

    详情页布局

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/frame_root"
    >
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/ll_content"
        android:orientation="vertical"
        android:alpha="0.0"
        >
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:text="古陵逝烟.大宗师"
            android:textSize="18sp"
            android:textStyle="bold"
            android:layout_marginTop="50dp"
            />
        <ImageView
            android:layout_width="300dp"
            android:layout_height="300dp"
            android:layout_gravity="center_horizontal"
            android:layout_marginTop="20dp"
            android:scaleType="centerCrop"
            android:src="@mipmap/timg"
            android:visibility="invisible"
            android:id="@+id/img_target"
            />
        <TextView
            android:layout_width="250dp"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:layout_marginTop="30dp"
            android:textStyle="bold"
            android:textSize="18sp"
            android:text="冷灯看剑,剑上几番功名?炉香无须计苍生,纵一川烟逝,万丈云埋,孤阳还照古陵。"
            />
    </LinearLayout>
</FrameLayout>

    item布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:paddingLeft="10dp"
        android:paddingRight="10dp"
        android:paddingTop="10px"
        >
        <ImageView
            android:layout_width="80dp"
            android:layout_height="80dp"
            android:id="@+id/item_img"
            android:src="@mipmap/timg"
            android:scaleType="centerCrop"
            />
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="烟都大宗师"
            android:layout_gravity="center_vertical"
            android:paddingLeft="50dp"
            />
    </LinearLayout>
    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="#ff0000"
        android:layout_marginTop="10px"
        />
</LinearLayout>

 布局上都比较简单,没什么可以讲的

2.页面代码

   ListInfoActivity.kt 

package com.example.zyb.tapdemo


import android.content.Intent
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager
import android.view.View
import android.widget.ImageView
import com.example.zyb.tapdemo.Adapter.TapAdapter
import com.example.zyb.tapdemo.Bean.ViewInfo
import kotlinx.android.synthetic.main.activity_main.*;

class ListInfoActivity : AppCompatActivity() {

    lateinit var adapter: TapAdapter
    //记录列表当前被点击的View的信息
    lateinit var viewinfo: ViewInfo
    //记录列表当前被点击的View的坐标信息
    private var item_loc = IntArray(2)
    var itemX = 0;
    var itemY = 0;
    //记录列表当前点击的view
    var focus_view : ImageView ?= null
    //是否可以隐藏列表当前点击的View
    var isCanHideView = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initRecycleView()
    }

    fun initRecycleView() {
        adapter = TapAdapter(this)
        rcv_tap.layoutManager = LinearLayoutManager(this,LinearLayoutManager.VERTICAL,false)
        rcv_tap.adapter = adapter
        adapter.itemOnclickListener = object: TapAdapter.itemOnClickListener{
            override fun itemOnclick(v: View) {
                when(v.id){
                    R.id.item_img ->{
                        imgItemClick(v as ImageView)
                    }
                }
            }
        }
    }

    fun imgItemClick(v:ImageView)
    {
        focus_view = v
        isCanHideView = true
        //获取当前view在屏幕的绝对坐标X,Y
        v.getLocationInWindow(item_loc)
        //减去状态栏高度即可得到当前view在Activity跟布局的绝对坐标Y
        itemX = item_loc[0]
        itemY = item_loc[1] - getStateBarHeight(this)

        viewinfo = ViewInfo(v.measuredWidth, v.measuredHeight, itemX, itemY)
        var intent = Intent(this,DetailActivity::class.java)
        intent.putExtra("viewinfo",viewinfo);
        startActivityForResult(intent, REQUEST_GO)
        overridePendingTransition(0,0)


    }


    override fun onResume() {
        super.onResume()
        isCanHideView = false
    }

    /**
     * 在onstop隐藏View 防止隐藏或者更改透明度时发生闪烁
     */
    override fun onStop() {
        super.onStop()
        if (focus_view != null && isCanHideView) {
            focus_view!!.visibility = View.INVISIBLE
            rcv_tap.alpha = 0f
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == REQUEST_GO && resultCode == RESULT_BACK)
        {
            var backViewInfo = data!!.getSerializableExtra("backViewInfo") as ViewInfo
            ViewHelper(this,focus_view!!)
                    .setRootView(frame_root)
                    .setViewInfos(backViewInfo,viewinfo)
                    .addCopyView()
                    .setDuration(500L)
                    .startAnim()
        }
    }




}

   DetailActivity .kt

package com.example.zyb.tapdemo

import android.app.Activity
import android.content.Intent
import android.os.Bundle
import com.example.zyb.tapdemo.Bean.ViewInfo
import kotlinx.android.synthetic.main.activity_detail.*

/**
 * Created by ZYB on 2018/8/3 0003.
 */
class DetailActivity : Activity() {

    lateinit var receiver_viewinfo: ViewInfo
    lateinit var target_viewinfo: ViewInfo
    var target_loc = IntArray(2)
    var targetX = 0
    var targetY = 0
    //是否第一次执行onWindowFocusChanged
    var isFirstWindowFocusChanged = true;
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_detail)
        receiver_viewinfo = intent.getSerializableExtra("viewinfo") as ViewInfo
    }

    //在此处可以获得布局控件的宽高属性等
    override fun onWindowFocusChanged(hasFocus: Boolean) {
        super.onWindowFocusChanged(hasFocus)
        if (isFirstWindowFocusChanged) {
            isFirstWindowFocusChanged = false
            img_target.getLocationInWindow(target_loc)
            targetX = target_loc[0]
            targetY = target_loc[1] - getStateBarHeight(this)
            target_viewinfo = ViewInfo(img_target.measuredWidth, img_target.measuredHeight, targetX, targetY)
            ViewHelper(this,img_target)
                    .setRootView(frame_root)
                    .setViewInfos(receiver_viewinfo,target_viewinfo)
                    .addCopyView()
                    .setDuration(500L)
                    .startAnim()
        }
    }

    //监听返回键 事件
    override fun onBackPressed() {
        var intent = Intent()
        intent.putExtra("backViewInfo", ViewInfo(img_target.measuredWidth, img_target.measuredHeight, targetX, targetY))
        setResult(RESULT_BACK,intent)
        finish()
        overridePendingTransition(0,0)
    }





}

  Adapter.kt

package com.example.zyb.tapdemo.Adapter

import android.content.Context
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import com.example.zyb.tapdemo.R

/**这里只是单纯显示布局而已,所以没有传入bean数据类,直接在getItemCount()设置列表的项数
 * Created by ZYB on 2018/8/3 0003.
 */
class TapAdapter(val context:Context):RecyclerView.Adapter<TapAdapter.Holder>(),View.OnClickListener
{

    lateinit var itemOnclickListener: itemOnClickListener


    override fun onBindViewHolder(holder: Holder?, position: Int) {
        holder!!.item_img!!.setOnClickListener(this)
    }

    override fun getItemCount(): Int {
       return 20;
    }

    override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): Holder {

        return Holder(LayoutInflater.from(context).inflate(R.layout.item_tap,null))
    }

    override fun onClick(v: View?) {
        if (itemOnclickListener != null)
        {
            itemOnclickListener.itemOnclick(v!!)
        }
    }

    inner class Holder(v:View):RecyclerView.ViewHolder(v){
        var item_img:ImageView
        init {
            item_img = v.findViewById(R.id.item_img) as ImageView
        }
    }

    //单个View的点击监听
    interface itemOnClickListener{
        fun itemOnclick(v:View);
    }
}

由于点击进入详情页以及从详情页返回的动画效果一致,所以这里定义了一个类来执行添加复制类以及动画效果

ViewHelper.kt

package com.example.zyb.tapdemo

import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.content.Context
import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateInterpolator
import android.widget.ImageView
import com.example.zyb.tapdemo.Bean.ViewInfo
import com.example.zyb.tapdemo.Listener.animationEndListener

/**
 * 负责添加复制的View到根布局 并且开始动画
 * context 上下文
 * targetView 目标View
 * Created by ZYB on 2018/8/5 0005.
 */
class ViewHelper(var context : Context,var targetView:View){
    //根View
    lateinit var rootView : ViewGroup
    //复制的ImageView
    lateinit var copyView : ImageView
    //复制的VIew的信息类
    lateinit var fromViewInfo : ViewInfo
    //目标View的信息类
    lateinit var toViewInfo : ViewInfo
    //动画时长
    private  var duration = 0L;

    fun setRootView(rootView : ViewGroup):ViewHelper{
        this.rootView = rootView
        return this
    }

    fun setViewInfos(fromViewInfo : ViewInfo, toViewInfo : ViewInfo) : ViewHelper{
        this.fromViewInfo = fromViewInfo
        this.toViewInfo = toViewInfo
        return this
    }

    fun  setDuration(duration: Long) : ViewHelper{
        this.duration = duration;
        return this
    }

    //构建一个View添加到根布局
    fun addCopyView() : ViewHelper{
        copyView = ImageView(context)
        var layoutParam = ViewGroup.LayoutParams(toViewInfo.width, toViewInfo.height)
        copyView.scaleType = ImageView.ScaleType.CENTER_CROP
        copyView.layoutParams = layoutParam
        copyView.x = toViewInfo.x.toFloat()
        copyView.y = toViewInfo.y.toFloat()
        copyView.setImageResource(R.mipmap.timg)
        rootView.addView(copyView)
        return this
    }
    //执行根布局透明度动画,复制ImageView的x坐标动画,Y坐标动画,宽高动画
    fun startAnim() : ViewHelper{
        var alphaAnim = ObjectAnimator.ofFloat(rootView.getChildAt(0),"alpha",0f,1f)
        var xAnim = ObjectAnimator.ofFloat(copyView, "x", fromViewInfo.x.toFloat(), toViewInfo.x.toFloat())
        var yAnim = ObjectAnimator.ofFloat(copyView, "y", fromViewInfo.y.toFloat(), toViewInfo.y.toFloat())
        var widthAnim = ValueAnimator.ofInt(fromViewInfo.width, toViewInfo.width)
        var heightAnim = ValueAnimator.ofInt(fromViewInfo.height, toViewInfo.height)
        widthAnim.addUpdateListener(object : ValueAnimator.AnimatorUpdateListener {
            override fun onAnimationUpdate(animation: ValueAnimator?) {
                var param = copyView.layoutParams
                param.width = animation!!.animatedValue as Int
                copyView.layoutParams = param

            }
        })
        heightAnim.addUpdateListener(object : ValueAnimator.AnimatorUpdateListener {
            override fun onAnimationUpdate(animation: ValueAnimator?) {
                var param = copyView.layoutParams
                param.height = animation!!.animatedValue as Int
                copyView.layoutParams = param
            }
        })
        //多个动画同时播放
        var animset = AnimatorSet()
        animset.playTogether(xAnim, yAnim, widthAnim, heightAnim,alphaAnim)
        animset.duration = 500;
        animset.interpolator = AccelerateInterpolator()
        animset.addListener(object : animationEndListener(){
            override fun animatorEnd(animation: Animator?) {
                //动画执行完毕后,目标ImageView显示出来,移除复制的ImageView
                targetView.visibility = View.VISIBLE
                rootView.removeView(copyView)
            }
        })
        animset.start()
        return this
    }

}

总结

实现这样的过场效果需要了解View的屏幕window坐标,以及View的坐标知识,属性动画,掌握了思路后,亲手实现这样的效果还是挺有趣的。(PS:感觉写的好乱啊,以下会有Demo下载地址,可以参考)

下载地址 https://download.csdn.net/download/qq_33617079/10587284

 

要实现图像扭曲过场效果,可以使用Unity3D中的Shader。具体实现步骤如下: 1. 创建一个新的Shader,并将其属性设置为Unlit/Texture。 2. 在Shader中添加一个名为“Distortion”的属性,类型为2D纹理,用于存储扭曲图像。 3. 在Shader中添加一个名为“DistortionStrength”的属性,类型为Range,用于控制扭曲强度。 4. 在Shader的片段着色器中,使用tex2D函数获取原始纹理的颜色,并使用uv坐标对扭曲图像进行采样。 5. 将扭曲图像的采样值与“DistortionStrength”属性相乘,并将结果添加到原始颜色中。 6. 最后,将新的颜色值输出到屏幕。 下面是一个简单的Shader示例: ``` Shader "Custom/DistortionTransition" { Properties { _MainTex ("Texture", 2D) = "white" {} _Distortion ("Distortion", 2D) = "white" {} _DistortionStrength ("Distortion Strength", Range(0.0, 1.0)) = 0.5 } SubShader { Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; sampler2D _MainTex; sampler2D _Distortion; float _DistortionStrength; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv; return o; } fixed4 frag (v2f i) : SV_Target { // Get the original color fixed4 texColor = tex2D(_MainTex, i.uv); // Get the distortion value float4 distortion = tex2D(_Distortion, i.uv); // Calculate the distortion offset float2 offset = (distortion.rg * 2.0 - 1.0) * _DistortionStrength; // Apply the distortion to the UV coordinates float2 distortedUV = i.uv + offset; // Get the color from the distorted UV coordinates fixed4 distortedColor = tex2D(_MainTex, distortedUV); // Add the distortion to the original color return texColor + (distortedColor - texColor); } ENDCG } } FallBack "Diffuse" } ``` 在使用这个Shader时,可以将原始纹理作为“_MainTex”属性的值,将扭曲图像作为“_Distortion”属性的值,然后通过修改“_DistortionStrength”属性的值来控制扭曲强度。 希望这个示例能够帮助你实现所需的效果
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值