?
緣由
在應用中使用ViewPager,并且設置預加載頁面。結果出現了一些異常的現象。
我們有4個頁面,分別是4個Fragment,暫且稱為FragmentA、FragmentB、FragmentC、FragmentD,ViewPager在MainActivity中,切換時,FragmentB會將FragmentA覆蓋,這個時候,在FragmentB中點擊某一處,如果該處的FragmentA也有控件,會出現異常的情況,事件會響應到FragmentA中的控件中去,也就是事件穿透了。
原因分析:
?這個問題在使用 ViewPager
和 PageTransformer
時確實比較常見。根本原因在于 ViewPager
的工作機制以及 PageTransformer
如何影響頁面視圖。
- ViewPager 預加載機制:
ViewPager
為了實現平滑的滑動效果,默認會預加載當前頁面左右兩側的頁面(數量由setOffscreenPageLimit(int limit)
控制,默認為1)。這意味著即使某個頁面在視覺上已經部分或完全移出屏幕,它的視圖 (View) 仍然存在于ViewPager
的視圖層級 (View Hierarchy) 中,并且可能仍然是活動的。 - PageTransformer 的作用:
PageTransformer
主要通過修改頁面的繪制屬性(如translationX
,translationY
,scaleX
,scaleY
,alpha
,rotation
等)來實現切換動畫。它改變的是頁面的 視覺 位置和外觀,但通常 不改變 頁面視圖在視圖層級中的存在狀態或其接收觸摸事件的能力。 - 事件分發機制: 當你在屏幕上點擊時,Android 的觸摸事件會從父視圖(這里是
ViewPager
)分發到子視圖。即使某個頁面(比如前一個頁面)通過PageTransformer
被平移到了當前頁面的后面或者屏幕外,如果它的視圖邊界(在進行變換之前的原始邊界,或者變換后的邊界仍然與點擊位置有重疊,并且它沒有被設置為不可交互)仍然能夠響應觸摸事件,那么點擊事件就可能被它攔截并處理。尤其是當PageTransformer
只是平移 (translation) 而沒有處理視圖的 Z 軸順序 (elevation) 或可點擊狀態時,這個問題更容易發生。默認情況下,后添加的頁面(索引更大的頁面)會繪制在先添加的頁面之上,但事件分發可能不會嚴格遵守這個視覺順序,特別是對于經過變換的視圖。
總結來說: 前一個頁面雖然在視覺上被移開了(或者被當前頁面遮擋了),但它的 View 實例仍然存在于內存和視圖樹中,并且默認情況下是可交互的。當你點擊的位置恰好也落在了這個“隱藏”頁面的可交互控件的原始或變換后的區域內時,事件可能就會被它錯誤地接收。
解決方案:
最有效的解決辦法是在你的 PageTransformer
的 transformPage(View page, float position)
方法中,根據頁面的 position
來動態地管理頁面的可交互狀態。
position
的含義:0
: 當前完全可見的頁面。(-1, 0)
: 左側相鄰的頁面,正在移入或移出屏幕。(0, 1)
: 右側相鄰的頁面,正在移入或移出屏幕。<= -1
: 完全移出屏幕左側的頁面。>= 1
: 完全移出屏幕右側的頁面。
你需要確保只有當前頁面(或者你希望可以交互的過渡頁面)是可點擊的,而那些視覺上處于非活動狀態(尤其是被當前頁面遮擋的頁面)應該被禁用交互。
具體實現方法:
在你的 PageTransformer
的 transformPage
方法中添加邏輯:
方法一:直接設置 clickable
或 enabled
(簡單但不一定完全解決子視圖問題)
// Java
import androidx.viewpager.widget.ViewPager;
import android.view.View;public class MyPageTransformer implements ViewPager.PageTransformer {@Overridepublic void transformPage(View page, float position) {// ... 你原有的變換邏輯 (translation, scale, alpha etc.)// 關鍵:根據 position 決定頁面是否可以交互// 通常我們只希望當前頁面 (-1 < position < 1 范圍內的頁面,特別是 position 接近 0 的) 可以交互// 對于完全移出屏幕或在后面的頁面 (position <= -1 或 position >= 1,尤其是 position < 0 時) 禁用交互if (position < -1f || position > 1f) {// 完全不可見page.setAlpha(0f); // 視覺上隱藏 (可選)page.setEnabled(false);page.setClickable(false);} else {// 在屏幕內或正在過渡page.setEnabled(true);page.setClickable(true);// 這里可以根據 position 的具體值調整 alpha 或其他視覺效果if (position == 0) {// 當前頁面,確保完全可見和可交互page.setAlpha(1f);} else {// 過渡頁面,可以根據需要設置透明度等// 例如,讓遠離中心的頁面更透明float scaleFactor = Math.max(0.85f, 1 - Math.abs(position));page.setScaleX(scaleFactor);page.setScaleY(scaleFactor);page.setAlpha(1 - Math.abs(position)); // 漸變效果}}// 可能需要遞歸禁用子視圖,如果 setEnabled(false) 對父視圖不夠的話// setChildrenClickable(page, position > -1f && position < 1f);}// 輔助方法,遞歸設置子視圖的可點擊性 (如果需要)private void setChildrenClickable(View view, boolean clickable) {if (view instanceof ViewGroup) {ViewGroup viewGroup = (ViewGroup) view;for (int i = 0; i < viewGroup.getChildCount(); i++) {setChildrenClickable(viewGroup.getChildAt(i), clickable);}}view.setClickable(clickable);}
}
// Kotlin
import androidx.viewpager.widget.ViewPager
import android.view.View
import android.view.ViewGroupclass MyPageTransformer : ViewPager.PageTransformer {override fun transformPage(page: View, position: Float) {// ... 你原有的變換邏輯 (translation, scale, alpha etc.)// 關鍵:根據 position 決定頁面是否可以交互val isVisible = position >= -1f && position <= 1fpage.isEnabled = isVisiblepage.isClickable = isVisible // 設置根視圖的可點擊性if (!isVisible) {page.alpha = 0f // 完全不可見時隱藏 (可選)} else {// 在屏幕內或正在過渡// 可以根據 position 調整 alpha 等視覺效果if (position == 0f) {// 當前頁面page.alpha = 1f} else {// 過渡頁面val scaleFactor = maxOf(0.85f, 1 - kotlin.math.abs(position))page.scaleX = scaleFactorpage.scaleY = scaleFactorpage.alpha = 1 - kotlin.math.abs(position) // 漸變效果}}// 如果 page.isEnabled = false 不足以阻止子視圖響應點擊,// 你可能需要遞歸地禁用子視圖。// setChildrenClickable(page, isVisible)}// 輔助方法,遞歸設置子視圖的可點擊性 (如果需要)private fun setChildrenClickable(view: View, clickable: Boolean) {view.isClickable = clickableif (view is ViewGroup) {for (i in 0 until view.childCount) {setChildrenClickable(view.getChildAt(i), clickable)}}}
}
方法二:使用 Elevation (API 21+)
如果你的應用最低支持 API 21 (Lollipop),你可以利用 elevation
屬性。通常,Z 軸更高的視圖會接收觸摸事件。確保當前頁面的 elevation
最高。
// Java (API 21+)
import androidx.viewpager.widget.ViewPager;
import android.view.View;
import android.os.Build;public class MyPageTransformer implements ViewPager.PageTransformer {private static final float MAX_ELEVATION = 8f; // dp, or just a relative value@Overridepublic void transformPage(View page, float position) {// ... 你原有的變換邏輯 ...if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {// 讓當前頁面及其鄰近頁面的 Elevation 根據 position 變化// 當前頁面 (position=0) Elevation 最高// 越遠的頁面 Elevation 越低float absPos = Math.abs(position);float elevation = (1 - absPos) * MAX_ELEVATION;page.setElevation(elevation);}// 你仍然可能需要結合方法一中的 setClickable/setEnabled// 特別是對于完全移出屏幕的頁面 (absPos >= 1)if (position < -1f || position > 1f) {page.setAlpha(0f);page.setEnabled(false);page.setClickable(false);} else {page.setEnabled(true);page.setClickable(true);// 根據需要設置 alpha 等page.setAlpha(1 - Math.abs(position)); // 示例}}
}
// Kotlin (API 21+)
import androidx.viewpager.widget.ViewPager
import android.view.View
import android.os.Buildclass MyPageTransformer : ViewPager.PageTransformer {companion object {private const val MAX_ELEVATION = 8f // dp, or just a relative value}override fun transformPage(page: View, position: Float) {// ... 你原有的變換邏輯 ...if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {val absPos = kotlin.math.abs(position)// 越靠近中間 (position=0), elevation 越高page.elevation = (1 - absPos) * MAX_ELEVATION}// 結合管理 clickable/enabled 狀態val isVisible = position >= -1f && position <= 1fpage.isEnabled = isVisiblepage.isClickable = isVisibleif (!isVisible) {page.alpha = 0f} else {page.alpha = 1 - kotlin.math.abs(position) // 示例}}
}
總結建議:
- 首選方法一,在
transformPage
中根據position
明確設置page.setEnabled(false)
和/或page.setClickable(false)
對于那些不應響應交互的頁面(特別是position < 0
且視覺上在后面的頁面)。如果頁面根布局設置clickable=false
不足以阻止其子控件響應事件,你可能需要遞歸地禁用子控件的可點擊狀態,或者直接將整個頁面的setEnabled(false)
。 - 如果你的
minSdkVersion
>= 21,可以結合方法二使用elevation
來輔助控制 Z 軸順序,這有助于系統更正確地進行事件分發。 - 測試:仔細測試你的
PageTransformer
在各種滑動狀態下的表現,確保只有期望的頁面可以響應點擊。
通過在 PageTransformer
中主動管理頁面的交互狀態,能夠解決這個點擊穿透的問題。