自定义引导控件
自定义引导控件
-
-
-
- 引导控件
- attrs文件
- 使用示例
-
- xml
- java
- 监听事件
-
-
引导控件
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 relationRects; private ArrayList hintResource; private boolean isDrawBound, isLayoutFinished; private float offset,//目标内边距 radius, distanceX, distanceY;//提示视图和目标边距 private int clipType, backgroundColor, hintViewId, stepNum=-1; private ArrayList 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 + width <= rectF.left) {//目标左边 if (_top < top) { _top = top; } hintView.layout(_left, _top, _left + width, Math.min(_top + height, bottom)); } else if (_right = rectF.right) {//目标右边 if (_bottom > bottom) { _bottom = bottom; } hintView.layout(_right - width, Math.max(_bottom - height, bottom), _right, _bottom); } else if (_bottom = 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 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 views) { if (this.views == null) { this.views = views; } if (isLayoutFinished && views != null && views.size() > 0) { ArrayList rectFS = new ArrayList(); for (View v : views) { rectFS.add(getRelationViewRectF(v)); } bindRectF(rectFS); } } public void bindHintText(ArrayList 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.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 = left && x = 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文件
使用示例
xml
注意 容器必须为FrameLayout 之类的可以让GuideView
match_parent
的容器
java
//java绑定目标集合方式一 按添加顺序进行引导ArrayList 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 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 hintResource)
监听事件
public interface OnBindListener { / * 跳转至指定引导目标时(绘制之前) 可修改提示文字和引导目标边框绘制属性 * * @param stepNum 当前引导顺序指针 */ void onBind(GuideView view, int stepNum); / * 当前引导目标位置点击事件 可拦截自定义处理跳转 * * @param nextStepNum 下一个目标顺序指针 * @return 是否拦击自动跳转下一个引导目标 */ boolean onRelationViewClick(GuideView view, int nextStepNum); }