引导控件
1.可在XML文件中直接绑定当页需引导的控件集合 2.可在java文件中手动绑定当页需引导的控件集合,亦可单独绑定/添加 3.可在java文件中手动绑定当页需引导的矩阵位置集合,亦可单独绑定/添加 注:绑定集合则跳转集合首位引导位置,绑定单一引导则跳转至该引导,添加时不跳转 支持矩形/圆角矩形/椭圆形镂空标注引导位置 支持任意View子控件做提示标注(标注位置自动计算),但标注控件需要为GuideView的ChildView
public class GuideView extends FrameLayout {
public static final int TYPE_RECT = 0, TYPE_ROUND_RECT = 1, TYPE_OVAL = 2;
private RectF rectF;
private Region region;
private View hintView;
private Path innerPath;
private Paint boundPaint;
private String resourceIds;
private OnBindListener opListener;
private ArrayList<RectF> relationRects;
private ArrayList<String> hintResource;
private boolean isDrawBound, isLayoutFinished;
private float offset,//目标内边距
radius,
distanceX, distanceY;//提示视图和目标边距
private int clipType, backgroundColor, hintViewId, stepNum=-1;
private ArrayList<View> views;
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (relationRects != null) {
relationRects.clear();
}
if (hintResource != null) {
hintResource.clear();
}
if (views != null) {
views.clear();
}
relationRects = null;
hintResource = null;
resourceIds = null;
boundPaint = null;
opListener = null;
innerPath = null;
hintView = null;
region = null;
rectF = null;
views = null;
}
@Retention(RetentionPolicy.SOURCE)
@IntDef({TYPE_RECT, TYPE_ROUND_RECT, TYPE_OVAL})
public @interface clipType {
}
public GuideView(Context context) {
this(context, null);
}
public GuideView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public GuideView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.GuideView);
clipType = array.getInt(R.styleable.GuideView_clipType, TYPE_RECT);
resourceIds = array.getString(R.styleable.GuideView_relation_ids);
hintViewId = array.getResourceId(R.styleable.GuideView_hint_view_id, NO_ID);
offset = array.getDimension(R.styleable.GuideView_offset, BaseUtils.dp2px(10));
float distance = array.getDimension(R.styleable.GuideView_distance, BaseUtils.dp2px(20));
distanceX = array.getDimension(R.styleable.GuideView_distanceX, distance);
distanceY = array.getDimension(R.styleable.GuideView_distanceY, distance);
radius = array.getDimension(R.styleable.GuideView_android_radius, BaseUtils.dp2px(10));
backgroundColor = array.getColor(R.styleable.GuideView_backgroundColor, context.getResources().getColor(R.color.translucent));
float boundWidth = array.getDimension(R.styleable.GuideView_boundWidth, 0);
int boundColor = array.getColor(R.styleable.GuideView_boundColor, Color.TRANSPARENT);
array.recycle();
relationRects = new ArrayList<>();
innerPath = new Path();
rectF = new RectF();
region = new Region();
setWillNotDraw(false);
boundPaint = new Paint();
boundPaint.setStyle(Paint.Style.STROKE);
boundPaint.setAntiAlias(true);
setBoundWidth(boundWidth, false);
setBoundColor(boundColor, false);
try {
getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
getViewTreeObserver().removeOnGlobalLayoutListener(this);
findRelationView();
}
});
} catch (Exception ignored) {
isLayoutFinished = true;
}
}
private void findRelationView() {
isLayoutFinished = true;
if (views != null) {
bindViews(views);
} else {
bindRelationViews();
}
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
if (hintViewId != NO_ID) {
hintView = findViewById(hintViewId);
}
}
private void bindRelationViews() {
try {
if (resourceIds == null || TextUtils.isEmpty(resourceIds))
return;
View rootView = getRootView();
String[] split = resourceIds.split(",");
for (String s : split) {
try {
addRelationView(rootView.findViewById(ResourceUtils.getIdByName(s)));
} catch (Exception ignored) {
}
}
jumpTo(0);
} catch (Exception ignored) {
}
}
public void setLabelView(View labelView) {
this.labelView = labelView;
bringChildToFront(labelView);
}
public void setDistanceX(float px) {
if (distanceX != px) {
this.distanceX = px;
if (!isInLayout()) {
requestLayout();
}
}
}
public void setDistanceY(float px) {
if (distanceY != px) {
this.distanceY = px;
if (!isInLayout()) {
requestLayout();
}
}
}
public void setDistance(float px) {
if (distanceX != px || distanceY != px) {
this.distanceY = px;
this.distanceX = px;
if (!isInLayout()) {
requestLayout();
}
}
}
public void setBoundWidth(int dp) {
setBoundWidth(BaseUtils.dp2px(dp), true);
}
public void setBoundWidth(float px) {
setBoundWidth(px, true);
}
public void setBoundWidth(float px, boolean isRefresh) {
if (boundPaint.getStrokeWidth() != px) {
isDrawBound = px > 0;
boundPaint.setStrokeWidth(px);
if (isRefresh) {
invalidate();
}
}
}
public void setBoundColor(@ColorInt int color) {
setBoundColor(color, true);
}
public void setBoundColor(@ColorInt int color, boolean isRefresh) {
if (boundPaint.getColor() != color) {
isDrawBound = isDrawBound && (color != Color.TRANSPARENT);
boundPaint.setColor(color);
if (isRefresh) {
invalidate();
}
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
//布局提示控件
if (hintView != null && rectF != null && rectF.width() > 0 && rectF.height() > 0) {
int width = hintView.getWidth();
int height = hintView.getHeight();
float realWidth = width + distanceX;
float realHeight = height + distanceY;
int _left = (int) (rectF.left - (realWidth));
int _top = (int) (rectF.top - realHeight);
int _right = (int) (rectF.right + realWidth);
int _bottom = (int) (rectF.bottom + realHeight);
left += getPaddingLeft();
right -= getPaddingRight();
top += getPaddingTop();
bottom -= getPaddingBottom();
if (_top >= top && _top + height <= rectF.top) {//目标上边
if (_left < left) {
_left = left;
}
hintView.layout(_left, _top, Math.min(_left + width, right), _top + height);
} else if (_left >= left && _left + width <= rectF.left) {//目标左边
if (_top < top) {
_top = top;
}
hintView.layout(_left, _top, _left + width, Math.min(_top + height, bottom));
} else if (_right <= right && _right - width >= rectF.right) {//目标右边
if (_bottom > bottom) {
_bottom = bottom;
}
hintView.layout(_right - width, Math.max(_bottom - height, bottom), _right, _bottom);
} else if (_bottom <= bottom && _bottom - height >= rectF.bottom) {//目标下边
if (_right > right) {
_right = right;
}
hintView.layout(Math.max(_right - width, left), _bottom - height, _right, _bottom);
} else {//目标内左上角
int x = (int) (rectF.left + distanceX + offset);
int y = (int) (rectF.top + distanceY + offset);
hintView.layout(x, y, x + width, y + height);
}
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec), MeasureSpec.EXACTLY));
}
public void setClipType(@clipType int clipType) {
this.clipType = clipType;
createPath(false);
}
public void setRadius(float radius) {
this.radius = radius;
createPath(false);
}
public void setRect(Rect rect) {
rectF.set(rect);
createPath();
}
public void setRect(RectF rect) {
rectF.set(rect);
createPath();
}
public void setOffset(@FloatRange(from = 0) float offset) {
this.offset = offset;
createPath();
}
/**
* 获取当前目标矩阵
*/
public RectF getRelationRectF() {
return rectF;
}
private void createPath() {
createPath(true);
}
/**
* 创建路径
*/
private void createPath(boolean isRequestLayout) {
if (offset > 0) {
rectF.left -= offset;
rectF.top -= offset;
rectF.right += offset;
rectF.bottom += offset;
}
innerPath.reset();
innerPath.moveTo(rectF.left, rectF.top);
if (clipType == TYPE_OVAL) {
innerPath.addOval(rectF, Path.Direction.CW);
} else if (clipType == TYPE_ROUND_RECT) {
innerPath.addRoundRect(rectF, radius, radius, Path.Direction.CW);
} else {
innerPath.addRect(rectF, Path.Direction.CW);
}
innerPath.close();
innerPath.computeBounds(rectF, true);
region.setEmpty();
region.setPath(innerPath, new Region((int) rectF.left, (int) rectF.top, (int) rectF.right, (int) rectF.bottom));
if (isRequestLayout && !isInLayout()) {
requestLayout();
}
postInvalidate();
}
/**
* 清空原有目标 绑定所有目标 并 显示对首目标
*/
public void bindRectF(ArrayList<RectF> rectFs) {
if (rectFs != null && rectFs.size() > 0) {
if (relationRects == null) {
relationRects = new ArrayList<>();
} else {
relationRects.clear();
}
if (relationRects.addAll(rectFs)) {
jumpTo(0);
}
}
}
/**
* 如果存在当前目标 则绑定显示 否则添加值队尾并绑定显示
*/
public void bindRectF(RectF rectF) {
if (rectF == null)
return;
if (relationRects != null) {
int index = relationRects.indexOf(rectF);
if (index > -1) {
jumpTo(index);
} else if (addRelationRectF(rectF)) {
jumpTo(relationRects.size() - 1);
}
} else {
relationRects = new ArrayList<>();
addRelationRectF(rectF);
jumpTo(0);
}
}
public void jumpToNext() {
jumpTo(stepNum + 1);
}
/**
* 绑定显示指定位置的目标
*/
public void jumpTo(int index) {
RectF rect = null;
if (relationRects != null && relationRects.size() > index) {
rect = relationRects.get(index);
}
if (rect == null||stepNum == index)
return;
stepNum = index;
if (opListener != null) {
opListener.onBind(this, index);
}
try {
if (hintView instanceof TextView && hintResource != null && hintResource.size() > index) {
((TextView) hintView).setText(hintResource.get(index));
}
} catch (Exception ignored) {
}
setRect(rect);
}
/**
* 绑定显示指定控件位置目标
*/
public void bindView(View view) {
bindRectF(getRelationViewRectF(view));
}
/**
* 清空原有目标 绑定所有目标 并 显示对首目标
*/
public void bindViews(ArrayList<View> views) {
if (this.views == null) {
this.views = views;
}
if (isLayoutFinished && views != null && views.size() > 0) {
ArrayList<RectF> rectFS = new ArrayList<>();
for (View v : views) {
rectFS.add(getRelationViewRectF(v));
}
bindRectF(rectFS);
}
}
public void bindHintText(ArrayList<String> hintResource) {
this.hintResource = hintResource;
}
/**
* 附加目标
*/
public boolean addRelationView(View view) {
return addRelationView(view, -1);
}
/**
* 附加目标
*/
public boolean addRelationView(View view, int index) {
return addRelationRectF(getRelationViewRectF(view), index);
}
/**
* 附加目标
*/
public boolean addRelationRectF(RectF rectF) {
return addRelationRectF(rectF, -1);
}
/**
* 附加目标
*/
public boolean addRelationRectF(RectF rectF, int index) {
try {
if (relationRects != null && rectF != null && !relationRects.contains(rectF)) {
if (index > -1) {
relationRects.add(index, rectF);
} else {
relationRects.add(rectF);
}
}
return true;
} catch (Exception ignored) {
}
return false;
}
public RectF getRelationViewRectF(View view) {
if (view == null)
return null;
int[] size = new int[2];
view.getLocationInWindow(size);
float x = size[0];
float y = size[1];
getLocationInWindow(size);
float left = x - size[0];
float top = y - size[1];
RectF rectF = new RectF();
rectF.left = left;
rectF.top = top;
rectF.right = left + view.getWidth();
rectF.bottom = top + view.getHeight();
return rectF;
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
boolean isContain = event != null && region != null && region.contains((int) event.getX(), (int) event.getY());
if (isContain) {
if (opListener == null || !opListener.onRelationViewClick(this, stepNum + 1)) {
jumpToNext();
}
}
return !(isContain || isClickLabel(event)) || super.dispatchTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return isClickLabel(ev) || super.onInterceptTouchEvent(ev);
}
private boolean isClickLabel(MotionEvent ev) {
if (ev == null || hintView== null)
return false;
float x = ev.getX();
float y = ev.getY();
return x >= hintView.getLeft() && x <= hintView.getRight()
&& y >= hintView.getTop() && y <= hintView.getBottom();
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
if (isClickLabel(event)) {
if (!onTouchEvent(hintView, event) && hintViewinstanceof ViewGroup) {
ViewGroup group = (ViewGroup) this.labelView;
for (int i = 0; i < group.getChildCount(); i++) {
onTouchEvent(group.getChildAt(i), event);
}
}
return true;
}
return super.onTouchEvent(event);
}
private boolean onTouchEvent(View v, MotionEvent event) {
return v != null && v.isEnabled() && v.isClickable() && isTouchView(v, event) && v.performClick();
}
private boolean isTouchView(View v, MotionEvent ev) {
if (v == null || ev == null) {
return false;
}
if (v == hintView)
return true;
int[] l = new int[2];
v.getLocationInWindow(l);
int left = l[0], top = l[1], bottom = top + v.getHeight(), right = left
+ v.getWidth();
float x = ev.getRawX();
float y = ev.getRawY();
return x >= left && x <= right && y >= top && y <= bottom;
}
@Override
public void setBackground(Drawable background) {
}
@Override
public void setBackgroundResource(int resid) {
}
@Override
public void setBackgroundDrawable(Drawable background) {
}
@Override
public void setBackgroundColor(int backgroundColor) {
if (this.backgroundColor != backgroundColor) {
this.backgroundColor = backgroundColor;
postInvalidate();
}
}
@Override
public void onDrawForeground(Canvas canvas) {
super.onDrawForeground(canvas);
canvas.save();
if (innerPath == null || innerPath.isEmpty())
return;
if (hintView != null) {
canvas.clipRect(hintView.getLeft(), hintView.getTop(), hintView.getRight(), hintView.getBottom(), Region.Op.DIFFERENCE);
}
//绘制背景
canvas.clipPath(innerPath, Region.Op.DIFFERENCE);
canvas.drawColor(backgroundColor);
if (isDrawBound) {
canvas.drawPath(innerPath, boundPaint);
}
canvas.restore();
}
public void setOnNextListener(OnBindListener nextListener) {
this.opListener = nextListener;
}
public interface OnBindListener {
/**
* 绑定目标视图事件(绘制前)
*
* @param stepNum 当前目标id
*/
void onBind(GuideView view, int stepNum);
/**
* 当前目标视图点击事件
*
* @param nextStepNum 下一个目标id
* @return 是否拦击自动绑定下一个目标视图
*/
boolean onRelationViewClick(GuideView view, int nextStepNum);
}
}
attrs文件
<declare-styleable name="GuideView">
<attr name="relation_ids" format="string" />
<attr name="hint_view_id" format="reference" />
<attr name="offset" format="dimension" />
<attr name="distance" format="dimension" />
<attr name="distanceX" format="dimension" />
<attr name="distanceY" format="dimension" />
<attr name="android:radius" />
<attr name="backgroundColor" format="color|reference" />
<attr name="boundColor" format="color|reference" />
<attr name="boundWidth" format="dimension" />
<attr name="clipType" format="enum">
<enum name="RECT" value="0" />
<enum name="ROUND_RECT" value="1" />
<enum name="OVAL" value="2" />
</attr>
</declare-styleable>
使用示例
xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<方式一内容布局>
<com.*.GuideView
android:id="@+id/guide_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
app:backgroundColor="@color/translucent"
app:boundColor="@color/colorPrimaryDark"
app:distanceX="-10dp"
app:relation_ids="idName,idName,idName,idName,idName"//关联内容布局中的目标idName
app:hint_view_id="@id/tv_hint"//关联目标内容提示控件的资源id
app:boundWidth="5dp">
<TextView
android:id="@+id/tv_hint"
android:layout_width="50dp"
android:layout_height="wrap_content"
android:background="@color/colorAccent"
android:text="asfasdfasdfasdfasdfsadfasffsadfsdaf"
android:textColor="@color/colorPrimary"
android:textSize="12sp" />
<方式二内容布局>
</com.*.GuideView>
</FrameLayout>
注意 容器必须为FrameLayout 之类的可以让GuideView match_parent 的容器
java
//java绑定目标集合方式一 按添加顺序进行引导
ArrayList<RectF> objects = new ArrayList<>();
objects.add(guideView.getRelationViewRectF(目标控件1));
objects.add(guideView.getRelationViewRectF(目标控件2));
objects.add(guideView.getRelationViewRectF(目标控件3));
objects.add(guideView.getRelationViewRectF(目标控件4));
objects.add(guideView.getRelationViewRectF(目标控件5));
guideView.bindRectF(objects);
//java绑定目标集合方式二 按添加顺序进行引导
ArrayList<View> objects = new ArrayList<>();
objects.add(目标控件1);
objects.add(目标控件2);
objects.add(目标控件3);
objects.add(目标控件4);
objects.add(目标控件5);
guideView.bindViews(objects);
//单一目标绑定方式 添加至队尾 并跳转至该引导
guideView.bindRectF(RectF rectF);
guideView.bindView(View view);
//跳转至index步
guideView. jumpTo(int index);
//单一目标添加方式 添加至队尾 /指定位置
guideView.addRelationView(View view) ;
guideView.addRelationView(View view, int index);
guideView.addRelationRectF(RectF rectF);
guideView.addRelationRectF(RectF rectF, int index);
//绑定提示文字 提示控件为文本控件时生效 内容顺序需和引导目标集合顺序一致 可在OnBindListener 监听中自定义提示
bindHintText(ArrayList<String> hintResource)
监听事件
public interface OnBindListener {
/**
* 跳转至指定引导目标时(绘制之前) 可修改提示文字和引导目标边框绘制属性
*
* @param stepNum 当前引导顺序指针
*/
void onBind(GuideView view, int stepNum);
/**
* 当前引导目标位置点击事件 可拦截自定义处理跳转
*
* @param nextStepNum 下一个目标顺序指针
* @return 是否拦击自动跳转下一个引导目标
*/
boolean onRelationViewClick(GuideView view, int nextStepNum);
}
|