1.需求
看小说时候感觉页面翻转动画挺有意思,现象如下:
2.分析现象
从图中可以看到。手指在上面View1左滑,上面View1随手左移,下面View2内容逐渐展示出来,这时至少存在两个页面展示。从现象来看,在上面View1滑动之前,下面View2数据就已经加载完成。手指在屏幕上滑动,最上层View1随手指滑动方向移动。抬手以后上面页面View会划出屏幕,让View2显示。
注意: View1代表上面滑View,View2代表下面被覆盖View。另外,须知整个过程只有View1滑动。
3.代码
3.1 创建布局文件
上面提到,这个界面需要至少两个View交互,才能完成上,下覆盖出现的效果。
首先需要考虑下最外层的GroupView,对于常用布局,FrameLayout和RelativeLayout可以做到view1整个盖在view2上面效果,而LinearLayout只会让View并排显示,其他可以实现覆盖效果的布局也可以,需要进行调研使用。多方面考虑相较于RelativeLayout,FrameLayout更友好,此处便使用这个父布局。
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="@color/cardview_light_background">
<TextView
android:id="@+id/tv1"
android:layout_margin="50dp"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:background="#333333"/>
<TextView
android:id="@+id/tv2"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:layout_margin="50dp"
android:background="#333333"/>
</FrameLayout>
3.2 滑动处理
3.2.1 随手移动
移动过程中,上层View需要根据手势判断向左还是向右移动,当前event.x大于上次保存startX值,表示需要将上面View1向左移动,此处使用view的translationX进行移动。相反当前event.x小于上次保存startX值则向右移动。
MotionEvent.ACTION_MOVE -> {
//上一页
if (event.x > startX) {
Log.d("TAG-sun", "onTouchEvent: 上一页")
movePage(1)
binding?.tv2?.translationX = event.x - startX
}
//下一页
if (event.x < startX) {
movePage(-1)
binding?.tv2?.translationX = event.x - startX
}
}
3.2.2 手指离开屏幕
手指离开屏幕后,会触发ACTION_UP,此时上层View1需要移出窗口,这里使用属性动画进行移动。screenWidth代表屏幕宽度。statu为1时表示上一个页面,statu为-1时表示下一个页面。从下面代码,ACTION_UP事件触发后可以根据statu值进行左右滑动区分。
MotionEvent.ACTION_UP -> {
transitionX?.setFloatValues(
binding?.tv2!!.translationX,
screenWidth!!.toFloat() * status
)
transitionX?.start()
}
3.2.3 View1位置恢复
由于在布局过程中View1始终是盖在View2上面的,为了体现出划入和划出的效果。在整个过程中,其实只有最上层的View1进行了滑动,View2进行数据的展示。那么对于 3.2.2 手指离开屏幕中我们将上层View1移出了窗口,此时我们需要将View1再次回到桌面,这个时候回来的过程我们需要悄悄进行,使用户看不出区别。可以这样做
addListener(object : Animator.AnimatorListener {
override fun onAnimationStart(animation: Animator) {
}
override fun onAnimationEnd(animation: Animator) {
updateText(text, binding?.tv2)
binding?.tv2?.alpha = 0f
binding?.tv2?.translationX = 0f
binding?.tv2?.alpha = 1f
}
override fun onAnimationCancel(animation: Animator) {
}
override fun onAnimationRepeat(animation: Animator) {
}
}
我们可以在动画开始和结束过程中做一些操作。上面代码中监听了MotionEvent.ACTION_UP时属性动画,当动画结束时做如下操作:
第一步:binding?.tv2?.alpha = 0f将View1隐藏
第二步:updateText(text, binding?.tv2)更新View1数据与View2数据同步
第三步:binding?.tv2?.translationX = 0f将View1移回到窗口
第四步:binding?.tv2?.alpha = 1f将View1显示出来,由于此时View1和View2数据完全相同,就能彻底覆盖,看不出有切换的效果。
3.2.4 完整代码
3.2.4.1 View代码
class PageMoveView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0)
: FrameLayout(context,attrs,defStyleAttr) {
val TAG = "PageMoveView"
var current = 0
var startX = 0f
var startY = 0f
var transitionX: ValueAnimator? = null
//1代表下一页 -1代表上一页
var status: Int = 1
var datas: MutableList<String> = mutableListOf<String>()
private var screenWidth: Int? = null
private var text: String? = null
private var interceptorUpdate = true
private var interceptorMove = false
private var binding: MovePageViewBinding? = null
init {
screenWidth = getScreenWidth(context)
binding = MovePageViewBinding.inflate(LayoutInflater.from(context))
val viewGroup = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
addView(binding!!.root, viewGroup)
}
fun setData(datas: MutableList<String>) {
this.datas = datas
initView()
}
private fun initView() {
transitionX = getAnimatorValue("transitionX")?.apply {
addUpdateListener {
binding?.tv2?.translationX = it.animatedValue as Float
}
addListener(object : Animator.AnimatorListener {
override fun onAnimationStart(animation: Animator) {
}
override fun onAnimationEnd(animation: Animator) {
updateText(text, binding?.tv2)
interceptorUpdate = true
binding?.tv2?.alpha = 0f
binding?.tv2?.translationX = 0f
binding?.tv2?.alpha = 1f
}
override fun onAnimationCancel(animation: Animator) {
}
override fun onAnimationRepeat(animation: Animator) {
}
})
}
if (datas.size <= 0) {
Log.d(TAG, "initView: ")
} else {
//第一页和第二页加载相同页面
updateText(datas[current], binding?.tv1)
updateText(datas[current], binding?.tv2)
}
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
startX = event.x
startY = event.y
}
MotionEvent.ACTION_MOVE -> {
//上一页
if (event.x > startX) {
Log.d("TAG-sun", "onTouchEvent: 上一页")
movePage(1)
binding?.tv2?.translationX = event.x - startX
}
//下一页
if (event.x < startX) {
movePage(-1)
binding?.tv2?.translationX = event.x - startX
}
}
MotionEvent.ACTION_UP -> {
transitionX?.setFloatValues(
binding?.tv2!!.translationX,
screenWidth!!.toFloat() * status
)
transitionX?.start()
}
MotionEvent.ACTION_CANCEL -> {
transitionX?.setFloatValues(
binding?.tv2!!.translationX,
screenWidth!!.toFloat() * status
)
transitionX?.start()
}
}
return super.onTouchEvent(event)
}
private fun movePage(status: Int) {
this.status = status
if (current >= 0 && datas.size > 0) {
//每次近来北京还一次锁死
if (interceptorUpdate) {
if (status == 1) {
//下一页
current = (current + 1) % datas.size
} else {
if (current == 0) {
current = datas.size - 1
} else {
current -= 1
}
}
text = datas[current]
updateText(text, binding!!.tv1)
interceptorUpdate = false
}
}
}
private fun updateText(text: String?, textView: TextView?) {
text.let {
textView?.text = text
}
}
fun getScreenWidth(context: Context): Int {
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
val display = windowManager.defaultDisplay
val size = Point()
display.getSize(size)
return size.x
}
var animaValue = HashMap<String, ValueAnimator>()
fun getAnimatorValue(key: String): ValueAnimator? {
if (animaValue[key] == null) {
animaValue[key] = ValueAnimator.ofFloat().apply {
duration = 100
}
}
return animaValue[key]
}
}
3.2.4.2 View中加载的布局文件
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="@color/cardview_light_background">
<TextView
android:id="@+id/tv1"
android:layout_margin="50dp"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:background="#333333"/>
<TextView
android:id="@+id/tv2"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:layout_margin="50dp"
android:background="#333333"/>
</FrameLayout>
3.2.4.3 Activity布局文件
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="@color/cardview_light_background">
<com.kt.view.move.PageMoveView
android:id="@+id/page"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusable="true"
android:clickable="true"/>
</FrameLayout>
3.2.4.4 Activity文件
class PageMoveActivity() : BaseActivity<MovePageActivityBinding>() {
var textList = mutableListOf<String>().apply {
add("第一页")
add("第二页")
add("第三页")
add("第四页")
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding.page.setData(textList)
}
}
3.3 效果展示