Android修行手册-超出父布局进行显示以及超出父布局实现点击

Unity3D特效百例 案例项目实战源码 Android-Unity实战问题汇总
游戏脚本-辅助自动化 Android控件全解手册 再战Android系列
Scratch编程案例 软考全系列 Unity3D学习专栏
蓝桥系列 ChatGPT和AIGC

👉关于作者

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

👉实践过程

😜超出父布局显示

我们实现一个 LinearLayout 布局,宽高是200,里面嵌套一个 Button ,默认是展示出来的。

xml 复制代码
<RelativeLayout
    android:layout_width="200mm"
    android:layout_height="200mm"
    android:background="@color/crane_swl_color_3">

    <Button
        android:id="@+id/idBtnText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Excel"
        android:textSize="26mm" />
</RelativeLayout>

但是我们将 Button 的间距设置超出父布局。默认是不会展示出来的。

xml 复制代码
<RelativeLayout
    android:layout_width="200mm"
    android:layout_height="200mm"
    android:background="@color/crane_swl_color_3">

    <Button
        android:id="@+id/idBtnText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Excel"
        android:textSize="26mm" />
</RelativeLayout>

我们需要借住属性:

android:clipChildren="false"
android:clipToPadding="false"

官方对于第一行的解释:

Defines whether a child is limited to draw inside of its bounds or not.

翻译:定义一个子视图是否局限于它的范围内。

所以我们设置为false,让子视图不局限与自己;

官方对于第二行的解释:

Defines whether the ViewGroup will clip its drawing surface so as to exclude the padding area.

翻译:定义ViewGroup是否将剪辑其绘图表面以排除填充区域。

要特别注意

  1. 如果你某个子 View 嵌套了多层,然后超出了父布局,需要所有的父布局都携带 clipChildren 属性。
  2. 这个子 View 的最近父布局需要是 RelativeLayout ,博主摸了摸秀发,并没有去深究为什么其他 ViewGroup 为什么不行。
    如下:
xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<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:orientation="vertical"
    tools:context=".MainActivity"
    tools:ignore="HardcodedText,InOrMmUsage">

    <RelativeLayout
        android:layout_width="200mm"
        android:layout_height="200mm"
        android:background="@color/crane_swl_color_3"
        android:clipChildren="false"
        android:clipToPadding="false">

        <Button
            android:id="@+id/idBtnText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="300mm"
            android:text="Excel"
            android:textSize="26mm" />
    </RelativeLayout>
</LinearLayout>

😜溢出的按钮可以点击

有两种方案

方案一

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

java 复制代码
@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);
}

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

推荐方案二:委托

小应用场景:有时候一个按钮效果很小,就很难触发点击事件,我们通常会增大下这个点击区间范围。

大应用场景:我实现了多个脑图的功能,里面因为方便画线穿插过某个UI,就用到了此类知识。

其他情况多种多样,相信看这篇文章的你也是因为有这个需求才查找的。

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

  1. 直接增大 View 的宽高,然后给View设置内边距 padding ;或者直接嵌套一层给这个父设置点击,但这会增加布局嵌套进而消耗性能。
  2. 利用委托功能直接增大点击的区间范围。
java 复制代码
    /**
     * 扩展点击区域的范围
     * @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));
                }
            });
        }
    }

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

java 复制代码
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;
    }
}

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

java 复制代码
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;
    }
}

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

复杂场景实现-重点

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

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<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">
<!--特别留意,因为是自定义的RelativeLayout,Touch时间默认只有个down,需要设置可点击才能回调所有的事件-->
        <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>
java 复制代码
/**
 * @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);
    }
}
java 复制代码
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. 自定义 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 的触摸范围:

java 复制代码
/**
 * 还原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);
			}
		}
	});
}

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

👉其他

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

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

相关推荐
ac-er88882 小时前
Yii框架中的队列:如何实现异步操作
android·开发语言·php
流氓也是种气质 _Cookie4 小时前
uniapp 在线更新应用
android·uniapp
zhangphil6 小时前
Android ValueAnimator ImageView animate() rotation,Kotlin
android·kotlin
命运之手6 小时前
[ Spring ] Nacos Config Auto Refresh 2025
spring·nacos·kotlin·config·refresh
徊忆羽菲6 小时前
CentOS7使用源码安装PHP8教程整理
android
编程、小哥哥8 小时前
python操作mysql
android·python
Couvrir洪荒猛兽8 小时前
Android实训十 数据存储和访问
android
闲暇部落10 小时前
kotlin内联函数——let,run,apply,also,with的区别
kotlin·内联函数
五味香10 小时前
Java学习,List 元素替换
android·java·开发语言·python·学习·golang·kotlin
十二测试录11 小时前
【自动化测试】—— Appium使用保姆教程
android·经验分享·测试工具·程序人生·adb·appium·自动化