解决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)
}