[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)
}
}
}
~~~
可以發現滑動沒有問題:

但是如果我們將其應用在前面的自定義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也不能滾動

到了一個正常的滑動沖突的現象。此時的現象為:
> 可以滾動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特效