背景:
需要开发一个如下图所示的"底部有新消息"的提示语。想了几种方案选择,用view直接添加(要改动布局,不想改)、用dialog、popwindow等展示。但是觉得这些对于这个来说有些重,所以考虑有没有其他简单的方案,直接添加一张图片或者直接添加一个view上去,找了下,发现还真有。
这就是要说的ViewOverlay.
ViewOverlay是在Android 4.3的时候添加的,作用就是在view的上面可以覆盖一层drawable(ViewgroupOverlay是可以覆盖View的)。
结合源码,讲下使用过程中遇到的坑
1、ViewGroupOverlay上添加view。
按照其他文档的写法,通过getOverlay()方法,获取ViewGroupOverlay对象,然后调用add()方法,将view添加进去。
mHostView.getOverlay().add(mTextView);
写完之后,问题出现了,界面没有展示!!!通过查看view tree,确实添加进去了,如下图:
我在通过recycleView获取到ViewGroupOverlay(基层ViewOverlay,所以tree中显示的是ViewOverlay),添加了3个textView。但是一个都没展示。通过属性发现view的宽高都是0,OK,那就给个宽高,发现还是不行。
那就只能寄出源码了
查找原因:看源码
public class ViewGroupOverlay extends ViewOverlay {
ViewGroupOverlay(Context context, View hostView) {
super(context, hostView);
}
public void add(@NonNull View view) {
mOverlayViewGroup.add(view);
}
public void remove(@NonNull View view) {
mOverlayViewGroup.remove(view);
}
}
去ViewOverlay中查看mOverlayViewGroup对象,发现其实是OverlayViewGroup对象,继续查看其源码
static class OverlayViewGroup extends ViewGroup {
public void add(@NonNull View child) {
....
super.addView(child);
}
public void remove(@NonNull View view) {
if (view == null) {
throw new IllegalArgumentException("view must be non-null");
}
super.removeView(view);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// Noop: children are positioned absolutely
}
}
OverlayViewGroup是ViewOverlay的静态内部类,而且继承自ViewGroup,这也是ViewGroupOverlay能够添加View的原因。(类名都是相似,注意区分)。
覆写了ViewGroup中的addView、removeView、onLayout等方法。注意其中的onLayout方法,发现并没有layout,只是一个空方法,原来如此,都没有layout,当然在界面展示不了啊。
解决方法:
既然OverlayView没有帮我们layout,那我们就自己调用layout方法,只要在调用onDraw的时候,能够拿到view的l、t、r、b信息就行。代码如下
private void addTips(String tips) {
mHostView.getOverlay().add(mTextView);
//计算文字的宽度和长度
StaticLayout staticLayout = new StaticLayout(tips, mTextView.getPaint(), mHostView.getWidth(), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0,false);
LoggerUtil.i("TipsViewOverlay", staticLayout.getLineCount()+",");
int widthMeasure = View.MeasureSpec.makeMeasureSpec((int) staticLayout.getLineWidth(0), View.MeasureSpec.EXACTLY);
int heightMeasure = View.MeasureSpec.makeMeasureSpec(staticLayout.getHeight(), View.MeasureSpec.EXACTLY);
mTextView.measure(widthMeasure, heightMeasure);
layoutContent(mTextView, mGravity, mOffsetX, mOffsetY, staticLayout);
mTextView.setText(tips);
}
private void layoutContent(View view, int gravity, int offsetX, int offsetY, StaticLayout staticLayout) {
//计算起始点的xy, note:默认为右上
int x = mHostView.getWidth() - (int) staticLayout.getLineWidth(0) - offsetX;
int y = offsetY;
view.layout(x, y, x + (int) staticLayout.getLineWidth(0), y + staticLayout.getHeight());
}
由于是文字,所以通过先通过StaticLayout计算出文字的宽高、行数等信息,根据这些信息,计算出该TextView所需的宽和高,在配合想要展示的位置(这里默认右上)计算出view的l、t、r、b信息。
可以看到view已经展示出来了。
2、点击事件:
view已经展示出来了,那么点击事件却没响应,首先确认该TextView已经添加到view tree中了,那么按照事件传递的逻辑,应该是出发该TextView的响应事件啊。这个具体原因还没找到,不过在官方文档中看到了说明。
Views in the overlay are visual-only; they do not receive input events and do not participate in focus traversal. Overlay views are intended to be transient, such as might be needed by a temporary animation effect.
英语好的人应该看懂了,不能接收input事件和焦点切换,不过可以被动画所影响,可以做一些位移、旋转等动画。
解决方法:
通过设置recycleview的touchlistener来解决,如果触摸事件触摸到了该textView则认为点击了该事件,这段看个人爱好,可以调用dispatchTouchEvent、performclick或者直接执行onClick逻辑。
mRecyclerView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
//确定点击范围是否是点击到overlay
if (event.getAction() == MotionEvent.ACTION_UP) {
if (isTouchPointInView(mTipsViewOverlay.mTextView, event.getX(), event.getY())) {
mTipsViewOverlay.mTextView.performClick();
return true;
}
}
//或者这样处理,两段效果是一样的,都会触发onClick.
if (isTouchPointInView(mTipsViewOverlay.mTextView, event.getX(), event.getY())) {
mTipsViewOverlay.mTextView.dispatchTouchEvent(event);
return true;
}
return false;
}
});
后续:对于点击事件的响应处理还是很粗糙的,如果要添加的view过多,或者有几个view需要点击事件的话,建议在onTouch()方法中,将event事件传递给相应的父view.从而解决该问题。