Android實(shí)現(xiàn)擴(kuò)大View點(diǎn)擊區(qū)域的三種方式
在 Android 應(yīng)用開(kāi)發(fā)中,有時(shí)候需要擴(kuò)大 View 的點(diǎn)擊區(qū)域以提高用戶(hù)交互的便利性,尤其是當(dāng)視圖元素較小或用戶(hù)界面密集時(shí)。擴(kuò)大點(diǎn)擊區(qū)域可以讓用戶(hù)更容易點(diǎn)擊目標(biāo),改善用戶(hù)體驗(yàn)。以下提供幾種擴(kuò)大點(diǎn)擊區(qū)域的思路。
方式一:增加padding
通過(guò)設(shè)置padding來(lái)增大點(diǎn)擊區(qū)域,如:
<TextView
android:id="@+id/tv_view_delegate2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/gray_holo_light"
android:padding="20dp"
android:text="TouchDelegate2" />
上面的代碼通過(guò)在XML中設(shè)置padding來(lái)擴(kuò)大點(diǎn)擊區(qū)域,當(dāng)然也可以通過(guò)代碼設(shè)置setPadding來(lái)實(shí)現(xiàn)。雖然設(shè)置padding可以起到效果,但是如果使用不當(dāng)可能會(huì)影響視圖的布局和外觀(guān),比如對(duì)ImageView設(shè)置padding的話(huà)可能會(huì)擠壓其形狀,所以使用Padding擴(kuò)大點(diǎn)擊區(qū)域時(shí)需要確保不影響視圖的布局和外觀(guān)。
方式二:TouchDelegate
TouchDelegate 類(lèi)是 Android 中的一個(gè)輔助類(lèi),用于擴(kuò)展 View 的觸摸區(qū)域,使其大于實(shí)際的 View 邊界。這對(duì)于增加某些 UI 元素的觸控便捷性非常有用,比如小按鈕。
TouchDelegate 使用示例:
/**
* 擴(kuò)展方法,擴(kuò)大點(diǎn)擊區(qū)域
* NOTE: 需要保證目標(biāo)targetView有父View,否則無(wú)法擴(kuò)大點(diǎn)擊區(qū)域
*
* @param expandSize 擴(kuò)大的大小,單位px
*/
fun View.expandTouchView(expandSize: Int = 10.dp2px()) {
val parentView = (parent as? View)
parentView?.post {
val rect = Rect()
getHitRect(rect) //getHitRect(rect)將視圖在父容器中所占據(jù)的區(qū)域存儲(chǔ)到rect中。
log("rect = $rect")
rect.left -= expandSize
rect.top -= expandSize
rect.right += expandSize
rect.bottom += expandSize
log("expandRect = $rect")
parentView.touchDelegate = TouchDelegate(rect, this)
}
}
在A(yíng)ctivity中使用:
private val tvExpandTouch: TextView by id(R.id.tv_view_delegate)
tvExpandTouch.run {
expandTouchView(50.dp2px()) //擴(kuò)大點(diǎn)擊區(qū)域
setOnClickListener { showToast("通過(guò)TouchDelegate擴(kuò)大點(diǎn)擊區(qū)域") }
}
上面就實(shí)現(xiàn)了View擴(kuò)大點(diǎn)擊區(qū)域,繼續(xù)來(lái)看下TouchDelegate 的源碼:
public class TouchDelegate {
private View mDelegateView; //需要接收觸摸事件的 View,即代理 View。
private Rect mBounds;//本地坐標(biāo)中的代理 View 的邊界,用于初始命中測(cè)試。
private Rect mSlopBounds;//增加一定范圍的 mBounds,用于追蹤觸摸事件是否應(yīng)被視為在代理 View 內(nèi)。
@UnsupportedAppUsage
private boolean mDelegateTargeted;
private TouchDelegateInfo mTouchDelegateInfo;
public TouchDelegate(Rect bounds, View delegateView) {
mBounds = bounds;
mSlop = ViewConfiguration.get(delegateView.getContext()).getScaledTouchSlop();
mSlopBounds = new Rect(bounds);
mSlopBounds.inset(-mSlop, -mSlop);
mDelegateView = delegateView;
}
//接收并處理觸摸事件。若事件在 mBounds 內(nèi),則會(huì)將其轉(zhuǎn)發(fā)到 mDelegateView。
public boolean onTouchEvent(@NonNull MotionEvent event) {
int x = (int)event.getX();
int y = (int)event.getY();
boolean sendToDelegate = false;
boolean hit = true;
boolean handled = false;
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mDelegateTargeted = mBounds.contains(x, y);
sendToDelegate = mDelegateTargeted;
break;
case MotionEvent.ACTION_POINTER_DOWN:
case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_MOVE:
sendToDelegate = mDelegateTargeted;
if (sendToDelegate) {
Rect slopBounds = mSlopBounds;
if (!slopBounds.contains(x, y)) {
hit = false;
}
}
break;
case MotionEvent.ACTION_CANCEL:
sendToDelegate = mDelegateTargeted;
mDelegateTargeted = false;
break;
}
if (sendToDelegate) {
if (hit) {
// Offset event coordinates to be inside the target view
event.setLocation(mDelegateView.getWidth() / 2, mDelegateView.getHeight() / 2);
} else {
// Offset event coordinates to be outside the target view (in case it does
// something like tracking pressed state)
int slop = mSlop;
event.setLocation(-(slop * 2), -(slop * 2));
}
//NOTE:重點(diǎn)看這里,最終是調(diào)用的代理View去處理事件了。
handled = mDelegateView.dispatchTouchEvent(event);
}
return handled;
}
}
View.java 源碼中使用 TouchDelegate:
private TouchDelegate mTouchDelegate = null;
public void setTouchDelegate(TouchDelegate delegate) {
mTouchDelegate = delegate;
}
public TouchDelegate getTouchDelegate() {
return mTouchDelegate;
}
public boolean onTouchEvent(MotionEvent event) {
//......
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
switch (action) {
case MotionEvent.ACTION_DOWN:
//...省略...
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
}
}
可以看到在onTouchEvent中,優(yōu)先去判斷是否有TouchDelegate,如果有的話(huà)會(huì)先去找對(duì)應(yīng)的代理View去處理事件。使用TouchDelegate的注意事項(xiàng):
- 目標(biāo)View必須有父View;
- 給多個(gè)目標(biāo)View擴(kuò)大點(diǎn)擊區(qū)域時(shí),不能是同一個(gè)父View,從View類(lèi)的源碼中可以看到,設(shè)置setTouchDelegate時(shí),會(huì)把之前的覆蓋掉。
方式三:RectF & getLocationOnScreen
RectF 是一個(gè)用于表示浮點(diǎn)坐標(biāo)的矩形區(qū)域的類(lèi),而 getLocationOnScreen 則用于獲取視圖在整個(gè)屏幕中的絕對(duì)坐標(biāo)。結(jié)合兩者,可以檢查觸摸事件是否在子視圖的“擴(kuò)展區(qū)域”內(nèi),然后執(zhí)行相應(yīng)的操作。代碼示例:
class ParentInnerTouchView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {
private val tvChildView: TextView
init {
inflate(context, R.layout.expand_touch_view, this)
tvChildView = findViewById(R.id.tv_expand_view)
tvChildView.setOnClickListener { showToast("擴(kuò)大了點(diǎn)擊事件") }
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent?): Boolean {
event?.let { ev ->
if (ev.action == MotionEvent.ACTION_DOWN) {
val isChildHit = isHitExpandChildView(tvChildView, Pair(ev.rawX, ev.rawY))
if (isChildHit) {
//將事件傳遞給子控件
tvChildView.performClick()
}
}
}
return super.onTouchEvent(event)
}
/**
* 判斷是否點(diǎn)擊到了子 View 的擴(kuò)大區(qū)域
* @param childView 子 View
* @param touchPair 點(diǎn)擊的位置 (x, y)
* @param expandSize 擴(kuò)大區(qū)域的大小
* @return 是否命中
*/
private fun isHitExpandChildView(
childView: View,
touchPair: Pair<Float, Float>,
expandSize: Int = 50.dp2px()
): Boolean {
// 獲取子 View 在屏幕上的位置
val location = IntArray(2)
childView.getLocationOnScreen(location)
val childX = location[0].toFloat()
val childY = location[1].toFloat()
val touchX = touchPair.first
val touchY = touchPair.second
// 擴(kuò)大點(diǎn)擊區(qū)域
val rect = RectF()
rect.set(
childX - expandSize,
childY - expandSize,
childX + childView.width + expandSize,
childY + childView.height + expandSize
)
// 判斷點(diǎn)擊是否在擴(kuò)大的子 View 區(qū)域內(nèi)
return rect.contains(touchX, touchY)
}
}
- getLocationOnScreen: 用于獲取子視圖在屏幕上的絕對(duì)坐標(biāo),返回一個(gè)包含 x 和 y 坐標(biāo)的數(shù)組。利用這些坐標(biāo)計(jì)算出子視圖在屏幕上的位置。
- RectF: 創(chuàng)建一個(gè)矩形區(qū)域,通過(guò)調(diào)用 set 方法擴(kuò)展矩形的上下左右邊界,從而擴(kuò)大點(diǎn)擊區(qū)域。
- onTouchEvent: 監(jiān)聽(tīng)觸摸事件,如果點(diǎn)擊位置在擴(kuò)大的區(qū)域內(nèi),則調(diào)用 performClick 觸發(fā)子視圖的點(diǎn)擊事件。
以上就是Android實(shí)現(xiàn)擴(kuò)大View點(diǎn)擊區(qū)域的三種方式的詳細(xì)內(nèi)容,更多關(guān)于A(yíng)ndroid View點(diǎn)擊區(qū)域的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android使用Gridview單行橫向滾動(dòng)顯示
這篇文章主要為大家詳細(xì)介紹了Android使用Gridview單行橫向滾動(dòng)顯示,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-07-07
Android基于Sqlite實(shí)現(xiàn)注冊(cè)和登錄功能
這篇文章主要為大家詳細(xì)介紹了Android基于Sqlite實(shí)現(xiàn)注冊(cè)和登錄功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-04-04
Android ViewPager實(shí)現(xiàn)Banner循環(huán)播放
這篇文章主要為大家詳細(xì)介紹了Android ViewPager實(shí)現(xiàn)Banner循環(huán)播放,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-09-09
Android手勢(shì)密碼view學(xué)習(xí)筆記(二)
這篇文章主要為大家詳細(xì)介紹了Android手勢(shì)密碼view的第二篇學(xué)習(xí)筆記,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-03-03
安卓APP測(cè)試之使用Burp Suite實(shí)現(xiàn)HTTPS抓包方法
這篇文章主要介紹了安卓APP測(cè)試之使用Burp Suite實(shí)現(xiàn)HTTPS抓包方法,本文詳解講解了測(cè)試環(huán)境和各個(gè)軟件的配置方法,需要的朋友可以參考下2015-04-04
Android中的SQL查詢(xún)語(yǔ)句LIKE綁定參數(shù)問(wèn)題解決辦法(sqlite數(shù)據(jù)庫(kù))
這篇文章主要介紹了Android中的SQL查詢(xún)語(yǔ)句LIKE綁定參數(shù)問(wèn)題解決辦法,本文使用的是sqlite數(shù)據(jù)庫(kù),需要的朋友可以參考下2014-06-06
Android使用AutoCompleteTextView實(shí)現(xiàn)自動(dòng)填充功能的案例
今天小編就為大家分享一篇關(guān)于A(yíng)ndroid使用AutoCompleteTextView實(shí)現(xiàn)自動(dòng)填充功能的案例,小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2019-03-03
Android開(kāi)發(fā)中避免應(yīng)用無(wú)響應(yīng)的方法(Application Not Responding、ANR)
這篇文章主要介紹了Android開(kāi)發(fā)中避免應(yīng)用無(wú)響應(yīng)的方法,即避免彈出Application Not Responding(ANR)對(duì)話(huà)框,需要的朋友可以參考下2014-06-06

