AndroidBanner - ViewPager 03
上一篇文章,描述瞭如何實現自動輪播的,以及手指觸控的時候停止輪播,抬起繼續輪播,其實還遺留了一些問題:
- 當banner不可見的時候,也需要停止輪播
- 給banner設定點選事件,長時間的觸控也會被預設是一個點選事件
這篇文章就來解決這些問題,並處理一下banner的曝光打點問題。
解決banner 不可見依舊輪播的問題
思考一下:什麼時候可以輪播,什麼時候不可以輪播
當Banner新增到螢幕上,且對使用者可見的時候,可以開始輪播
當Banner從螢幕上移除,或者Banner不可見的時候,可以停止輪播
當手指觸控到Banner時,停止輪播
當手指移開時,開始輪播
所以,我們需要知道什麼時候View可見,不可見,新增到螢幕上和從螢幕上移除,幸運的是,這些,android都提供了對應的介面來獲取。
OnAttachStateChangeListenner
該介面可以通知我們view新增到螢幕上或者從螢幕上被移除,或者可以直接重寫view的onAttachedToWindow和onDetachedFromWindow方法
// view提供的介面,可以透過 addOnAttachStateChangeListener 新增艦艇
public interface OnAttachStateChangeListener {
public void onViewAttachedToWindow(@NonNull View v);
public void onViewDetachedFromWindow(@NonNull View v);
}
// 複寫view的方法
override fun onAttachedToWindow() {
super.onAttachedToWindow()
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
}
這裡我們透過複寫方法的方式處理
onVisibilityChanged
view 提供了方法,可以複寫該方法,獲取到view 的可見性變化
protected void onVisibilityChanged(@NonNull View changedView, @Visibility int visibility) {
}
onWindowVisibilityChanged
view 提供了方法,可以複習該方法,當前widow的可見性發生變化的時候,會呼叫通知給我們
protected void onWindowVisibilityChanged(@Visibility int visibility) {
if (visibility == VISIBLE) {
initialAwakenScrollBars();
}
}
我們根據上面的api,可以封裝一個介面,來監聽View的可見性
VisibleChangeListener
interface VisibleChangeListener {
/**
* view 可見
*/
fun onShown()
/**
* view 不可見
*/
fun onDismiss()
}
Banner重寫方法,進行呼叫
override fun onVisibilityChanged(changedView: View, visibility: Int) {
Log.e(TAG, "onVisibilityChanged ${changedView == this}, vis: $visibility")
dispatchVisible(visibility)
}
override fun onWindowVisibilityChanged(visibility: Int) {
super.onWindowVisibilityChanged(visibility)
Log.e(TAG, "onWindowVisibilityChanged $visibility")
dispatchVisible(visibility)
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
Log.e(TAG, "onAttachedToWindow ")
this.mAttached = true
}
override fun onDetachedFromWindow() {
Log.e(TAG, "onDetachedFromWindow ")
super.onDetachedFromWindow()
this.mAttached = false
}
private fun dispatchVisible(visibility: Int) {
val visible = mAttached && visibility == VISIBLE
if (visible) {
prepareLoop()
} else {
stopLoop()
}
mVisibleChangeListener?.let {
when (visible) {
true -> it.onShown()
else -> it.onDismiss()
}
}
}
頁面滾動時處理banner輪播
滾動監聽,如果是scrollview,就監聽滾動事件處理即可。如果是listview,recyclerview可以選擇監聽onscrollstatechanged,更高效。
下面是scrollview的監聽處理
mBinding.scrollView.setOnScrollChangeListener(object :OnScrollChangeListener{
override fun onScrollChange(
v: View?,
scrollX: Int,
scrollY: Int,
oldScrollX: Int,
oldScrollY: Int
) {
val visible = mBinding.vpBanner.getGlobalVisibleRect(Rect())
Log.e(TAG,"banner visible : $visible")
if(visible){
mBinding.vpBanner.startLoop()
}else{
mBinding.vpBanner.stopLoop()
}
}
})
點選事件的處理
首先要宣告一個點選事件回撥介面
interface PageClickListener {
fun onPageClicked(position: Int)
}
重寫banner的onTouch事件,將移動距離小於100,且按壓時間小於500ms的事件認為是點選事件
private var mMoved = false
private var mDownX = 0F
private var mDownY = 0F
/**
* 當前事件流結束時,恢復touch處理的相關變數
*/
private fun initTouch() {
this.mMoved = false
this.mDownX = 0F
this.mDownY = 0F
}
private fun calculateMoved(x: Float, y: Float, ev: MotionEvent) {
mClickListener?.let {
// 超過500ms(系統預設的時間) 我們認為不是點選事件
if (ev.eventTime - ev.downTime >= 500) {
return
}
// 移動小於閾值我們認為是點選
if (sqrt(((x - mDownX).pow(2) + (y - mDownY).pow(2))) >= MOVE_FLAG) {
return
}
val count = adapter?.count ?: 0
if (count == 0) {
return
}
// 由於我們實現無限輪播的方式是重新設定當前選中的item,這裡要將currentItem重新對映回去
val index = when (currentItem) {
in 1..count - 2 -> currentItem - 1
0 -> count - 1
else -> 0
}
it.onPageClicked(index)
}
}
override fun onTouchEvent(ev: MotionEvent?): Boolean {
when (ev?.action) {
MotionEvent.ACTION_DOWN -> {
this.mDownY = ev.y
this.mDownX = ev.x
stopLoop()
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
val y = ev.y
val x = ev.x
calculateMoved(x, y, ev)
initTouch()
prepareLoop()
}
}
return super.onTouchEvent(ev)
}
曝光打點的處理
監聽page切換,當page變化的時候,從實際展示的資料佇列中取出資料進行曝光。
class ExposureHelper(private val list: List<*>, private var last: Int = -1) :
ViewPager.OnPageChangeListener {
private var mStart: AtomicBoolean = AtomicBoolean(false);
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) =
Unit
override fun onPageSelected(position: Int) {
Log.e(TAG, "$position $last")
if (last >= 0) {
exposure()
}
last = position
}
override fun onPageScrollStateChanged(state: Int) = Unit
/**
* 開始曝光
* @param current Int
*/ fun startExposure(current: Int) {
mStart.set(true)
last = current
}
/**
* 停止曝光
*/
fun endExposure() {
if (mStart.get()) {
mStart.set(false)
exposure()
}
}
/**
* 實際執行資料上報的處理
*/
private fun exposure() {
val data = list[last]
Log.e(TAG, "data:$data")
}
companion object {
private const val TAG = "ExposureHelper"
}
}
VPAdapter 對外提供實際展示的資料集
private val mData = mutableListOf<T>()
fun setData(data: List<T>) {
mData.clear()
if (this.loop && data.size > 1) {
// 陣列組織一下,用來實現無限輪播
mData.add(data[data.size - 1])
mData.addAll(data)
mData.add(data[0])
} else {
mData.addAll(data)
}
}
fun getShowDataList():List<T>{
return mData
}
在Banner中的配置使用
private var mExposureHelper: ExposureHelper? = null
/**
* 自動輪播
*/
fun startLoop() {
if (mLoopHandler == null) {
mLoopHandler = Handler(Looper.getMainLooper()) { message ->
return@Handler when (message.what) {
LOOP_NEXT -> {
loopNext()
true
}
else -> false
}
}
}
if (mLoopHandler?.hasMessages(LOOP_NEXT) != true) {
Log.e(TAG, "startLoop")
mLoopHandler?.sendEmptyMessageDelayed(LOOP_NEXT, mLoopDuration)
}
// 開始輪播時開始曝光(可見時會觸發輪播)
mExposureHelper?.startExposure(currentItem)
}
fun stopLoop() {
// 停止輪播時結束曝光(不可見時會停止輪播)
mExposureHelper?.endExposure()
mLoopHandler?.removeMessages(LOOP_NEXT)
}
fun bindExposureHelper(exposureHelper: ExposureHelper?) {
mExposureHelper = exposureHelper
mExposureHelper?.let {
addOnPageChangeListener(it)
}
mExposureHelper?.startExposure(currentItem)
}