[TOC]
# 1. 自定义ViewPager
比如在自定义ViewPager中,中的某个子页面使用了一个scrollView。对于自定义ViewPager这里再次复习一下:
* 定义对应的类,继承自ViewGroup,并复写onLayout方法,使得所有的页面在逻辑上是连着的。
* 通过addView来添加子视图,这里直接使用ImageView,然后为其指定Background;
* 通过上述步骤后,就可以显示出来一个页面;然后我们需要为这个自定义ViewPager指定手指触摸的滑动事件;
* 使用手势识别GestureDetector的onTouchEvent事件来进行事件的拦截,在对应的onScroll方法中进行滑动,这里使用scrollBy进行,当然需要进行边界的判断;
* 然后我们需要为他添加一个回弹的动画,这里可以采用自定义,也可以使用系统中提供的android.widget.Scroller来实现,使用scroller.startScroll来开始滑动,使用scroller.computeScrollerOffset判断是否滑动结束。同时,这个类也提供了插值器,所以在最后会有个很好看的平滑效果。
对应代码:
~~~
/**
* 使用系统自带Scroller
*/
class MyViewPager(context: Context?, attrs: AttributeSet?) : ViewGroup(context, attrs) {
// 手势识别器
var gestureDetector: GestureDetector? = null
var mOnPagerChangerListener: OnPagerChangerListener? = null
init {
gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
override fun onScroll(
e1: MotionEvent?,
e2: MotionEvent?,
distanceX: Float,
distanceY: Float
): Boolean {
// X轴移动
if(scrollX + distanceX >= 0 && scrollX + distanceX <= width * (childCount - 1)){
scrollBy(distanceX.toInt(), 0)
}
return true
}
})
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
for (i in 0..childCount) {
val childView = getChildAt(i)
childView?.layout(i * width, 0, (i + 1) * width, height)
}
}
var startX = 0f
var index = 0
// 使用系统android.widget.Scroller
var scroller: Scroller = Scroller(context)
override fun onTouchEvent(event: MotionEvent?): Boolean {
//3.把事件传递给手势识别器
gestureDetector?.onTouchEvent(event)
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
// 起始位置
startX = event.x
}
MotionEvent.ACTION_MOVE -> {
}
MotionEvent.ACTION_UP -> {
var tempIndex = index
if((startX - event.x) > width / 2){
tempIndex++
}else if((event.x - startX) > width / 2 ){
tempIndex--
}
// 非法处理
if(tempIndex < 0) tempIndex = 0
if(tempIndex > childCount - 1) tempIndex = childCount - 1
index = tempIndex
// 监听接口调用
mOnPagerChangerListener?.onPageChange(index)
// 回弹
scrollToPage(index)
}
}
return true
}
/**
* 按照页面下标进行滚动
*/
fun scrollToPage(tempIndex: Int){
scroller.startScroll(scrollX, scrollY, tempIndex*width - scrollX, 0)
invalidate()
}
override fun computeScroll() {
if (scroller.computeScrollOffset()) {
scrollTo(scroller.currX, 0)
invalidate()
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
// 获取子元素个数
if (childCount == 0) return
// 如果是wrap_content,也就是AT_MOST模式
// 如果是match_parents,也就是精确模式
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
// 自适应,也就是取决于最大的子元素的宽和高,这里直接设置为屏幕的宽和高
var height = MeasureSpec.getSize(heightMeasureSpec)
var width = MeasureSpec.getSize(widthMeasureSpec)
if (widthMode == MeasureSpec.AT_MOST ){
width = resources.displayMetrics.widthPixels
}
if(heightMode == MeasureSpec.AT_MOST) {
height = resources.displayMetrics.heightPixels
}
setMeasuredDimension(width, height)
// 需要测量孩子
for (i in 0 until childCount){
getChildAt(i).measure(width, height)
}
}
// 定义一个页面下标改变的监听接口
interface OnPagerChangerListener{
fun onPageChange(position: Int)
}
}
~~~
这里设置监听器是为了关联指示器,指示器使用RadioButton来实现。比如:
~~~
class MainActivity : AppCompatActivity() {
val custom_viewpager by lazy { findViewById<MyViewPager>(R.id.custom_viewpager) }
val radioGroup by lazy { findViewById<RadioGroup>(R.id.radioGroup) }
var imageRes = listOf<Int>(R.drawable.a, R.drawable.b, R.drawable.c)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
for (i in 0 until imageRes.size){
val imageView = ImageView(this)
imageView.setBackgroundResource(imageRes.get(i))
custom_viewpager.addView(imageView)
}
val view = layoutInflater.inflate(R.layout.activity_other, null)
custom_viewpager.addView(view, 2)
for(i in 0 until custom_viewpager.childCount){
val btn = RadioButton(this)
if(i == 0) btn.isChecked = true
btn.id = i
radioGroup.addView(btn)
}
radioGroup.setOnCheckedChangeListener(object : RadioGroup.OnCheckedChangeListener {
override fun onCheckedChanged(group: RadioGroup?, checkedId: Int) {
// 切换页面
custom_viewpager.scrollToPage(checkedId)
}
})
// 页面切换关联radioButton
custom_viewpager.mOnPagerChangerListener = object : MyViewPager.OnPagerChangerListener{
override fun onPageChange(position: Int) {
radioGroup.check(position)
}
}
}
}
~~~
# 2. 滑动冲突
## 2.1 环境构建
在上面的代码中我们使用了:
```
val view = layoutInflater.inflate(R.layout.activity_other, null)
custom_viewpager.addView(view, 2)
```
来添加一个页面,在这个页面中使用了ScrollView:
~~~
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/linearlayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
</LinearLayout>
</ScrollView>
</LinearLayout>
~~~
如果我们在另外一个Activity中测试:
~~~
class TestActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_other)
val linearlayout by lazy { findViewById<LinearLayout>(R.id.linearlayout) }
for( i in 0..50){
val textView = TextView(this)
textView.text = "文本:${i}"
linearlayout.addView(textView)
}
}
}
~~~
可以发现滑动没有问题:
![](https://img.kancloud.cn/b7/d3/b7d330e413ceb3f8292ad60899afee8a_373x597.png)
但是如果我们将其应用在前面的自定义ViewPager中:
~~~
val view = layoutInflater.inflate(R.layout.activity_other, null)
val linearlayout by lazy { view.findViewById<LinearLayout>(R.id.linearlayout) }
for( i in 0..50){
val textView = TextView(this)
textView.width = resources.displayMetrics.widthPixels
textView.gravity = Gravity.CENTER
textView.textSize = 22F
textView.text = "文本:${i}"
linearlayout.addView(textView)
}
custom_viewpager.addView(view, 0)
~~~
那么,按照逻辑这里就会出现滑动冲突。这里我的现象是滚动不了ViewPager,同时ScrollerView也不能滚动。
## 2.2 环境构建中问题排查
其实这个现象是不应该的,因为按照道理来说滑动冲突,也能有响应发生,故而这里排查一下:
> ScrollView内的最外层控件或布局,如果尺寸大小不明确,会导致无法滑动。
但由于我的布局为:
~~~
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:id="@+id/scrollview"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/linearlayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
</LinearLayout>
</ScrollView>
</LinearLayout>
~~~
然后custom_viewpager.addView(view, 0),也就是说可能的原因就在于xml布局中外层LinearLayout的大小没有测量出来。也就是测量孩子的宽高有问题。再次看下前面的测量孩子的代码:
```
// 需要测量孩子
for (i in 0 until childCount){
getChildAt(i).measure(width, height)
}
```
很明显,这里只是手动的测量了直接孩子的大小。所以这里就由系统自己去测量:
~~~
// 测量孩子
measureChildren(widthMeasureSpec, heightMeasureSpec)
~~~
即:
~~~
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
// 获取子元素个数
if (childCount == 0) return
// 测量孩子
measureChildren(widthMeasureSpec, heightMeasureSpec)
// 如果是wrap_content,也就是AT_MOST模式
// 如果是match_parents,也就是精确模式
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
// 自适应,也就是取决于最大的子元素的宽和高,这里直接设置为屏幕的宽和高
var height = MeasureSpec.getSize(heightMeasureSpec)
var width = MeasureSpec.getSize(widthMeasureSpec)
if (widthMode == MeasureSpec.AT_MOST ){
width = resources.displayMetrics.widthPixels
}
if(heightMode == MeasureSpec.AT_MOST) {
height = resources.displayMetrics.heightPixels
}
setMeasuredDimension(width, height)
}
~~~
然后就解决了前面的现象:
> 滚动不了ViewPager,同时ScrollerView也不能滚动
![](https://img.kancloud.cn/6b/f1/6bf1899406ff3b5d63f3200fa4aa9e1a_242x403.png)
到了一个正常的滑动冲突的现象。此时的现象为:
> 可以滚动ScrollView中的文本;
> 自定义的ViewPager无法滑动切换;
这里的**冲突也就是常见的非同向冲突。**
## 2.3 滑动冲突解决
这里的滑动冲突解决起来比较简单,因为是两个非同向的冲突,我们只需要判断一下触摸事件的方向,然后决定由谁来处理即可。具体来说涉及到几个方法:
* onTouchEvent,判断起始位置,判断用户滑动事件方向;
* onInterceptTouchEvent,返回true表示拦截,否则为不拦截;
* parent.requestDisallowInterceptTouchEvent(boolean),传入的参数为true表示要求父控件不处理,由自己处理;
首先看下这个方法:
~~~
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
}
~~~
根据事件传递规则,如果一开始就直接返回true或者false,那么就会导致两种情况。要么事件直接被当前的VeiwGroup拦截,要么就是使得当前的ViewGroup响应不了事件。所以这里提供的思路就可以有两种:
* 根据条件判断,是否要拦截事件;
* 传递到子View,由子View决定自己要消费的事件;
当然,这里首先考虑使用第一种,代码如下:
~~~
var interceptorX = 0f
var interceptorY = 0f
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
when (ev?.action) {
MotionEvent.ACTION_DOWN -> { // 放行Down事件
startX = ev.x // 因为后续接受不了Down事件,所以在这里赋值
// 起始位置
interceptorX = ev.x
interceptorY = ev.y
}
MotionEvent.ACTION_MOVE -> { // MOVE事件选择放行
return abs(ev.x - interceptorX) >= abs(ev.y - interceptorY)
}
}
return false
}
~~~
当然还是来尝试一下使用第二种方式:
由于ViewGroup默认onInterceptTouchEvent返回false,所以是交给子View处理,也就是这里的ScrollView来处理。我们只需要复写一下其方法,然后对需要的事件进行消费,当然还是需要在ViewGroup中对其进行放行Down事件才行:
~~~
var interceptorX = 0f
var interceptorY = 0f
// 添加到ViewGroup之后执行
scrollview.setOnTouchListener(object : View.OnTouchListener {
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
Log.e("TAG", "ACTION_DOWN: ${scrollview.parent == null}")
// 告诉父控件,自己要处理,不允许拦截
scrollview.parent?.apply {
(scrollview.parent as ViewGroup).requestDisallowInterceptTouchEvent(true)
}
// 起始位置
interceptorX = event.x
interceptorY = event.y
return true
}
MotionEvent.ACTION_MOVE -> {
Log.e("TAG", "ACTION_MOVE: ")
scrollview.parent?.apply {
if (abs(event.x - interceptorX) >= abs(event.y - interceptorY)) {
// 让父控件拦截
(scrollview.parent as ViewGroup).requestDisallowInterceptTouchEvent(
false
)
return false // ScrollView不消费
} else {
(scrollview.parent as ViewGroup).requestDisallowInterceptTouchEvent(
true
)
// 交给scrollview处理
scrollview.onTouchEvent(event)
return true
}
}
}
}
return true
}
})
~~~
从代码量来说,在这个场景中第一种方式解决冲突的更好,因为代码量更少。
- 介绍
- UI
- MaterialButton
- MaterialButtonToggleGroup
- 字体相关设置
- Material Design
- Toolbar
- 下拉刷新
- 可折叠式标题栏
- 悬浮按钮
- 滑动菜单DrawerLayout
- NavigationView
- 可交互提示
- CoordinatorLayout
- 卡片式布局
- 搜索框SearchView
- 自定义View
- 简单封装单选
- RecyclerView
- xml设置点击样式
- adb
- 连接真机
- 小技巧
- 通过字符串ID获取资源
- 自定义View组件
- 使用系统控件重新组合
- 旋转菜单
- 轮播图
- 下拉输入框
- 自定义VIew
- 图片组合的开关按钮
- 自定义ViewPager
- 联系人快速索引案例
- 使用ListView定义侧滑菜单
- 下拉粘黏效果
- 滑动冲突
- 滑动冲突之非同向冲突
- onMeasure
- 绘制字体
- 设置画笔Paint
- 贝赛尔曲线
- Invalidate和PostInvalidate
- super.onTouchEvent(event)?
- setShadowLayer与阴影效果
- Shader
- ImageView的scaleType属性
- 渐变
- LinearGradient
- 图像混合模式
- PorterDuffXfermode
- 橡皮擦效果
- Matrix
- 离屏绘制
- Canvas和图层
- Canvas简介
- Canvas中常用操作总结
- Shape
- 圆角属性
- Android常见动画
- Android动画简介
- View动画
- 自定义View动画
- View动画的特殊使用场景
- LayoutAnimation
- Activity的切换转场效果
- 属性动画
- 帧动画
- 属性动画监听
- 插值器和估值器
- 工具
- dp和px的转换
- 获取屏幕宽高
- JNI
- javah命令
- C和Java相互调用
- WebView
- Android Studio快捷键
- Bitmap和Drawable图像
- Bitmap简要介绍
- 图片缩放和裁剪效果
- 创建指定颜色的Bitmap图像
- Gradle本地仓库
- Gradle小技巧
- RxJava+Okhttp+Retrofit构建网络模块
- 服务器相关配置
- node环境配置
- 3D特效