AndroidBanner - ViewPager 03

真菜啊發表於2023-04-08

AndroidBanner - ViewPager 03

上一篇文章,描述瞭如何實現自動輪播的,以及手指觸控的時候停止輪播,抬起繼續輪播,其實還遺留了一些問題:

  1. 當banner不可見的時候,也需要停止輪播
  2. 給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)  
}

程式碼:huyuqiwolf/Banner (github.com)

相關文章