I am participating in the "Nuggets · Sailing Program"
write in front
This article is based ViewPager2
on the Banner
effect achieved, and then realizes the effect of imitating Taobao and Jingdong Banner
when swiping to the last page and continuing to swipe to view the details of the pictures and texts. About ViewPager2
the principle and its packaging, you can refer to the previous two articles:
1. Android in-depth understanding of ViewPager2 principle and practice (Part 1)
2. Android in-depth understanding of ViewPager2 principle and practice (Part 2)
renderings
Principle analysis
Banner
The one on the right查看更多View
is wrapped, the default子View
width is , and the one on the right side of the screen is invisible;父View
Banner
match_parent
查看更多
- When
Banner
sliding left and right, the current sliding event isBanner
consumed in , that is, it父View
will not be intercepted. - When you
Banner
swipe to the far right and continue to swipe,父View
the event will be intercepted at this time, so that the event will be父View
taken over and the父View
eventonTouchEvent()
will be consumed in ,父View
and the content in the swipe can be swiped. How to slide it? During theMOVE
event, byscrollTo()/scrollBy()
sliding, and during theUP/CANCEL
event, you need toScroller
automaticallystartScroll()
slide to子View
the left or right to view more, so as to complete the consumption of an event; - When the
UP/CANCEL
event is triggered,子View
the distance to view more swipes is more than half, and it is considered that more operations need to be triggered to view more. Of course, the values here can be set by yourself.
core code
- TJBannerFragment.kt
/**
* 仿淘宝京东宝贝详情Fragment
*/
class TJBannerFragment : BaseFragment() {
private val mModels: MutableList<Any> = mutableListOf()
private val mContainer: VpLoadMoreView by id(R.id.vp2_load_more)
override fun getLayoutId(): Int {
return R.layout.fragment_tx_news_n
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
initVerticalTxScroll()
}
private fun initVerticalTxScroll() {
mModels.add(TxNewsModel(MConstant.IMG_4, "美轮美奂节目", "奥运五环缓缓升起"))
mModels.add(TxNewsModel(MConstant.IMG_1, "精美商品", "9块9包邮"))
mContainer.setData(mModels) {
showToast("打开更多页面")
}
}
}
复制代码
- VpLoadMoreView.kt (parent View)
class VpLoadMoreView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0,
) : LinearLayout(context, attrs, defStyle) {
private val mMVPager2: MVPager2 by id(R.id.mvp_pager2)
private var mNeedIntercept: Boolean = false //是否需要拦截VP2事件
private val mLoadMoreContainer: LinearLayout by id(R.id.load_more_container)
private val mIvArrow: ImageView by id(R.id.iv_pull)
private val mTvTips: TextView by id(R.id.tv_tips)
private var mCurPos: Int = 0 //Banner当前滑动的位置
private var mLastX = 0f
private var mLastDownX = 0f //用于判断滑动方向
private var mMenuWidth = 0 //加载更多View的宽度
private var mShowMoreMenuWidth = 0 //加载更多发生变化时的宽度
private var mLastStatus = false // 默认箭头样式
private var mAction: (() -> Unit)? = null
private var mScroller: OverScroller
private var isTouchLeft = false //是否是向左滑动
private var animRightStart = RotateAnimation(0f, -180f,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f).apply {
duration = 300
fillAfter = true
}
private var animRightEnd = RotateAnimation(-180f, 0f,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f).apply {
duration = 300
fillAfter = true
}
init {
orientation = HORIZONTAL
View.inflate(context, R.layout.fragment_tx_news, this)
mScroller = OverScroller(context)
}
/**
* @param mModels 要加载的数据
* @param action 回调Action
*/
fun setData(mModels: MutableList<Any>, action: () -> Unit) {
this.mAction = action
mMVPager2.setModels(mModels)
.setLoop(false) //非循环模式
.setIndicatorShow(false)
.setLoader(TxNewsLoader(mModels))
.setPageTransformer(CompositePageTransformer().apply {
addTransformer(MarginPageTransformer(15))
})
.setOrientation(ViewPager2.ORIENTATION_HORIZONTAL)
.setAutoPlay(false)
.setOnBannerClickListener(object : OnBannerClickListener {
override fun onItemClick(position: Int) {
showToast(mModels[position].toString())
}
})
.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageScrollStateChanged(state: Int) {
if (mCurPos == mModels.lastIndex && isTouchLeft && state == ViewPager2.SCROLL_STATE_DRAGGING) {
//Banner在最后一页 & 手势往左滑动 & 当前是滑动状态
mNeedIntercept = true //父View可以拦截
mMVPager2.setUserInputEnabled(false) //VP2设置为不可滑动
}
}
override fun onPageSelected(position: Int) {
mCurPos = position
}
})
.start()
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
mMenuWidth = mLoadMoreContainer.measuredWidth
mShowMoreMenuWidth = mMenuWidth / 3 * 2
super.onLayout(changed, l, t, r, b)
}
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
when (ev?.action) {
MotionEvent.ACTION_DOWN -> {
mLastX = ev.x
mLastDownX = ev.x
}
MotionEvent.ACTION_MOVE -> {
isTouchLeft = mLastDownX - ev.x > 0 //判断滑动方向
}
}
return super.dispatchTouchEvent(ev)
}
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
var isIntercept = false
when (ev?.action) {
MotionEvent.ACTION_MOVE -> isIntercept = mNeedIntercept //是否拦截Move事件
}
//log("ev?.action: ${ev?.action},isIntercept: $isIntercept")
return isIntercept
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(ev: MotionEvent?): Boolean {
when (ev?.action) {
MotionEvent.ACTION_MOVE -> {
val mDeltaX = mLastX - ev.x
if (mDeltaX > 0) {
//向左滑动
if (mDeltaX >= mMenuWidth || scrollX + mDeltaX >= mMenuWidth) {
//右边缘检测
scrollTo(mMenuWidth, 0)
return super.onTouchEvent(ev)
}
} else if (mDeltaX < 0) {
//向右滑动
if (scrollX + mDeltaX <= 0) {
//左边缘检测
scrollTo(0, 0)
return super.onTouchEvent(ev)
}
}
showLoadMoreAnim(scrollX + mDeltaX)
scrollBy(mDeltaX.toInt(), 0)
mLastX = ev.x
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
smoothCloseMenu()
mNeedIntercept = false
mMVPager2.setUserInputEnabled(true)
//执行回调
val mDeltaX = mLastX - ev.x
if (scrollX + mDeltaX >= mShowMoreMenuWidth) {
mAction?.invoke()
}
}
}
return super.onTouchEvent(ev)
}
private fun smoothCloseMenu() {
mScroller.forceFinished(true)
/**
* 左上为正,右下为负
* startX:X轴开始位置
* startY: Y轴结束位置
* dx:X轴滑动距离
* dy:Y轴滑动距离
* duration:滑动时间
*/
mScroller.startScroll(scrollX, 0, -scrollX, 0, 300)
invalidate()
}
override fun computeScroll() {
if (mScroller.computeScrollOffset()) {
showLoadMoreAnim(0f) //动画还原
scrollTo(mScroller.currX, mScroller.currY)
invalidate()
}
}
private fun showLoadMoreAnim(dx: Float) {
val showLoadMore = dx >= mShowMoreMenuWidth
if (mLastStatus == showLoadMore) return
if (showLoadMore) {
mIvArrow.startAnimation(animRightStart)
mTvTips.text = "释放查看图文详情"
mLastStatus = true
} else {
mIvArrow.startAnimation(animRightEnd)
mTvTips.text = "滑动查看图文详情"
mLastStatus = false
}
}
}
复制代码
父View
The comments are very clear, so there is no need to explain too much. Here is one thing to pay attention to. It is known that Banner
when sliding on the last page, you need to judge the sliding direction: if you continue to slide to the left, you need to 父View
intercept the sliding event and consume it yourself; when you slide to the right, 父View
do not Sliding events need to be processed, Banner
and event consumption is still performed.
However 滑动方向需要起始位置(DOWN事件)的X坐标 - 滑动时的X坐标(MOVE事件) 的差值进行判断
, where does the problem take the X coordinate of the starting position? 父View
Are you onInterceptTouchEvent()->DOWN事件
there? This is not possible, because the sliding direction is MOVE事件
judged in the in, and 父View
if onInterceptTouchEvent()->DOWN事件
it is intercepted in the in, the subsequent events will not Banner
be passed in. Here you can choose 父View
to dispatchTouchEvent()->DOWN事件
solve it.
The XML layout corresponding to VpLoadMoreView :
<?xml version="1.0" encoding="utf-8"?>
<merge 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"
android:background="@color/white"
android:orientation="horizontal"
tools:parentTag="android.widget.LinearLayout">
<!--ViewPager2-->
<org.ninetripods.lib_viewpager2.MVPager2
android:id="@+id/mvp_pager2"
android:layout_width="match_parent"
android:layout_height="200dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!--加载更多View-->
<LinearLayout
android:id="@+id/load_more_container"
android:layout_width="100dp"
android:layout_height="200dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/iv_pull"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="10dp"
android:src="@drawable/icon_arrow_pull" />
<TextView
android:id="@+id/tv_tips"
android:layout_width="16dp"
android:layout_height="match_parent"
android:layout_marginStart="10dp"
android:gravity="center_vertical"
android:text="滑动查看图文详情"
android:textColor="#333333"
android:textSize="14sp"
android:textStyle="bold" />
</LinearLayout>
</merge>
复制代码
Here , and must 父View(VpLoadMoreView)
be LinearLayout
a horizontal layout, XML
the label used in the top-level layout merge
, which can not only optimize the layout of one layer, but also 父View
directly operate and load the graphic details in it 子View
.
Source address
For the complete code address, see: Android imitation Taobao, Jingdong Banner swipe to the end to view the graphic details