• Android修行手册-溢出父布局的按钮实现点击


    👉关于作者

    专注于Android/Unity和各种游戏开发技巧,以及各种资源分享(网站、工具、素材、源码、游戏等)
    有什么需要欢迎底部卡片私我,交流让学习不再孤单

    在这里插入图片描述

    👉实践过程

    有两种方案

    😜方案一

    方案一是在整个Activity窗口捕捉点击事件。

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //首先定义一个数组用来接收按钮的坐标xy值
        int[] xy = new int[2];
        //获取按钮的top/left xy值
        //button变量我在onCreat()函数中已经获取了控件,具体按实际情况写
        button.getLocationOnScreen(xy);
    	//再定义一个数组用来计算控件的bottom/right xy值
        int[] xy_end = new int[2];
        xy_end[0] = buttom.getWidth() + xy[0];
        xy_end[1] = buttom.getHeight() + xy[1];
    	//现在我们已经得到了按钮的左上坐标和右下坐标
    	//两个点可以确定一个矩形嘛  event里包含了点击的信息;
    	//我们判断点击的坐标是否在按钮坐标内,实际就是判断点击的xy值是否在上述矩形中;
        if (event.getX() >= xy[0] && event.getX() <= xy_end[0]
            && event.getY() >= xy[1] && event.getY() <= xy_end[1]) {
            //如果是,那么就执行里边的代码,在这里我们可以callOnClick()按钮
    		//实际体验了一番,发现轻点一下和长按均可以激活按钮;
    		//但是,我的按钮拥有animate()事件,所以连续点击会在动画未完成时再次点击按钮,
    		//所以我做了个判断,让动画未完成时不再执行点击,机制如我
    		//实际中,读者完全不用这两行代码
    		//让我看看有哪些读者看都不看直接复制代码--手动滑稽
    		//虽说站在巨人肩膀上,但是也要搞懂其原理才不会摔下来。
            if (isMoreShow == false && xy[0] >= button.getHeight())
                return false;
    		//我们callOnClick了按钮,也就是模拟点击了按钮;
            button.callOnClick();
            return false;
        }
        return super.onTouchEvent(event);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    不足之处也很明显,如果页面点击事件要素过多,写入的判断就很多了,毕竟你是整个 Activity 自己处理事件了。

    😜推荐方案二:委托

    小应用场景:有时候一个按钮效果很小,就很难触发点击事件,我们通常会增大下这个点击区间范围。
    大应用场景:我实现了多个脑图的功能,里面因为方便画线穿插过某个UI,就用到了此类知识。
    在这里插入图片描述
    其他情况多种多样,相信看这篇文章的你也是因为有这个需求才查找的。

    小应用场景的实现很简单:

    1. 直接增大 View 的宽高,然后给View设置内边距 padding ;或者直接嵌套一层给这个父设置点击,但这会增加布局嵌套进而消耗性能。
    2. 利用委托功能直接增大点击的区间范围。
        /**
         * 扩展点击区域的范围
         * @param view       需要扩展的元素,此元素必需要有父级元素
         * @param expendSize 需要扩展的尺寸,当然也可以分别设置增大范围
         */
        public static void expendTouchArea(final View view, final int expendSize) {
            if (view != null) {
                final View parentView = (View) view.getParent();
    
                parentView.post(new Runnable() {
                    @Override
                    public void run() {
                        Rect rect = new Rect();
                        view.getHitRect(rect); 
                        rect.left -= expendSize;
                        rect.top -= expendSize;
                        rect.right += expendSize;
                        rect.bottom += expendSize;
                        parentView.setTouchDelegate(new TouchDelegate(rect, view));
                    }
                });
            }
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    事实是,委托就是系统给我们提供的扩大控件点击区域判断范围的代理方式,我们看下View类的源码。

    class View{
        /**
         * The delegate to handle touch events that are physically in this view
         * but should be handled by another view.
         */
        private TouchDelegate mTouchDelegate = null;
    	public boolean onTouchEvent(MotionEvent event) {
    		//...
    		if (mTouchDelegate != null) {
                if (mTouchDelegate.onTouchEvent(event)) {
                    return true;
                }
            }
    	}
        /**
         * Sets the TouchDelegate for this View.
         */
        public void setTouchDelegate(TouchDelegate delegate) {
            mTouchDelegate = delegate;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    从源码中可以看到如果设置了TouchDelegate,touchEvent会优先交给TouchDelegate来处理。

    package android.view;
    import android.graphics.Rect;
    /**
     * Helper class to handle situations where you want a view to have a larger touch area than its
     * actual view bounds. The view whose touch area is changed is called the delegate view. This
     * class should be used by an ancestor of the delegate. To use a TouchDelegate, first create an
     * instance that specifies the bounds that should be mapped to the delegate and the delegate
     * view itself.
     * The ancestor should then forward all of its touch events received in its
     * {@link android.view.View#onTouchEvent(MotionEvent)} to {@link #onTouchEvent(MotionEvent)}.
     */
    public class TouchDelegate {
        private View mDelegateView;
        private Rect mBounds;
        private boolean mDelegateTargeted;
        public TouchDelegate(Rect bounds, View delegateView) {
            mBounds = bounds;
            mDelegateView = delegateView;
        }
        public boolean onTouchEvent(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;
                    //...
            }
            if (sendToDelegate) {
                final View delegateView = mDelegateView;
                if (hit) {
                    // Offset event coordinates to be inside the target view
                    event.setLocation(delegateView.getWidth() / 2, delegateView.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));
                }
                handled = delegateView.dispatchTouchEvent(event);
            }
            return handled;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    从源码中 可以看到,创建TouchDelegate 需要传入一个Rect(left,top,right,bottom) 和delegateView, onTouchEvent触发时,会通过这个Rect来判断点击事件是否落在区域内,如果是 则转发给代理view来处理该事件。

    😜复杂场景实现-重点

    子 View 超出父布局显示,然后触发点击事件,同样利用的委托功能,但是因为要处理 Touch 需要自定义一下。

    
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:clipChildren="false"
        android:clipToPadding="false"
        android:id="@+id/rootLay"
        android:orientation="vertical"
        tools:context=".MainActivity"
        tools:ignore="HardcodedText,InOrMmUsage">
        
        <cn.akitaka.test.TestOverClick
            android:id="@+id/testLay"
            android:layout_width="200mm"
            android:layout_height="200mm"
            android:background="@color/crane_swl_color_3"
            android:clipChildren="false"
            android:clickable="true"
            android:clipToPadding="false">
    
            <Button
                android:id="@+id/idBtnTest"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="300mm"
                android:text="Excel"
                android:textSize="26mm" />
        cn.akitaka.test.TestOverClick>
    LinearLayout>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    /**
     * @author akitaka 2023/11/22 960576866@qq.com
     * @describe TestOverClick
     */
    public class TestOverClick extends RelativeLayout {
    
        public TestOverClick(Context context) {
            super(context);
        }
    
        public TestOverClick(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        public TestOverClick(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    
        public TestOverClick(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
            super(context, attrs, defStyleAttr, defStyleRes);
        }
    
        private void initClickRect() {
            View rootParent = ((View) getParent());// 获取父视图
            rootParent.post(() -> {// 将当前代码放在消息队列中异步执行
                Rect rect = new Rect();// 创建一个矩形对象
                // 获取当前视图的点击区域  如果太早执行本函数,会获取rect失败,因为此时UI界面尚未开始绘制,无法获得正确的坐标
                getHitRect(rect);
                rect.left -= 0;
                rect.top -= 0;
                //布局中控件是距离左300像素 控件本身是200 他俩的中间间距为100 加上按钮的本身宽度
                rect.right += AutoSizeUtils.mm2px(getContext(), 100) + btn.getWidth();
                rect.bottom += 0;
    
                rootParent.setTouchDelegate(new TouchDelegate(rect, this));  // 设置根视图的触摸委托为当前视图
            });
        }
    
        private Button btn;//外部的按钮对象设置
    
        public void setBtn(Button btn) {
            this.btn = btn;
            initClickRect();
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            Log.e("TAG", "事件类型: " + event.getAction());
            int x = (int) event.getX();
            int y = (int) event.getY();
    //        if () {  TODO  重点注意
    //            //这个if判断是你点击的x、y坐标是否在按钮的范围内,不在的话直接进行return不处理即可
    //            //具体的区间判断范围,就需要自己的项目具体调整了。
    //            return true;
    //        }
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    Log.e("TAG", "按下事件: " + btn);
                    btn.setBackgroundResource(R.color.purple_200);
                    break;
                case MotionEvent.ACTION_UP:
                    btn.performClick();
                    Log.e("TAG", "抬起事件: " + btn);
                    HandlerUtils.INSTANCE.postRunnable(() -> {
                        btn.setBackgroundResource(R.color.purple_700);
                    }, 30);//30毫秒延迟
                    break;
                default:
                    break;
            }
            return super.onTouchEvent(event);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    public class MainActivity extends FragmentActivity {
    		@Override
    		protected void onCreate(Bundle savedInstanceState) {
    				super.onCreate(savedInstanceState);
    				setContentView(R.layout.activity_main);
    				TestOverClick testLay = findViewById(R.id.testLay);
    				Button idBtnTest = findViewById(R.id.idBtnTest);
    				idBtnTest.setOnClickListener(v -> Log.e("TAG", "点击了内容: "));
    				testLay.setBtn(idBtnTest);
    		}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    上面的注释简直是保姆级的了。

    1. 自定义 TestOverClick 嵌套了个子 Button 控件,设置android:clickable="true"可点击,设置属性android:clipChildren="false"android:clipToPadding="false"实现超出区域可见。
    2. 自定义 TestOverClick 有个方法 initClickRect 是用来设置点击响应区域的,咱们向右侧进行了扩大,红色为默认响应区域,经过计算:布局中控件是距离左300像素 控件本身是200 他俩的中间间距为100 加上按钮的本身宽度。右侧增加了绿框范围的响应区域。
      在这里插入图片描述
    3. 接着我们在 onTouchEvent 函数中做两个处理:处理一是判断下点击的区间,通过计算允许在按钮范围内处理,否则的话直接消耗事件,这样就假装模拟出了只响应按钮了。处理二是在事件中抬起的时候回调下按钮的模拟点击事件,就会进入业务逻辑。注意我们真正点击的其实是父控件,只不过模拟点击了按钮。
    4. 默认模拟点击是没有点击效果的,所以我们在 onTouchEvent 中 down 和 up 的时候自己更改下按钮背景状态即可完美实现点击UI变化。
    5. activity 中直接使用即可,我们内部需要用到按钮,记得要传递进去按钮对象。

    😜题外

    一个Parent只能设置一个View的TouchDelegate,设置多个时只有最后设置的生效。
    如果想恢复 View 的触摸范围:

    /**
     * 还原View的触摸和点击响应范围,最小不小于View自身范围
     */
    public static void restoreViewTouchDelegate(final View view) {
    	((View) view.getParent()).post(new Runnable() {
    		@Override
    		public void run() {
    			Rect bounds = new Rect();
    			bounds.setEmpty();
    			TouchDelegate touchDelegate = new TouchDelegate(bounds, view);
    			if (View.class.isInstance(view.getParent())) {
    				((View) view.getParent()).setTouchDelegate(touchDelegate);
    			}
    		}
    	});
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    还没懂?下方卡片联系我,手把手教你。

    👉其他

    📢作者:小空和小芝中的小空
    📢转载说明-务必注明来源:https://zhima.blog.csdn.net/
    📢这位道友请留步☁️,我观你气度不凡,谈吐间隐隐有王者霸气💚,日后定有一番大作为📝!!!旁边有点赞👍收藏🌟今日传你,点了吧,未来你成功☀️,我分文不取,若不成功⚡️,也好回来找我。

    温馨提示点击下方卡片获取更多意想不到的资源。
    空名先生

  • 相关阅读:
    程序员必须了解的 10个免费 Devops 工具
    内容、文档和流程数字化如何支持精益原则
    个人编程笔记 - 子类和父类有同名的成员?
    Vue3视图渲染技术
    IT创业项目-赚钱项目-网赚项目:月入2W+的视频号创业项目
    初试scikit-learn库
    人机融合态势感知的压缩
    网工内推 | 网络安全工程师,上市公司,13薪,食宿有补贴
    云平台下ESB产品开发步骤说明
    [附源码]计算机毕业设计旅游度假村管理系统Springboot程序
  • 原文地址:https://blog.csdn.net/qq_27489007/article/details/134555782