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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章