用过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。
Activity默认的转场效果是从右往左,从gif的效果图上看是进场Activity直接从上覆盖到原Activity,所以这里可以用以下代码来实现,可以看成是透明度进场时长为0的动画效果,也可以定义动画xml文件来控制时长。
overridePendingTransition(0,0)
从列表页跳转到详情页需要传递列表页当前项图片的View信息,从详情页回到列表页也需要传递信息,所以跳转这里用startActivityForResult方法来实现
这里我们定义一个类来记录页面传的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{
}
根布局采用的是FrameLayout帧布局,新添加的ImageView处于最上的图层,通过坐标方便的定位到ImageView在根布局所处的位置(只要是继承于ViewGroup都可以)
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;
}
列表页布局
详情页布局
item布局
布局上都比较简单,没什么可以讲的
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(),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