用過TapTap的APP發現在排行榜的列表頁點擊單項會有一個進入詳情頁的過場效果,覺得很不錯, 小米的系統相冊也有類似的過場效果,個人對這個效果很有興趣,便決定自己也實現下這個效果。雖說做完Demo後瞭解到android 5.0以上的sdk有共享元素動畫的方式去實現,但是,這裏並不採用該方式。按照自己的思路來實現,記錄一下實現的過程。Demo是基於kotlin寫的。效果圖如下:
目錄
目錄
效果分析
從上面的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