自定义引导控件

引导控件

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);
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值