上篇文章安卓两个页面组件的无缝衔接part1(共享元素)
介绍了如何使用安卓系统提供的共享元素来实现多个页面之间元素的无缝衔接,本文讨论第二个方案:利用属性动画移动一个全局View,先看效果
该方案的原理是利用ViewAttr
类记录第一个页面和第二个页面的需要衔接的View的属性(x、y、width、height),然后在在页面切换时对View执行属性动画。这个方案的效果和上篇文章的效果差不多,页面之间View元素之间的衔接很丝滑,播放视频没有卡帧、跳帧现象,而且音频也没有被中断
一 第一个页面MainAct
MainAct.kt
class MainAct : AppCompatActivity() {
lateinit var bind: ActivityMain2Binding
var playPaused: Boolean = false
var clickGotoPage2 = false
lateinit var playViewLp: ViewGroup.LayoutParams
var attr: ViewAttr? = null
var mPlayer: SysMediaPlayer = SysMediaPlayer.getInstance()
var mSurface: Surface? = null
var seamlessCb = object : SeamlessObserver.Callback {
override fun onEvent(type: Int, currentAttr: ViewAttr?) {
Log.w("zzh", "on event on mainact type=${type}")
if (type == 2) {
// var target = TextureView(this@MainAct)
// ViewMgr.getInstance().target = target
var target = ViewMgr.getInstance().target
val parent: ViewParent? = target.getParent()
if (parent != null) {
(parent as ViewGroup).removeView(target)
}
// initTextureListener(target as? TextureView)
bind.root.addView(target, playViewLp)
Log.w("zzh", "back to main act currentAttr=${currentAttr}")
Log.w("zzh", "back to main act attr=${attr}")
}
clickGotoPage2 = false
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bind = ActivityMain2Binding.inflate(layoutInflater)
supportActionBar?.title = "MainActInSeamless"
setContentView(bind.root)
SeamlessObserver.getInstance().register(seamlessCb)
playViewLp = bind.playView.layoutParams
bind.btnGotoSingleSeamlessAct.setOnClickListener { v -> onClickGotoSingleSeamless(v) }
initTextureListener(bind.playView)
}
private fun onClickGotoSingleSeamless(v: View) {
clickGotoPage2 = true
ViewMgr.getInstance().target = bind.playView // important
val intent = Intent(this@MainAct, SingleSeamlessListAct::class.java)
if (attr == null) {
attr = ViewAttr()
val location = IntArray(2)
bind.playView.getLocationInWindow(location)
// attr?.setX(location[0])
// attr?.setY(location[1])
attr?.setX(bind.playView.x.toInt())
attr?.setY(bind.playView.y.toInt())
attr?.setWidth(bind.playView.getMeasuredWidth())
attr?.setHeight(bind.playView.getMeasuredHeight())
}
Log.w("zzh", "goto single seamless act attr=${attr}")
intent.putExtra("attr", attr)
startActivity(intent)
overridePendingTransition(0, 0)
}
private fun initTextureListener(textureView: TextureView?) {
textureView?.surfaceTextureListener = object : TextureView.SurfaceTextureListener {
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, w: Int, h: Int) {
Log.w("zzh", "surface log available in main act w=$w h=$h thread=${Thread.currentThread()}")
if (mSurface == null) {
mPlayer!!.setAudioStreamType(AudioManager.STREAM_MUSIC)
mPlayer!!.setDataSource("https://hw-v.cztv.com/cztv/vod/2023/11/11/0b1098f4e2dc7e4ba4e8a0ceb39ccadf/0b1098f4e2dc7e4ba4e8a0ceb39ccadf_h264_800k_mp4.mp4_playlist.m3u8")
mPlayer!!.mInternalMediaPlayer.setOnPreparedListener(object : MediaPlayer.OnPreparedListener {
override fun onPrepared(mp: MediaPlayer?) {
mPlayer!!.start()
}
})
mPlayer!!.mInternalMediaPlayer.prepare()
}
mSurface = Surface(surface)
mPlayer!!.setSurface(mSurface)
}
override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, w: Int, h: Int) {
Log.w("zzh", "surface log size changed in main activity w=$w h=$h")
}
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
Log.w("zzh", "surface log destroy in main activity")
return false
}
override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {
}
}
}
override fun onPause() {
super.onPause()
if (!clickGotoPage2) {
mPlayer.pause()
playPaused = true
}
}
override fun onResume() {
if (playPaused) {
mPlayer.start()
}
clickGotoPage2 = false
super.onResume()
}
override fun onDestroy() {
SeamlessObserver.getInstance().unregister(seamlessCb)
super.onDestroy()
}
}
activity_main2.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".singleseamless.MainAct">
<TextureView
android:id="@+id/playView"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_width="match_parent"
android:layout_height="160dp" />
<Button
android:id="@+id/btnGotoSingleSeamlessAct"
android:layout_marginTop="20dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="GotoSingleSeamlessAct"
app:layout_constraintTop_toBottomOf="@id/playView"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
在函数onClickGotoSingleSeamless
里,我们记录了要移动的View的属性数据(x、y、width、height)存储到ViewAttr
并将数据通过Intent传递给下个页面,在下个页面进行属性动画的执行。
二 第二个页面SingleSeamlessListAct
SingleSeamlessListAct.kt
class SingleSeamlessListAct : AppCompatActivity() {
lateinit var bind: ActivitySingleSeamlessListBinding
lateinit var mAdapter: SingleAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bind = ActivitySingleSeamlessListBinding.inflate(layoutInflater)
supportActionBar?.title = "SingleSeamlessListAct"
setContentView(bind.root)
var lm = LinearLayoutManager(this)
lm.orientation = RecyclerView.VERTICAL
bind.recycler.layoutManager = lm
var attr: ViewAttr? = intent.getParcelableExtra("attr")
mAdapter = SingleAdapter(this)
mAdapter.setAttr(attr)
mAdapter.setRoot(bind.root)
bind.recycler.adapter = mAdapter
}
override fun onBackPressed() {
SeamlessObserver.getInstance().execute(2, mAdapter.getCurAttr())
ViewMoveHelper(ViewMgr.getInstance().target, mAdapter.getCurAttr(), mAdapter.getAttr(), ViewMgr.DURATION, object :
ViewMoveHelper.EndCallback {
override fun onEnd() {
}
}).startAnim()
super.onBackPressed()
overridePendingTransition(0, 0)
}
}
activity_single_seamless_list.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".SecondListAct">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
SingleAdapter.java
class SingleAdapter : RecyclerView.Adapter<SingleAdapter.ThisVH> {
private val mDatas: ArrayList<String> = ArrayList<String>()
private var mSelected = -1
private var mLayoutId = -1
private var currentAttr: ViewAttr? = null
private var attr: ViewAttr? = null
private var root: ViewGroup? = null
var mPlayer: SysMediaPlayer = SysMediaPlayer.getInstance()
var mSurface: Surface? = null
constructor(ctx: Context, defaultIndex: Int = 2, layoutId: Int = R.layout.list_item_single) {
for (i in 0..6) {
mDatas.add("index:$i")
}
mSelected = defaultIndex
mLayoutId = layoutId
}
public fun setRoot(vg: ViewGroup) {
root = vg
}
public fun setAttr(a: ViewAttr?) {
attr = a
}
public fun getAttr(): ViewAttr? {
return attr
}
public fun getCurAttr(): ViewAttr? {
return currentAttr
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ThisVH {
val view: View =
LayoutInflater.from(parent.context).inflate(mLayoutId, parent, false)
return ThisVH(parent, view)
}
override fun getItemCount(): Int {
return mDatas.size
}
override fun onBindViewHolder(holder: ThisVH, position: Int) {
holder.bind(position)
}
inner class ThisVH(parent: ViewGroup, itemView: View) : RecyclerView.ViewHolder(itemView) {
var container: FrameLayout
init {
container = itemView.findViewById(R.id.container)
}
fun bind(pos: Int) {
Log.w("zzh", "bind view holder pos=$pos holder=$this")
if (mSelected == pos) {
container.viewTreeObserver.addOnPreDrawListener(object :
ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
container.viewTreeObserver.removeOnPreDrawListener(this)
var target = ViewMgr.getInstance().target
val parent: ViewParent? = target.getParent()
if (parent != null) {
(parent as ViewGroup).removeView(target)
}
root!!.addView(target, FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
currentAttr = ViewAttr()
val location = IntArray(2)
container.getLocationInWindow(location)
currentAttr!!.setX(location[0])
// currentAttr!!.setY(location[1])
currentAttr!!.setY(itemView.y.toInt())
currentAttr!!.setWidth(container.measuredWidth)
currentAttr!!.setHeight(container.measuredHeight)
Log.w("zzh", "on pre draw attr0=${attr}")
Log.w("zzh", "on pre draw attr1=${currentAttr} newparent=${target.parent}")
ViewMoveHelper(target, attr, currentAttr, ViewMgr.DURATION, object : EndCallback {
override fun onEnd() {
Log.w("zzh", "on pre draw attr2=${currentAttr} newparent=${target.parent}")
}
}).startAnim()
// val animation = AlphaAnimation(0f, 1f)
// animation.duration = ViewMgr.DURATION
// llContent.setAnimation(animation)
// animation.start()
return true
}
})
} else {
}
}
}
}
list_item_single.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/teal_700"
android:layout_marginBottom="5dp">
<FrameLayout
android:id="@+id/container"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_width="match_parent"
android:layout_height="160dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
在SingleAdapter的onBindViewHolder中,对View执行属性动画,开始的属性是View在在第一个页面的属性,结束的属性是在第二个页面即当前页面的属性,两个页面共用一个播放器实例,保证视频和音频的流畅播放和丝滑衔接。
三 其余辅助类源码
ViewMgr.java
public class ViewMgr {
public static final long DURATION = 250;
private View mTarget;
private ViewMgr() {
}
private static class Holder {
private static ViewMgr INSTANCE = new ViewMgr();
}
public static ViewMgr getInstance() {
return Holder.INSTANCE;
}
public View getTarget() {
return mTarget;
}
public void setTarget(View v) {
mTarget = v;
}
}
SysMediaPlayer.java
public class SysMediaPlayer {
public MediaPlayer mInternalMediaPlayer;
private enum PlayerState {
STATE_ERROR,
STATE_IDLE,
STATE_INITIALIZED,
STATE_PREPARING,
STATE_PREPARED,
STATE_STARTED,
STATE_PAUSED,
STATE_STOPPED,
STATE_COMPLETE,
}
private PlayerState mPlayerState;
public SysMediaPlayer(Context context, int resId) {
mInternalMediaPlayer = MediaPlayer.create(context, resId);
}
private SysMediaPlayer() {
mInternalMediaPlayer = new MediaPlayer();
}
private static class Holder {
private static SysMediaPlayer INSTANCE = new SysMediaPlayer();
}
public static SysMediaPlayer getInstance() {
return Holder.INSTANCE;
}
public void setDataSource(String s) {
try {
Map<String, String> headers = new HashMap<String, String>();
mInternalMediaPlayer.setDataSource(s);
} catch (Exception e) {
Log.e("zzh", "set data source error", e);
}
}
public void setDataSource(Context context, Uri s) {
try {
mInternalMediaPlayer.setDataSource(context, s);
} catch (Exception e) {
Log.e("zzh", "set data source2 error", e);
}
}
public void setAudioStreamType(int type) {
mInternalMediaPlayer.setAudioStreamType(type);
}
public void setSurface(Surface surface) {
mInternalMediaPlayer.setSurface(surface);
}
public void prepareAsync() throws IllegalStateException {
if (mPlayerState == PlayerState.STATE_INITIALIZED || mPlayerState == PlayerState.STATE_STOPPED) {
mInternalMediaPlayer.prepareAsync();
mPlayerState = PlayerState.STATE_PREPARING;
}
}
public void prepare() {
try {
mInternalMediaPlayer.prepare();
mPlayerState = PlayerState.STATE_PREPARED;
} catch (IOException e) {
Log.e("zzh", "prepare error", e);
}
}
public void start() throws IllegalStateException {
// if (mPlayerState == PlayerState.STATE_PREPARED || mPlayerState == PlayerState.STATE_COMPLETE || mPlayerState == PlayerState.STATE_PAUSED) {
// // 必须在onPrepared后设置,如果onPrepared时设置相当于调用start/pause,所以移到start接口
// // realSetSpeed();
// mInternalMediaPlayer.start();
// }
mInternalMediaPlayer.start();
mPlayerState = PlayerState.STATE_STARTED;
}
public void pause() throws IllegalStateException {
if (mPlayerState == PlayerState.STATE_STARTED) {
}
mPlayerState = PlayerState.STATE_PAUSED;
mInternalMediaPlayer.pause();
}
public void stop() throws IllegalStateException {
if (mPlayerState == PlayerState.STATE_STARTED ||
mPlayerState == PlayerState.STATE_PREPARED ||
mPlayerState == PlayerState.STATE_PAUSED ||
mPlayerState == PlayerState.STATE_COMPLETE) {
mInternalMediaPlayer.stop();
mPlayerState = PlayerState.STATE_STOPPED;
}
}
public void release() {
mInternalMediaPlayer.release();
}
}
SeamlessObserver.java
public class SeamlessObserver {
private List<Callback> callbacks = new ArrayList<>();
private SeamlessObserver(){}
private static class Holder {
private static SeamlessObserver INSTANCE = new SeamlessObserver();
}
public static SeamlessObserver getInstance() {
return Holder.INSTANCE;
}
public void register(Callback cb) {
if (cb == null || callbacks.contains(cb)) {
return;
}
callbacks.add(cb);
}
public void unregister(Callback cb) {
if (!callbacks.contains(cb)) {
return;
}
callbacks.remove(cb);
}
public void execute(int type, ViewAttr attr) {
if (callbacks.size() > 0) {
Callback callback = callbacks.get(callbacks.size() - 1);
callback.onEvent(type, attr);
}
// for (Callback callback : callbacks) {
// }
}
public interface Callback {
void onEvent(int type, ViewAttr attr);
}
}
ViewAttr.java
public class ViewAttr implements Parcelable {
public static final Creator<ViewAttr> CREATOR = new Creator<ViewAttr>() {
@Override
public ViewAttr createFromParcel(Parcel in) {
return new ViewAttr(in);
}
@Override
public ViewAttr[] newArray(int size) {
return new ViewAttr[size];
}
};
private int x;
private int y;
private int width;
private int height;
public ViewAttr() {
}
protected ViewAttr(Parcel in) {
x = in.readInt();
y = in.readInt();
width = in.readInt();
height = in.readInt();
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(x);
dest.writeInt(y);
dest.writeInt(width);
dest.writeInt(height);
}
@Override
public String toString() {
return "ViewAttr{" +
"x=" + x +
", y=" + y +
", width=" + width +
", height=" + height +
'}';
}
}
ViewMoveHelper.java
public class ViewMoveHelper {
private View targetView;
private ViewAttr fromViewInfo;
private ViewAttr toViewInfo;
private long duration;
private EndCallback mEndCallback = new EndCallback() {
};
/**
* @param targetView 目标布局
* @param fromViewInfo 起始view坐标信息
* @param toViewInfo 目标view坐标信息
* @param duration 动画时长
*/
public ViewMoveHelper(View targetView, ViewAttr fromViewInfo, ViewAttr toViewInfo, long duration, @NonNull EndCallback callback) {
this.targetView = targetView;
this.fromViewInfo = fromViewInfo;
this.toViewInfo = toViewInfo;
this.duration = duration;
mEndCallback = callback;
}
public void startAnim() {
ObjectAnimator xAnim = ObjectAnimator.ofFloat(targetView, "x", fromViewInfo.getX(), toViewInfo.getX());
ObjectAnimator yAnim = ObjectAnimator.ofFloat(targetView, "y", fromViewInfo.getY(), toViewInfo.getY());
ValueAnimator widthAnim = ValueAnimator.ofInt(fromViewInfo.getWidth(), toViewInfo.getWidth());
ValueAnimator heightAnim = ValueAnimator.ofInt(fromViewInfo.getHeight(), toViewInfo.getHeight());
widthAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
ViewGroup.LayoutParams param = targetView.getLayoutParams();
param.width = (int) valueAnimator.getAnimatedValue();
targetView.setLayoutParams(param);
}
});
heightAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
ViewGroup.LayoutParams param = targetView.getLayoutParams();
param.height = (int) valueAnimator.getAnimatedValue();
targetView.setLayoutParams(param);
}
});
AnimatorSet animation = new AnimatorSet();
animation.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(@NonNull Animator animation) {
}
@Override
public void onAnimationEnd(@NonNull Animator animation) {
mEndCallback.onEnd();
}
@Override
public void onAnimationCancel(@NonNull Animator animation) {
}
@Override
public void onAnimationRepeat(@NonNull Animator animation) {
}
});
animation.playTogether(xAnim, yAnim, widthAnim, heightAnim);
animation.setDuration(duration);
animation.setInterpolator(new DecelerateInterpolator());
animation.start();
}
public interface EndCallback {
default void onEnd() {
}
}
}
对比上个文章里介绍的共享元素的方案,还是推荐共享元素的方案,原因:
- 代码少。动画通过系统的Scene和Transition实现,不用像第二种方案里那样手搓实现属性动画,不用计算和维护x、y、width、height等属性;
- 效果好。其实对比下,还是共享元素的效果比较好,因为用到了一个GhostView的东西,它的作用是在不改变view的parent的情况下,将view绘制在另一个parent下,这就避免了第二种方案里的来回removeView和addView的操作。
参考文献: