Unity3D特效百例 | 案例项目实战源码 | Android-Unity实战问题汇总 |
---|---|---|
游戏脚本-辅助自动化 | Android控件全解手册 | 再战Android系列 |
Scratch编程案例 | 软考全系列 | Unity3D学习专栏 |
蓝桥系列 | ChatGPT和AIGC |
👉关于作者
专注于Android/Unity和各种游戏开发技巧,以及各种资源分享(网站、工具、素材、源码、游戏等)
有什么需要欢迎底部卡片私我,交流让学习不再孤单。
👉实践过程
😜超出父布局显示
我们实现一个 LinearLayout 布局,宽高是200,里面嵌套一个 Button ,默认是展示出来的。
<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 的间距设置超出父布局。默认是不会展示出来的。
<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是否将剪辑其绘图表面以排除填充区域。
要特别注意
- 如果你某个子 View 嵌套了多层,然后超出了父布局,需要所有的父布局都携带 clipChildren 属性。
- 这个子 View 的最近父布局需要是 RelativeLayout ,博主摸了摸秀发,并没有去深究为什么其他 ViewGroup 为什么不行。
如下:
<?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窗口捕捉点击事件。
@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,就用到了此类知识。
其他情况多种多样,相信看这篇文章的你也是因为有这个需求才查找的。
小应用场景的实现很简单:
- 直接增大 View 的宽高,然后给View设置内边距 padding ;或者直接嵌套一层给这个父设置点击,但这会增加布局嵌套进而消耗性能。
- 利用委托功能直接增大点击的区间范围。
/**
* 扩展点击区域的范围
* @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类的源码。
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来处理。
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 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>
/**
* @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);
}
}
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);
}
}
上面的注释简直是保姆级的了。
- 自定义 TestOverClick 嵌套了个子 Button 控件,设置
android:clickable="true"
可点击,设置属性android:clipChildren="false"
和android:clipToPadding="false"
实现超出区域可见。 - 自定义 TestOverClick 有个方法 initClickRect 是用来设置点击响应区域的,咱们向右侧进行了扩大,红色为默认响应区域,经过计算:布局中控件是距离左300像素 控件本身是200 他俩的中间间距为100 加上按钮的本身宽度。右侧增加了绿框范围的响应区域。
- 接着我们在 onTouchEvent 函数中做两个处理:处理一是判断下点击的区间,通过计算允许在按钮范围内处理,否则的话直接消耗事件,这样就假装模拟出了只响应按钮了。处理二是在事件中抬起的时候回调下按钮的模拟点击事件,就会进入业务逻辑。注意我们真正点击的其实是父控件,只不过模拟点击了按钮。
- 默认模拟点击是没有点击效果的,所以我们在 onTouchEvent 中 down 和 up 的时候自己更改下按钮背景状态即可完美实现点击UI变化。
- 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);
}
}
});
}
还没懂?下方卡片联系我,手把手教你。
👉其他
📢作者:小空和小芝中的小空
📢转载说明-务必注明来源:https://zhima.blog.csdn.net/
📢这位道友请留步☁️,我观你气度不凡,谈吐间隐隐有王者霸气💚,日后定有一番大作为📝!!!旁边有点赞👍收藏🌟今日传你,点了吧,未来你成功☀️,我分文不取,若不成功⚡️,也好回来找我。
温馨提示:点击下方卡片获取更多意想不到的资源。