在视图上绘制内容的一种方法是使用 ViewOverlay 实现。ViewOverlay 和 ViewGroupOverlay 用于添加可在视图顶部绘制的任意数量的 Drawable 对象。但是应用程序不能直接创建ViewOverlay,而是在层次结构中的任意视图上调用 getOverlay() 来获得ViewOverlay。视图被约束为只能在其边界内绘制,因此如果覆盖层中的内容延伸出驻留覆盖层的视图的边界,则超出边界的部分会被裁剪。
ViewGroupOverlay 是在 ViewGroup 上调用 getOverlay() 返回得到的,ViewGroupOverlay 具有附加的 add() 和 remove() 方法,用于操作视图而非 Drawable。
任意 View 子类(无论是显示文本、HTML、图片、还是显示一些自定义的内容)都可以操作覆盖层。
下面一个示例是:应用程序在可交互视图上用户触摸的位置放置箭头标志或者调整大小的方框。只要用户按下手指并拖动,就可以移动标志或者调整方框的大小。释放触摸之后,一个标记会持续显示在视图上,直到再次点击该标记,就会彻底的将其删除。
效果图(API 至少18)如下:
代码中有详细的注释,这里就不在叙述其思路。
MainActivity.java :
package com.crazy.viewoverlaytest;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.MotionEvent;
import android.view.View;
import android.widget.RadioGroup;
import java.util.ArrayList;
public class MainActivity extends AppCompatActivity implements View.OnTouchListener{
private RadioGroup mOptions;
private ArrayList<Drawable> mMarkers;
private Drawable mTrackingMarker;
private Point mTrackingPoint;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// 接收要在其上绘制的视图的触摸事件
findViewById(R.id.textView).setOnTouchListener(this);
mOptions = (RadioGroup)findViewById(R.id.container_options);
mMarkers = new ArrayList<>();
}
/**
* 所监控视图中的触摸事件将在此处传递
*/
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (mOptions.getCheckedRadioButtonId()) {
case R.id.option_box:
handleEvent(R.id.option_box, v, event);
break;
case R.id.option_arrow:
handleEvent(R.id.option_arrow, v, event);
break;
default:
return false;
}
return true;
}
/**
* 当用户选择绘制方框时处理触摸事件
*/
private void handleEvent(int optionId, View v, MotionEvent event) {
int x = (int)event.getX();
int y = (int)event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Drawable current = markerAt(x, y);
// 遍历后得到的 Drawable 对象的起始位置不在已经绘制上的对象上;
// 也就是说,没有点击在已经绘制好的图形上
if (current == null) {
// 在新的触摸上添加新的标记
switch (optionId) {
case R.id.option_box:
mTrackingMarker = addBox(v, x, y);
mTrackingPoint = new Point(x, y);
break;
case R.id.option_arrow:
mTrackingMarker = addFlag(v, x, y);
break;
}
} else {
// 移除现有的标记
// 当点击在了已经绘制好的图形上,就会删除
// (矩形包括其内部也是整个的图形,不是其内部看上去矩形是空白的就不包括)
removeMarker(v, current);
}
break;
case MotionEvent.ACTION_MOVE:
// 在移动时更新当前标记
if (mTrackingMarker != null) {
switch (optionId) {
case R.id.option_box:
resizeBox(v, mTrackingMarker, mTrackingPoint, x, y);
break;
case R.id.option_arrow:
offsetFlag(v, mTrackingMarker, x, y);
break;
}
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
// 当手势结束时清除状态(相当于清除缓存)
mTrackingMarker = null;
mTrackingPoint = null;
break;
}
}
/**
* 更新现有标记的位置
*/
private void offsetFlag(View v, Drawable marker, int x, int y) {
// 与 RectF 有些不同,RectF 的坐标是浮点数,而 Rect 则是整数。(当然还有其他的一些区别)
Rect bounds = new Rect(marker.getBounds());
// 移动 Drawable 的边界以对齐新的坐标
// x轴的坐标在箭头的中心上,y轴的坐标在箭头底部的 x轴上,交界处就是箭头的箭尖处(图形箭头方向向下)
bounds.offset(x - bounds.left - (bounds.width() / 2),
y - bounds.top - bounds.height());
// 更新并重绘
marker.setBounds(bounds);
v.invalidate();
}
/**
* 更新现有方框以基于给定坐标调整大小
*/
private void resizeBox(View v, Drawable target, Point trackingPoint, int x, int y) {
Rect bounds = new Rect(target.getBounds());
// 如果新的触摸点位于跟踪点的左侧,则向左增大,就是从右往左点击拖动
// 否则,向右增大,也就是从左往右拖动
if (x < trackingPoint.x) {
bounds.left = x;
} else {
bounds.right = x;
}
// 如果新的触摸点位于跟踪点的上方,则向上增大,就是从下往上点击拖动
// 否则,向下增大,就是从上往下点击拖动
if (y < trackingPoint.y) {
bounds.top = y;
} else {
bounds.bottom = y;
}
// 由上面的 x轴,y轴方向拖动就可以绘制一个矩形
// 更新 Drawable 的边界并重绘
target.setBounds(bounds);
v.invalidate();
}
/**
* 在给定坐标处添加新的标记
*/
private Drawable addFlag(View v, int x, int y) {
// 创建新的标记 Drawable
Drawable marker = getResources().getDrawable(R.drawable.jiantou);
// 创建符合图片大小的边界
Rect bounds = new Rect(0, 0, marker.getIntrinsicWidth(), marker.getIntrinsicHeight());
// 在坐标周围底部居中标记;
// 由于图片是从原点开始绘制,如果注释掉这段代码,则在屏幕上点击(不拖动),就会只在左上角绘制
// 不会点击哪里,哪里就会有相应的绘制
bounds.offset(x - (bounds.width()/7), y - bounds.height());
marker.setBounds(bounds);
// 添加到覆盖层
mMarkers.add(marker);
// 需要 API 至少18以上
v.getOverlay().add(marker);
return marker;
}
/**
* 移除请求的标记条目
*/
private void removeMarker(View v, Drawable marker) {
mMarkers.remove(marker);
// 需要 API 至少18以上
v.getOverlay().remove(marker);
}
/**
* 在给定坐标处添加新的可调整大小的方框
*/
private Drawable addBox(View v, int x, int y) {
Drawable box = getResources().getDrawable(R.drawable.box);
// 在触摸点首先创建一个大小为 0 的方框
Rect bounds = new Rect(x, y, x , y);
box.setBounds(bounds);
// 添加到 ViewOverlay
mMarkers.add(box);
// 需要 API 至少18以上
v.getOverlay().add(box);
return box;
}
/**
* 查找包含请求坐标的第一个标记(如果存在的话)
*/
private Drawable markerAt(int x, int y) {
// 返回找到的包含给定点的第一个标记
for (Drawable marker : mMarkers) {
if (marker.getBounds().contains(x, y)) {
return marker;
}
}
return null;
}
}
对于初始的 ACTION_DOWN ,只调用 addBox() 或 addFlag() 以创建新的 Drawable ,使用setBounds(0 设置它的大小和位置,将其应用到主视图的 ViewOverlay 上。只需要调用 add(),就可以将标记添加到覆盖层。
ViewOverlay 没有提供合适的方法来跟踪所添加的条目,因为维护自己的条目列表,该列表稍后可用基于触摸查找标记。
其中的边界大小有来自图片资源内在的高度和宽度;但是,取自 XML 的内容没有内在的大小,必须明确设置其大小。无论何种情况,特别是在 ViewOverlay 中使用时,边界用于协助在正确的位置放置内容,因此一定要为每个添加的元素之少调用 setBounds() 一次。
布局文件, content_main.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:orientation="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="com.crazy.viewoverlaytest.MainActivity"
tools:showIn="@layout/activity_main">
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="center"
android:text="欢迎访问:http://blog.csdn.net/antimage08" />
<RadioGroup
android:id="@+id/container_options"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="#CCC">
<RadioButton
android:id="@+id/option_box"
android:checked="true"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="方框" />
<RadioButton
android:id="@+id/option_arrow"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="箭头"/>
</RadioGroup>
</LinearLayout>
布局文件用到的 box.xml (在 drawable 目录下):
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@android:color/transparent" />
<stroke android:width="3dp" android:color="#F00" />
</shape>