Просмотр исходного кода

1.添加图片加载占位图
2.添加首次登陆欢迎弹窗

王鹏鹏 2 лет назад
Родитель
Сommit
05c4adbeb3

+ 1 - 0
.idea/misc.xml

@@ -48,6 +48,7 @@
         <entry key="..\:/workspace/hcp-pad/home/src/main/res/drawable/shape_ract_gold.xml" value="0.2185" />
         <entry key="..\:/workspace/hcp-pad/home/src/main/res/layout/activity_home.xml" value="0.2" />
         <entry key="..\:/workspace/hcp-pad/home/src/main/res/layout/item_game.xml" value="0.16" />
+        <entry key="..\:/workspace/hcp-pad/home/src/main/res/layout/layout_play_with_blue_porpoise.xml" value="0.136" />
         <entry key="..\:/workspace/hcp-pad/livebroadcast/src/main/res/drawable/bg_live_broadcast.xml" value="0.219" />
         <entry key="..\:/workspace/hcp-pad/livebroadcast/src/main/res/drawable/bg_live_broadcast_button.xml" value="0.219" />
         <entry key="..\:/workspace/hcp-pad/livebroadcast/src/main/res/drawable/bg_live_room.xml" value="0.219" />

+ 25 - 0
baselib/src/main/java/com/yingyangfly/baselib/guideview/BuildException.java

@@ -0,0 +1,25 @@
+package com.yingyangfly.baselib.guideview;
+
+/**
+ * 遮罩系统运行异常的封装
+ * Created by binIoter
+ */
+
+class BuildException extends RuntimeException {
+
+    private static final long serialVersionUID = 6208777692136933357L;
+    private final String mDetailMessage;
+
+    public BuildException() {
+        mDetailMessage = "General error.";
+    }
+
+    public BuildException(String detailMessage) {
+        mDetailMessage = detailMessage;
+    }
+
+    @Override
+    public String getMessage() {
+        return "Build GuideFragment failed: " + mDetailMessage;
+    }
+}

+ 39 - 0
baselib/src/main/java/com/yingyangfly/baselib/guideview/Common.java

@@ -0,0 +1,39 @@
+package com.yingyangfly.baselib.guideview;
+
+import android.graphics.Rect;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Created by binIoter
+ */
+
+class Common {
+  /**
+   * 设置Component
+   */
+  static View componentToView(LayoutInflater inflater, Component c) {
+    View view = c.getView(inflater);
+    final MaskView.LayoutParams lp = new MaskView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+        ViewGroup.LayoutParams.WRAP_CONTENT);
+    lp.offsetX = c.getXOffset();
+    lp.offsetY = c.getYOffset();
+    lp.targetAnchor = c.getAnchor();
+    lp.targetParentPosition = c.getFitPosition();
+    view.setLayoutParams(lp);
+    return view;
+  }
+
+  /**
+   * Rect在屏幕上去掉状态栏高度的绝对位置
+   */
+  static Rect getViewAbsRect(View view, int parentX, int parentY) {
+    int[] loc = new int[2];
+    view.getLocationInWindow(loc);
+    Rect rect = new Rect();
+    rect.set(loc[0], loc[1], loc[0] + view.getMeasuredWidth(), loc[1] + view.getMeasuredHeight());
+    rect.offset(-parentX, -parentY);
+    return rect;
+  }
+}

+ 86 - 0
baselib/src/main/java/com/yingyangfly/baselib/guideview/Component.java

@@ -0,0 +1,86 @@
+package com.yingyangfly.baselib.guideview;
+
+import android.view.LayoutInflater;
+import android.view.View;
+
+/**
+ * * 遮罩系统中相对于目标区域而绘制一些图片或者文字等view需要实现的接口. <br>
+ *  * <br>
+ *  * {@link #getView(LayoutInflater)} <br>
+ *  * {@link #getAnchor()} <br>
+ *  * {@link #getFitPosition()} <br>
+ *  * {@link #getXOffset()} <br>
+ *  * {@link #getYOffset()}
+ *  * <br>
+ *  * 具体创建遮罩的说明请参加{@link GuideBuilder}
+ *  *
+ *
+ * Created by binIoter
+ */
+
+public interface Component {
+
+  public final static int FIT_START = MaskView.LayoutParams.PARENT_START;
+
+  public final static int FIT_END = MaskView.LayoutParams.PARENT_END;
+
+  public final static int FIT_CENTER = MaskView.LayoutParams.PARENT_CENTER;
+
+  public final static int ANCHOR_LEFT = MaskView.LayoutParams.ANCHOR_LEFT;
+
+  public final static int ANCHOR_RIGHT = MaskView.LayoutParams.ANCHOR_RIGHT;
+
+  public final static int ANCHOR_BOTTOM = MaskView.LayoutParams.ANCHOR_BOTTOM;
+
+  public final static int ANCHOR_TOP = MaskView.LayoutParams.ANCHOR_TOP;
+
+  public final static int ANCHOR_OVER = MaskView.LayoutParams.ANCHOR_OVER;
+
+  /**
+   * 圆角矩形&矩形
+   */
+  public final static int ROUNDRECT = 0;
+
+  /**
+   * 圆形
+   */
+  public final static int CIRCLE = 1;
+
+  /**
+   * 需要显示的view
+   *
+   * @param inflater use to inflate xml resource file
+   * @return the component view
+   */
+  View getView(LayoutInflater inflater);
+
+  /**
+   * 相对目标View的锚点
+   *
+   * @return could be {@link #ANCHOR_LEFT}, {@link #ANCHOR_RIGHT},
+   * {@link #ANCHOR_TOP}, {@link #ANCHOR_BOTTOM}, {@link #ANCHOR_OVER}
+   */
+  int getAnchor();
+
+  /**
+   * 相对目标View的对齐
+   *
+   * @return could be {@link #FIT_START}, {@link #FIT_END},
+   * {@link #FIT_CENTER}
+   */
+  int getFitPosition();
+
+  /**
+   * 相对目标View的X轴位移,在计算锚点和对齐之后。
+   *
+   * @return X轴偏移量, 单位 dp
+   */
+  int getXOffset();
+
+  /**
+   * 相对目标View的Y轴位移,在计算锚点和对齐之后。
+   *
+   * @return Y轴偏移量,单位 dp
+   */
+  int getYOffset();
+}

+ 140 - 0
baselib/src/main/java/com/yingyangfly/baselib/guideview/Configuration.java

@@ -0,0 +1,140 @@
+package com.yingyangfly.baselib.guideview;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.View;
+
+/**
+ * 遮罩系统创建时配置参数的封装 <br/>
+ * Created by binIoter
+ */
+
+class Configuration implements Parcelable {
+
+  /**
+   * 需要被找的View
+   */
+  View mTargetView = null;
+
+  /**
+   * 高亮区域的padding
+   */
+  int mPadding = 0;
+  /**
+   * 高亮区域的左侧padding
+   */
+  int mPaddingLeft = 0;
+  /**
+   * 高亮区域的顶部padding
+   */
+  int mPaddingTop = 0;
+  /**
+   * 高亮区域的右侧padding
+   */
+  int mPaddingRight = 0;
+  /**
+   * 高亮区域的底部padding
+   */
+  int mPaddingBottom = 0;
+
+  /**
+   * 是否可以透过蒙层点击,默认不可以
+   */
+  boolean mOutsideTouchable = false;
+
+  /**
+   * 遮罩透明度
+   */
+  int mAlpha = 255;
+
+  /**
+   * 遮罩覆盖区域控件Id
+   * <p/>
+   * 该控件的大小既该导航页面的大小
+   */
+  int mFullingViewId = -1;
+
+  /**
+   * 目标控件Id
+   */
+  int mTargetViewId = -1;
+
+  /**
+   * 高亮区域的圆角大小
+   */
+  int mCorner = 0;
+
+  /**
+   * 高亮区域的图形样式,默认为矩形
+   */
+  int mGraphStyle = Component.ROUNDRECT;
+
+  /**
+   * 遮罩背景颜色id
+   */
+  int mFullingColorId = android.R.color.black;
+
+  /**
+   * 是否在点击的时候自动退出导航
+   */
+  boolean mAutoDismiss = true;
+
+  /**
+   * 是否覆盖目标控件
+   */
+  boolean mOverlayTarget = false;
+
+  boolean mShowCloseButton = false;
+
+  int mEnterAnimationId = -1;
+
+  int mExitAnimationId = -1;
+
+  @Override
+  public int describeContents() {
+    return 0;
+  }
+
+  @Override
+  public void writeToParcel(Parcel dest, int flags) {
+    dest.writeInt(mAlpha);
+    dest.writeInt(mFullingViewId);
+    dest.writeInt(mTargetViewId);
+    dest.writeInt(mFullingColorId);
+    dest.writeInt(mCorner);
+    dest.writeInt(mPadding);
+    dest.writeInt(mPaddingLeft);
+    dest.writeInt(mPaddingTop);
+    dest.writeInt(mPaddingRight);
+    dest.writeInt(mPaddingBottom);
+    dest.writeInt(mGraphStyle);
+    dest.writeByte((byte) (mAutoDismiss ? 1 : 0));
+    dest.writeByte((byte) (mOverlayTarget ? 1 : 0));
+  }
+
+  public static final Creator<Configuration> CREATOR = new Creator<Configuration>() {
+    @Override
+    public Configuration createFromParcel(Parcel source) {
+      Configuration conf = new Configuration();
+      conf.mAlpha = source.readInt();
+      conf.mFullingViewId = source.readInt();
+      conf.mTargetViewId = source.readInt();
+      conf.mFullingColorId = source.readInt();
+      conf.mCorner = source.readInt();
+      conf.mPadding = source.readInt();
+      conf.mPaddingLeft = source.readInt();
+      conf.mPaddingTop = source.readInt();
+      conf.mPaddingRight = source.readInt();
+      conf.mPaddingBottom = source.readInt();
+      conf.mGraphStyle = source.readInt();
+      conf.mAutoDismiss = source.readByte() == 1;
+      conf.mOverlayTarget = source.readByte() == 1;
+      return conf;
+    }
+
+    @Override
+    public Configuration[] newArray(int size) {
+      return new Configuration[size];
+    }
+  };
+}

+ 35 - 0
baselib/src/main/java/com/yingyangfly/baselib/guideview/DimenUtil.java

@@ -0,0 +1,35 @@
+package com.yingyangfly.baselib.guideview;
+
+import android.content.Context;
+
+/**
+ * Created by binIoter
+ */
+
+public class DimenUtil {
+    
+    /** sp转换成px */
+    public static int sp2px(Context context, float spValue) {
+        float fontScale = context.getApplicationContext().getResources().getDisplayMetrics().scaledDensity;
+        return (int) (spValue * fontScale + 0.5f);
+    }
+
+    /** px转换成sp */
+    public static int px2sp(Context context, float pxValue) {
+        float fontScale = context.getApplicationContext().getResources().getDisplayMetrics().density;
+        return (int) (pxValue / fontScale + 0.5f);
+    }
+
+    /** dip转换成px */
+    public static int dp2px(Context context, float dipValue) {
+        float scale = context.getApplicationContext().getResources().getDisplayMetrics().scaledDensity;
+        return (int) (dipValue * scale + 0.5f);
+    }
+
+    /** px转换成dip */
+    public static int px2dp(Context context, float pxValue) {
+        float scale = context.getApplicationContext().getResources().getDisplayMetrics().scaledDensity;
+        return (int) (pxValue / scale + 0.5f);
+    }
+
+}

+ 268 - 0
baselib/src/main/java/com/yingyangfly/baselib/guideview/Guide.java

@@ -0,0 +1,268 @@
+package com.yingyangfly.baselib.guideview;
+
+import android.app.Activity;
+import android.content.Context;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+
+/**
+ * 遮罩系统的封装 <br>
+ * * 外部需要调用{@link GuideBuilder}来创建该实例,实例创建后调用
+ * * {@link #show(Activity)} 控制显示; 调用 {@link #dismiss()}让遮罩系统消失。 <br>
+ * <p>
+ * Created by binIoter
+ */
+
+public class Guide implements View.OnKeyListener, View.OnTouchListener {
+
+    Guide() {
+    }
+
+    /**
+     * 滑动临界值
+     */
+    private static final int SLIDE_THRESHOLD = 30;
+    private Configuration mConfiguration;
+    private MaskView mMaskView;
+    private Component[] mComponents;
+    // 根据locInwindow定位后,是否需要判断loc值非0
+    private boolean mShouldCheckLocInWindow = true;
+    private GuideBuilder.OnVisibilityChangedListener mOnVisibilityChangedListener;
+    private GuideBuilder.OnSlideListener mOnSlideListener;
+
+    void setConfiguration(Configuration configuration) {
+        mConfiguration = configuration;
+    }
+
+    void setComponents(Component[] components) {
+        mComponents = components;
+    }
+
+    void setCallback(GuideBuilder.OnVisibilityChangedListener listener) {
+        this.mOnVisibilityChangedListener = listener;
+    }
+
+    public void setOnSlideListener(GuideBuilder.OnSlideListener onSlideListener) {
+        this.mOnSlideListener = onSlideListener;
+    }
+
+    /**
+     * 显示遮罩
+     *
+     * @param activity 目标Activity
+     */
+    public void show(Activity activity) {
+        show(activity, null);
+    }
+
+    /**
+     * 显示遮罩
+     *
+     * @param activity 目标Activity
+     * @param overlay  遮罩层view
+     */
+    public void show(Activity activity, ViewGroup overlay) {
+        mMaskView = onCreateView(activity, overlay);
+        if (overlay == null) {
+            overlay = (ViewGroup) activity.getWindow().getDecorView();
+        }
+        if (mMaskView.getParent() == null && mConfiguration.mTargetView != null) {
+            overlay.addView(mMaskView);
+            if (mConfiguration.mEnterAnimationId != -1) {
+                Animation anim = AnimationUtils.loadAnimation(activity, mConfiguration.mEnterAnimationId);
+                assert anim != null;
+                anim.setAnimationListener(new Animation.AnimationListener() {
+                    @Override
+                    public void onAnimationStart(Animation animation) {
+
+                    }
+
+                    @Override
+                    public void onAnimationEnd(Animation animation) {
+                        if (mOnVisibilityChangedListener != null) {
+                            mOnVisibilityChangedListener.onShown();
+                        }
+                    }
+
+                    @Override
+                    public void onAnimationRepeat(Animation animation) {
+
+                    }
+                });
+                mMaskView.startAnimation(anim);
+            } else {
+                if (mOnVisibilityChangedListener != null) {
+                    mOnVisibilityChangedListener.onShown();
+                }
+            }
+        }
+    }
+
+    public void clear() {
+        if (mMaskView == null) {
+            return;
+        }
+        final ViewGroup vp = (ViewGroup) mMaskView.getParent();
+        if (vp == null) {
+            return;
+        }
+        vp.removeView(mMaskView);
+        onDestroy();
+    }
+
+    /**
+     * 隐藏该遮罩并回收资源相关
+     */
+    public void dismiss() {
+        if (mMaskView == null) {
+            return;
+        }
+        final ViewGroup vp = (ViewGroup) mMaskView.getParent();
+        if (vp == null) {
+            return;
+        }
+        if (mConfiguration.mExitAnimationId != -1) {
+            // mMaskView may leak if context is null
+            Context context = mMaskView.getContext();
+            assert context != null;
+
+            Animation anim = AnimationUtils.loadAnimation(context, mConfiguration.mExitAnimationId);
+            assert anim != null;
+            anim.setAnimationListener(new Animation.AnimationListener() {
+                @Override
+                public void onAnimationStart(Animation animation) {
+
+                }
+
+                @Override
+                public void onAnimationEnd(Animation animation) {
+                    vp.removeView(mMaskView);
+                    if (mOnVisibilityChangedListener != null) {
+                        mOnVisibilityChangedListener.onDismiss();
+                    }
+                    onDestroy();
+                }
+
+                @Override
+                public void onAnimationRepeat(Animation animation) {
+
+                }
+            });
+            mMaskView.startAnimation(anim);
+        } else {
+            vp.removeView(mMaskView);
+            if (mOnVisibilityChangedListener != null) {
+                mOnVisibilityChangedListener.onDismiss();
+            }
+            onDestroy();
+        }
+    }
+
+    /**
+     * 根据locInwindow定位后,是否需要判断loc值非0
+     */
+    public void setShouldCheckLocInWindow(boolean set) {
+        mShouldCheckLocInWindow = set;
+    }
+
+    private MaskView onCreateView(Activity activity, ViewGroup overlay) {
+        if (overlay == null) {
+            overlay = (ViewGroup) activity.getWindow().getDecorView();
+        }
+        MaskView maskView = new MaskView(activity);
+        maskView.setFullingColor(activity.getResources().getColor(mConfiguration.mFullingColorId));
+        maskView.setFullingAlpha(mConfiguration.mAlpha);
+        maskView.setHighTargetCorner(mConfiguration.mCorner);
+        maskView.setPadding(mConfiguration.mPadding);
+        maskView.setPaddingLeft(mConfiguration.mPaddingLeft);
+        maskView.setPaddingTop(mConfiguration.mPaddingTop);
+        maskView.setPaddingRight(mConfiguration.mPaddingRight);
+        maskView.setPaddingBottom(mConfiguration.mPaddingBottom);
+        maskView.setHighTargetGraphStyle(mConfiguration.mGraphStyle);
+        maskView.setOverlayTarget(mConfiguration.mOverlayTarget);
+        maskView.setOnKeyListener(this);
+
+        // For removing the height of status bar we need the root content view's
+        // location on screen
+        int parentX = 0;
+        int parentY = 0;
+        if (overlay != null) {
+            int[] loc = new int[2];
+            overlay.getLocationInWindow(loc);
+            parentX = loc[0];
+            parentY = loc[1];
+        }
+
+        if (mConfiguration.mTargetView != null) {
+            maskView.setTargetRect(Common.getViewAbsRect(mConfiguration.mTargetView, parentX, parentY));
+        } else {
+            // Gets the target view's abs rect
+            View target = activity.findViewById(mConfiguration.mTargetViewId);
+            if (target != null) {
+                maskView.setTargetRect(Common.getViewAbsRect(target, parentX, parentY));
+            }
+        }
+
+        if (mConfiguration.mOutsideTouchable) {
+            maskView.setClickable(false);
+        } else {
+            maskView.setOnTouchListener(this);
+        }
+
+        // Adds the components to the mask view.
+        for (Component c : mComponents) {
+            maskView.addView(Common.componentToView(activity.getLayoutInflater(), c));
+        }
+
+        return maskView;
+    }
+
+    private void onDestroy() {
+        mConfiguration = null;
+        mComponents = null;
+        mOnVisibilityChangedListener = null;
+        mOnSlideListener = null;
+        mMaskView.removeAllViews();
+        mMaskView = null;
+    }
+
+    @Override
+    public boolean onKey(View v, int keyCode, KeyEvent event) {
+        if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) {
+            if (mConfiguration != null && mConfiguration.mAutoDismiss) {
+                dismiss();
+                return true;
+            } else {
+                return false;
+            }
+        }
+        return false;
+    }
+
+    float startY = -1f;
+
+    @Override
+    public boolean onTouch(View view, MotionEvent motionEvent) {
+        if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
+            startY = motionEvent.getY();
+        } else if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
+            if (startY - motionEvent.getY() > DimenUtil.dp2px(view.getContext(), SLIDE_THRESHOLD)) {
+                if (mOnSlideListener != null) {
+                    mOnSlideListener.onSlideListener(GuideBuilder.SlideState.UP);
+                }
+            } else if (motionEvent.getY() - startY > DimenUtil.dp2px(view.getContext(), SLIDE_THRESHOLD)) {
+                if (mOnSlideListener != null) {
+                    mOnSlideListener.onSlideListener(GuideBuilder.SlideState.DOWN);
+                }
+            }
+            if (mConfiguration != null && mConfiguration.mAutoDismiss) {
+                dismiss();
+            }
+        }
+        return true;
+    }
+}

+ 362 - 0
baselib/src/main/java/com/yingyangfly/baselib/guideview/GuideBuilder.java

@@ -0,0 +1,362 @@
+package com.yingyangfly.baselib.guideview;
+
+import android.view.View;
+
+import androidx.annotation.AnimatorRes;
+import androidx.annotation.IdRes;
+import androidx.annotation.IntRange;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ *
+ * <h1>遮罩系统构建器</h1>
+ * 本系统能够快速的为一个Activity里的任何一个View控件创建一个遮罩式的引导页。
+ * </p>
+ * <h3>工作原理</h3>
+ * 首先它需要一个目标View或者它的id,我们通过findViewById来得到这个View,计算它在屏幕上的区域targetRect,参见
+ * {@link #setTargetViewId(int)}与{@link #setTargetView(View)}通过这个区域,
+ * 开始绘制一个覆盖整个Activity的遮罩,可以定义蒙板的颜色{@link #setFullingColorId(int)}和透明度
+ * {@link #setAlpha(int)}。然而目标View的区域不会被绘制从而实现高亮的效果。
+ * </p>
+ * 接下来是在相对于这个targetRect的区域绘制一些图片或者文字。我们把这样一张图片或者文字抽象成一个Component接口
+ * {@link Component},设置文字或者图片等
+ * {@link Component#getView(android.view.LayoutInflater)}
+ * . 所有的图片文字都是相对于targetRect来定义的。可以设定额外的x,
+ * {@link Component#getXOffset()} ;y偏移量,
+ * {@link Component#getYOffset()}。
+ * </p>
+ * 可以对遮罩系统设置可见状态的发生变化时的监听回调
+ * {@link #setOnVisibilityChangedListener(OnVisibilityChangedListener)}
+ * </p>
+ * 可以对遮罩系统设置开始和结束时的动画效果 {@link #setEnterAnimationId(int)}
+ * {@link #setExitAnimationId(int)}
+ * </p>
+ *
+ * Created by binIoter
+ **/
+
+public class GuideBuilder {
+
+    public enum SlideState {
+        UP,DOWN;
+    }
+
+    private Configuration mConfiguration;
+
+    /**
+     * Builder被创建后,不允许在对它进行更改
+     */
+    private boolean mBuilt;
+
+    private List<Component> mComponents = new ArrayList<Component>();
+    private OnVisibilityChangedListener mOnVisibilityChangedListener;
+    private OnSlideListener mOnSlideListener;
+
+    /**
+     * 构造函数
+     */
+    public GuideBuilder() {
+        mConfiguration = new Configuration();
+    }
+
+    /**
+     * 设置蒙板透明度
+     *
+     * @param alpha [0-255] 0 表示完全透明,255表示不透明
+     * @return GuideBuilder
+     */
+    public GuideBuilder setAlpha(@IntRange(from = 0, to = 255)  int alpha) {
+        if (mBuilt) {
+            throw new BuildException("Already created. rebuild a new one.");
+        } else if (alpha < 0 || alpha > 255) {
+            alpha = 0;
+        }
+        mConfiguration.mAlpha = alpha;
+        return this;
+    }
+
+    /**
+     * 设置目标view
+     */
+    public GuideBuilder setTargetView(View v) {
+        if (mBuilt) {
+            throw new BuildException("Already created. rebuild a new one.");
+        }
+        mConfiguration.mTargetView = v;
+        return this;
+    }
+
+    /**
+     * 设置目标View的id
+     *
+     * @param id 目标View的id
+     * @return GuideBuilder
+     */
+    public GuideBuilder setTargetViewId(@IdRes int id) {
+        if (mBuilt) {
+            throw new BuildException("Already created. rebuild a new one.");
+        }
+        mConfiguration.mTargetViewId = id;
+        return this;
+    }
+
+    /**
+     * 设置高亮区域的圆角大小
+     *
+     * @return GuideBuilder
+     */
+    public GuideBuilder setHighTargetCorner(int corner) {
+        if (mBuilt) {
+            throw new BuildException("Already created. rebuild a new one.");
+        } else if (corner < 0) {
+            mConfiguration.mCorner = 0;
+        }
+        mConfiguration.mCorner = corner;
+        return this;
+    }
+
+    /**
+     * 设置高亮区域的图形样式
+     *
+     * @return GuideBuilder
+     */
+    public GuideBuilder setHighTargetGraphStyle(int style) {
+        if (mBuilt) {
+            throw new BuildException("Already created. rebuild a new one.");
+        }
+        mConfiguration.mGraphStyle = style;
+        return this;
+    }
+
+    /**
+     * 设置蒙板颜色的资源id
+     *
+     * @param id 资源id
+     * @return GuideBuilder
+     */
+    public GuideBuilder setFullingColorId(@IdRes int id) {
+        if (mBuilt) {
+            throw new BuildException("Already created. rebuild a new one.");
+        }
+        mConfiguration.mFullingColorId = id;
+        return this;
+    }
+
+    /**
+     * 是否在点击的时候自动退出蒙板
+     *
+     * @param b true if needed
+     * @return GuideBuilder
+     */
+    public GuideBuilder setAutoDismiss(boolean b) {
+        if (mBuilt) {
+            throw new BuildException("Already created, rebuild a new one.");
+        }
+        mConfiguration.mAutoDismiss = b;
+        return this;
+    }
+
+    /**
+     * 是否覆盖目标
+     *
+     * @param b true 遮罩将会覆盖整个屏幕
+     * @return GuideBuilder
+     */
+    public GuideBuilder setOverlayTarget(boolean b) {
+        if (mBuilt) {
+            throw new BuildException("Already created, rebuild a new one.");
+        }
+        mConfiguration.mOverlayTarget = b;
+        return this;
+    }
+
+    /**
+     * 设置进入动画
+     *
+     * @param id 进入动画的id
+     * @return GuideBuilder
+     */
+    public GuideBuilder setEnterAnimationId(@AnimatorRes int id) {
+        if (mBuilt) {
+            throw new BuildException("Already created. rebuild a new one.");
+        }
+        mConfiguration.mEnterAnimationId = id;
+        return this;
+    }
+
+    /**
+     * 设置退出动画
+     *
+     * @param id 退出动画的id
+     * @return GuideBuilder
+     */
+    public GuideBuilder setExitAnimationId(@AnimatorRes int id) {
+        if (mBuilt) {
+            throw new BuildException("Already created. rebuild a new one.");
+        }
+        mConfiguration.mExitAnimationId = id;
+        return this;
+    }
+
+    /**
+     * 添加一个控件
+     *
+     * @param component 被添加的控件
+     * @return GuideBuilder
+     */
+    public GuideBuilder addComponent(Component component) {
+        if (mBuilt) {
+            throw new BuildException("Already created, rebuild a new one.");
+        }
+        mComponents.add(component);
+        return this;
+    }
+
+    /**
+     * 设置遮罩可见状态变化时的监听回调
+     */
+    public GuideBuilder setOnVisibilityChangedListener(
+            OnVisibilityChangedListener onVisibilityChangedListener) {
+        if (mBuilt) {
+            throw new BuildException("Already created, rebuild a new one.");
+        }
+        mOnVisibilityChangedListener = onVisibilityChangedListener;
+        return this;
+    }
+
+    /**
+     * 设置手势滑动的监听回调
+     */
+    public GuideBuilder setOnSlideListener(
+            OnSlideListener onSlideListener) {
+        if (mBuilt) {
+            throw new BuildException("Already created, rebuild a new one.");
+        }
+        mOnSlideListener = onSlideListener;
+        return this;
+    }
+
+    /**
+     * 设置遮罩系统是否可点击并处理点击事件
+     *
+     * @param touchable true 遮罩不可点击,处于不可点击状态 false 可点击,遮罩自己可以处理自身点击事件
+     */
+    public GuideBuilder setOutsideTouchable(boolean touchable) {
+        mConfiguration.mOutsideTouchable = touchable;
+        return this;
+    }
+
+    /**
+     * 设置高亮区域的padding
+     *
+     * @return GuideBuilder
+     */
+    public GuideBuilder setHighTargetPadding(int padding) {
+        if (mBuilt) {
+            throw new BuildException("Already created. rebuild a new one.");
+        } else if (padding < 0) {
+            mConfiguration.mPadding = 0;
+        }
+        mConfiguration.mPadding = padding;
+        return this;
+    }
+
+    /**
+     * 设置高亮区域的左侧padding
+     *
+     * @return GuideBuilder
+     */
+    public GuideBuilder setHighTargetPaddingLeft(int padding) {
+        if (mBuilt) {
+            throw new BuildException("Already created. rebuild a new one.");
+        } else if (padding < 0) {
+            mConfiguration.mPaddingLeft = 0;
+        }
+        mConfiguration.mPaddingLeft = padding;
+        return this;
+    }
+
+    /**
+     * 设置高亮区域的顶部padding
+     *
+     * @return GuideBuilder
+     */
+    public GuideBuilder setHighTargetPaddingTop(int padding) {
+        if (mBuilt) {
+            throw new BuildException("Already created. rebuild a new one.");
+        } else if (padding < 0) {
+            mConfiguration.mPaddingTop = 0;
+        }
+        mConfiguration.mPaddingTop = padding;
+        return this;
+    }
+
+    /**
+     * 设置高亮区域的右侧padding
+     *
+     * @return GuideBuilder
+     */
+    public GuideBuilder setHighTargetPaddingRight(int padding) {
+        if (mBuilt) {
+            throw new BuildException("Already created. rebuild a new one.");
+        } else if (padding < 0) {
+            mConfiguration.mPaddingRight = 0;
+        }
+        mConfiguration.mPaddingRight = padding;
+        return this;
+    }
+
+    /**
+     * 设置高亮区域的底部padding
+     *
+     * @return GuideBuilder
+     */
+    public GuideBuilder setHighTargetPaddingBottom(int padding) {
+        if (mBuilt) {
+            throw new BuildException("Already created. rebuild a new one.");
+        } else if (padding < 0) {
+            mConfiguration.mPaddingBottom = 0;
+        }
+        mConfiguration.mPaddingBottom = padding;
+        return this;
+    }
+
+    /**
+     * 创建Guide,非Fragment版本
+     *
+     * @return Guide
+     */
+    public Guide createGuide() {
+        Guide guide = new Guide();
+        Component[] components = new Component[mComponents.size()];
+        guide.setComponents(mComponents.toArray(components));
+        guide.setConfiguration(mConfiguration);
+        guide.setCallback(mOnVisibilityChangedListener);
+        guide.setOnSlideListener(mOnSlideListener);
+        mComponents = null;
+        mConfiguration = null;
+        mOnVisibilityChangedListener = null;
+        mBuilt = true;
+        return guide;
+    }
+
+    /**
+     * 手势滑动监听
+     */
+    public static interface OnSlideListener {
+
+        void onSlideListener(SlideState state);
+    }
+
+    /**
+     * 遮罩可见发生变化时的事件监听
+     */
+    public static interface OnVisibilityChangedListener {
+
+        void onShown();
+
+        void onDismiss();
+    }
+}

+ 393 - 0
baselib/src/main/java/com/yingyangfly/baselib/guideview/MaskView.java

@@ -0,0 +1,393 @@
+package com.yingyangfly.baselib.guideview;
+
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+
+/**
+ * Created by binIoter
+ */
+
+class MaskView extends ViewGroup {
+    /**
+     * 高亮区域
+     */
+    private final RectF mTargetRect = new RectF();
+    /**
+     * 蒙层区域
+     */
+    private final RectF mOverlayRect = new RectF();
+
+    /**
+     * 中间变量
+     */
+    private final RectF mChildTmpRect = new RectF();
+    /**
+     * 蒙层背景画笔
+     */
+    private final Paint mFullingPaint;
+    private int mPadding = 0;
+    private int mPaddingLeft = 0;
+    private int mPaddingTop = 0;
+    private int mPaddingRight = 0;
+    private int mPaddingBottom = 0;
+    /**
+     * 是否覆盖目标区域
+     */
+    private boolean mOverlayTarget = false;
+    /**
+     * 圆角大小
+     */
+    private int mCorner = 0;
+    /**
+     * 目标区域样式,默认为矩形
+     */
+    private int mStyle = Component.ROUNDRECT;
+    /**
+     * 挖空画笔
+     */
+    private Paint mEraser;
+    /**
+     * 橡皮擦Bitmap
+     */
+    private Bitmap mEraserBitmap;
+    /**
+     * 橡皮擦Cavas
+     */
+    private Canvas mEraserCanvas;
+
+    private boolean ignoreRepadding;
+
+    private int mInitHeight;
+    private int mChangedHeight = 0;
+    private boolean mFirstFlag = true;
+
+    public MaskView(Context context) {
+        this(context, null, 0);
+    }
+
+    public MaskView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public MaskView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        //自我绘制
+        setWillNotDraw(false);
+
+        WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
+        DisplayMetrics displayMetrics = new DisplayMetrics();
+        wm.getDefaultDisplay().getRealMetrics(displayMetrics);
+        int width = displayMetrics.widthPixels;
+        int height = displayMetrics.heightPixels;
+        mOverlayRect.set(0, 0, width, height);
+        mEraserBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+        mEraserCanvas = new Canvas(mEraserBitmap);
+        mFullingPaint = new Paint();
+        mEraser = new Paint();
+        mEraser.setColor(0xFFFFFFFF);
+        //图形重叠时的处理方式,擦除效果
+        mEraser.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
+        //位图抗锯齿设置
+        mEraser.setFlags(Paint.ANTI_ALIAS_FLAG);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        try {
+            clearFocus();
+            mEraserCanvas.setBitmap(null);
+            mEraserBitmap = null;
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        final int w = MeasureSpec.getSize(widthMeasureSpec);
+        final int h = MeasureSpec.getSize(heightMeasureSpec);
+        if (mFirstFlag) {
+            mInitHeight = h;
+            mFirstFlag = false;
+        }
+        if (mInitHeight > h) {
+            mChangedHeight = h - mInitHeight;
+        } else if (mInitHeight < h) {
+            mChangedHeight = h - mInitHeight;
+        } else {
+            mChangedHeight = 0;
+        }
+        setMeasuredDimension(w, h);
+        mOverlayRect.set(0, 0, w, h);
+        resetOutPath();
+
+        final int count = getChildCount();
+        View child;
+        for (int i = 0; i < count; i++) {
+            child = getChildAt(i);
+            if (child != null) {
+                measureChild(child, widthMeasureSpec, heightMeasureSpec);
+            }
+        }
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        final int count = getChildCount();
+        final float density = getResources().getDisplayMetrics().density;
+        View child;
+        for (int i = 0; i < count; i++) {
+            child = getChildAt(i);
+            if (child == null) {
+                continue;
+            }
+            LayoutParams lp = (LayoutParams) child.getLayoutParams();
+            if (lp == null) {
+                continue;
+            }
+            switch (lp.targetAnchor) {
+                case LayoutParams.ANCHOR_LEFT://左
+                    mChildTmpRect.right = mTargetRect.left;
+                    mChildTmpRect.left = mChildTmpRect.right - child.getMeasuredWidth();
+                    verticalChildPositionLayout(child, mChildTmpRect, lp.targetParentPosition);
+                    break;
+                case LayoutParams.ANCHOR_TOP://上
+                    mChildTmpRect.bottom = mTargetRect.top;
+                    mChildTmpRect.top = mChildTmpRect.bottom - child.getMeasuredHeight();
+                    horizontalChildPositionLayout(child, mChildTmpRect, lp.targetParentPosition);
+                    break;
+                case LayoutParams.ANCHOR_RIGHT://右
+                    mChildTmpRect.left = mTargetRect.right;
+                    mChildTmpRect.right = mChildTmpRect.left + child.getMeasuredWidth();
+                    verticalChildPositionLayout(child, mChildTmpRect, lp.targetParentPosition);
+                    break;
+                case LayoutParams.ANCHOR_BOTTOM://下
+                    mChildTmpRect.top = mTargetRect.bottom;
+                    mChildTmpRect.bottom = mChildTmpRect.top + child.getMeasuredHeight();
+                    horizontalChildPositionLayout(child, mChildTmpRect, lp.targetParentPosition);
+                    break;
+                case LayoutParams.ANCHOR_OVER://中心
+                    mChildTmpRect.left = ((int) mTargetRect.width() - child.getMeasuredWidth()) >> 1;
+                    mChildTmpRect.top = ((int) mTargetRect.height() - child.getMeasuredHeight()) >> 1;
+                    mChildTmpRect.right = ((int) mTargetRect.width() + child.getMeasuredWidth()) >> 1;
+                    mChildTmpRect.bottom = ((int) mTargetRect.height() + child.getMeasuredHeight()) >> 1;
+                    mChildTmpRect.offset(mTargetRect.left, mTargetRect.top);
+                    break;
+            }
+            //额外的xy偏移
+            mChildTmpRect.offset((int) (density * lp.offsetX + 0.5f),
+                    (int) (density * lp.offsetY + 0.5f));
+            child.layout((int) mChildTmpRect.left, (int) mChildTmpRect.top, (int) mChildTmpRect.right,
+                    (int) mChildTmpRect.bottom);
+        }
+    }
+
+    private void horizontalChildPositionLayout(View child, RectF rect, int targetParentPosition) {
+        switch (targetParentPosition) {
+            case LayoutParams.PARENT_START:
+                rect.left = mTargetRect.left;
+                rect.right = rect.left + child.getMeasuredWidth();
+                break;
+            case LayoutParams.PARENT_CENTER:
+                rect.left = (mTargetRect.width() - child.getMeasuredWidth()) / 2;
+                rect.right = (mTargetRect.width() + child.getMeasuredWidth()) / 2;
+                rect.offset(mTargetRect.left, 0);
+                break;
+            case LayoutParams.PARENT_END:
+                rect.right = mTargetRect.right;
+                rect.left = rect.right - child.getMeasuredWidth();
+                break;
+        }
+    }
+
+    private void verticalChildPositionLayout(View child, RectF rect, int targetParentPosition) {
+        switch (targetParentPosition) {
+            case LayoutParams.PARENT_START:
+                rect.top = mTargetRect.top;
+                rect.bottom = rect.top + child.getMeasuredHeight();
+                break;
+            case LayoutParams.PARENT_CENTER:
+                rect.top = (mTargetRect.width() - child.getMeasuredHeight()) / 2;
+                rect.bottom = (mTargetRect.width() + child.getMeasuredHeight()) / 2;
+                rect.offset(0, mTargetRect.top);
+                break;
+            case LayoutParams.PARENT_END:
+                rect.bottom = mTargetRect.bottom;
+                rect.top = mTargetRect.bottom - child.getMeasuredHeight();
+                break;
+        }
+    }
+
+    private void resetOutPath() {
+        resetPadding();
+    }
+
+    /**
+     * 设置padding
+     */
+    private void resetPadding() {
+        if (!ignoreRepadding) {
+            if (mPadding != 0 && mPaddingLeft == 0) {
+                mTargetRect.left -= mPadding;
+            }
+            if (mPadding != 0 && mPaddingTop == 0) {
+                mTargetRect.top -= mPadding;
+            }
+            if (mPadding != 0 && mPaddingRight == 0) {
+                mTargetRect.right += mPadding;
+            }
+            if (mPadding != 0 && mPaddingBottom == 0) {
+                mTargetRect.bottom += mPadding;
+            }
+            if (mPaddingLeft != 0) {
+                mTargetRect.left -= mPaddingLeft;
+            }
+            if (mPaddingTop != 0) {
+                mTargetRect.top -= mPaddingTop;
+            }
+            if (mPaddingRight != 0) {
+                mTargetRect.right += mPaddingRight;
+            }
+            if (mPaddingBottom != 0) {
+                mTargetRect.bottom += mPaddingBottom;
+            }
+            ignoreRepadding = true;
+        }
+    }
+
+    @Override
+    protected LayoutParams generateDefaultLayoutParams() {
+        return new LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
+    }
+
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        final long drawingTime = getDrawingTime();
+        try {
+            View child;
+            for (int i = 0; i < getChildCount(); i++) {
+                child = getChildAt(i);
+                drawChild(canvas, child, drawingTime);
+            }
+        } catch (NullPointerException e) {
+
+        }
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+        if (mChangedHeight != 0) {
+            mTargetRect.offset(0, mChangedHeight);
+            mInitHeight = mInitHeight + mChangedHeight;
+            mChangedHeight = 0;
+        }
+        mEraserBitmap.eraseColor(Color.TRANSPARENT);
+        mEraserCanvas.drawColor(mFullingPaint.getColor());
+        if (!mOverlayTarget) {
+            switch (mStyle) {
+                case Component.ROUNDRECT:
+                    mEraserCanvas.drawRoundRect(mTargetRect, mCorner, mCorner, mEraser);
+                    break;
+                case Component.CIRCLE:
+                    mEraserCanvas.drawCircle(mTargetRect.centerX(), mTargetRect.centerY(),
+                            mTargetRect.width() / 2, mEraser);
+                    break;
+                default:
+                    mEraserCanvas.drawRoundRect(mTargetRect, mCorner, mCorner, mEraser);
+                    break;
+            }
+        }
+        canvas.drawBitmap(mEraserBitmap, mOverlayRect.left, mOverlayRect.top, null);
+    }
+
+    public void setTargetRect(Rect rect) {
+        mTargetRect.set(rect);
+    }
+
+    public void setFullingAlpha(int alpha) {
+        mFullingPaint.setAlpha(alpha);
+    }
+
+    public void setFullingColor(int color) {
+        mFullingPaint.setColor(color);
+    }
+
+    public void setHighTargetCorner(int corner) {
+        this.mCorner = corner;
+    }
+
+    public void setHighTargetGraphStyle(int style) {
+        this.mStyle = style;
+    }
+
+    public void setOverlayTarget(boolean b) {
+        mOverlayTarget = b;
+    }
+
+    public void setPadding(int padding) {
+        this.mPadding = padding;
+    }
+
+    public void setPaddingLeft(int paddingLeft) {
+        this.mPaddingLeft = paddingLeft;
+    }
+
+    public void setPaddingTop(int paddingTop) {
+        this.mPaddingTop = paddingTop;
+    }
+
+    public void setPaddingRight(int paddingRight) {
+        this.mPaddingRight = paddingRight;
+    }
+
+    public void setPaddingBottom(int paddingBottom) {
+        this.mPaddingBottom = paddingBottom;
+    }
+
+    static class LayoutParams extends ViewGroup.LayoutParams {
+
+        public static final int ANCHOR_LEFT = 0x01;
+        public static final int ANCHOR_TOP = 0x02;
+        public static final int ANCHOR_RIGHT = 0x03;
+        public static final int ANCHOR_BOTTOM = 0x04;
+        public static final int ANCHOR_OVER = 0x05;
+
+        public static final int PARENT_START = 0x10;
+        public static final int PARENT_CENTER = 0x20;
+        public static final int PARENT_END = 0x30;
+
+        public int targetAnchor = ANCHOR_BOTTOM;
+        public int targetParentPosition = PARENT_CENTER;
+        public int offsetX = 0;
+        public int offsetY = 0;
+
+        public LayoutParams(Context c, AttributeSet attrs) {
+            super(c, attrs);
+        }
+
+        public LayoutParams(int width, int height) {
+            super(width, height);
+        }
+
+        public LayoutParams(ViewGroup.LayoutParams source) {
+            super(source);
+        }
+    }
+}

+ 438 - 0
baselib/src/main/java/com/yingyangfly/baselib/widget/HighLight.java

@@ -0,0 +1,438 @@
+package com.yingyangfly.baselib.widget;
+
+import android.app.Activity;
+import android.graphics.RectF;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.RelativeLayout;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 设置View 的一些属性 on 2017/11/11.
+ * 封装一些枚举与接口
+ * 枚举主要是用于指定相关属性和提供回调。
+ */
+
+public class HighLight {
+
+    private static final int DEFAULT_WIDTH_BLUR = 15;   //默认模糊边界的大小
+    private static final int DEFAULT_RADIUS = 6;        //默认圆角度数
+
+    //虚线的排列方式,需要setIsNeedBorder(true)并且边框类型为HighLight.MyType.DASH_LINE,该样式才能生效
+    private float[] intervals = new float[]{4, 4};
+    private float borderWidth = 3;                      //边框宽度,单位:dp,默认3dp
+
+    private boolean shadow = false;                     //是否需要模糊化边界,默认不需要
+    private boolean intercept = true;                   //是否拦截点击事件
+    private boolean isNeedBorder = true;                //设置是否需要边框,默认需要
+    private int maskColor = 0x99000000;                 // 默认背景颜色
+    private int borderColor = maskColor;                //设置边框颜色,默认和背景颜色一样
+    private int blurSize = DEFAULT_WIDTH_BLUR;          //模糊边界大小,默认15
+    private int radius = DEFAULT_RADIUS;                //圆角大小,默认6
+
+    private List<ViewPosInfo> mViewRects;               //保存高亮View的信息的集合
+
+    private Activity mContext;                          // Activity对象
+    private View mAnchor;                               //需要增加高亮区域的根布局
+    private LightGuideView mLightGuideView;             //  表示高亮视图的对象
+    private ViewUtils viewUtils;                        //ViewUtils对象
+
+    private OnClickCallback clickCallback;              //点击事件的回调,要想点击有效果,必须设置intercept为TRUE
+
+    private MyType myType = MyType.DASH_LINE;//边框类型,默认虚线 HighLight.MyType.DASH_LINE
+
+
+    // 标识需要高亮的形状:圆形、矩形
+    public enum MyShape {
+        CIRCULAR, RECTANGULAR,
+    }
+
+    // 边框的样式,实线、虚线
+    public enum MyType {
+        FULL_LINE, DASH_LINE,//虚线
+    }
+
+    /**
+     * 封装了  需要高亮view的信息
+     * (静态类)
+     */
+    public static class ViewPosInfo {
+        public RectF rectF;
+        public View view;
+
+        public int layoutId = -1;
+        public MyShape myShape;
+
+        public MarginInfo marginInfo;
+        public OnPosCallback onPosCallback;
+    }
+
+
+    /**
+     * 封装上下左右的边距
+     */
+    public static class MarginInfo {
+        public float topMargin;
+        public float bottomMargin;
+        public float leftMargin;
+        public float rightMargin;
+    }
+
+    //构造方法
+    public HighLight(Activity activity) {
+        this.mContext = activity;
+        viewUtils = ViewUtils.getInsance(activity);
+        mViewRects = new ArrayList<>();                         //保存高亮View的信息的集合
+        mAnchor = activity.findViewById(android.R.id.content);  //需要增加高亮区域的根布局
+    }
+
+    /**
+     * 增加高亮的布局 1
+     *
+     * @param viewId        需要高亮的控件id
+     * @param decorLayoutId 布局文件资源id
+     * @param onPosCallback 回调,用于设置位置
+     */
+    public HighLight addHighLight(int viewId, int decorLayoutId, OnPosCallback onPosCallback) {
+        ViewGroup parent = (ViewGroup) mAnchor;
+        View view = parent.findViewById(viewId);
+        addHighLight(view, decorLayoutId, onPosCallback);
+        return this;
+    }
+
+    /**
+     * 增加高亮的布局 1-1
+     *
+     * @param view          高亮布局的视图
+     * @param decorLayoutId 布局文件资源id
+     * @param onPosCallback 回调,用于设置位置
+     */
+    private HighLight addHighLight(View view, int decorLayoutId, OnPosCallback onPosCallback) {
+        ViewGroup parent = (ViewGroup) mAnchor;
+        RectF rect = new RectF(viewUtils.getLocationInView(parent, view));
+
+        ViewPosInfo viewPosInfo = new ViewPosInfo();
+        viewPosInfo.layoutId = decorLayoutId;
+        viewPosInfo.rectF = rect;
+        viewPosInfo.view = view;
+
+        if (onPosCallback == null && decorLayoutId != -1) {
+            throw new IllegalArgumentException("参数错误:OnPosCallback == null && decorLayoutId != -1");
+        }
+        MarginInfo marginInfo = new MarginInfo();
+        onPosCallback.getPos(parent.getWidth() - rect.right, parent.getHeight() - rect.bottom,
+                rect, marginInfo);
+
+        viewPosInfo.marginInfo = marginInfo;
+        viewPosInfo.onPosCallback = onPosCallback;
+        mViewRects.add(viewPosInfo);
+        return this;
+    }
+
+
+    /**
+     * 增加高亮布局 2
+     *
+     * @param viewId        需要高亮的控件id
+     * @param decorLayoutId 布局文件资源id
+     * @param onPosCallback 回调,用于设置位置
+     * @param shape         指定高亮的形状,枚举类型
+     * @return
+     */
+    public HighLight addHighLight(int viewId, int decorLayoutId, OnPosCallback onPosCallback, MyShape shape) {
+        ViewGroup parent = (ViewGroup) mAnchor;
+        View view = parent.findViewById(viewId);
+        addHighLight(view, decorLayoutId, onPosCallback, shape);
+        return this;
+    }
+
+    /**
+     * 增加高亮布局 2-1
+     *
+     * @param view          高亮布局的视图
+     * @param decorLayoutId 布局文件资源id
+     * @param onPosCallback 回调,用于设置位置
+     * @param shape         指定高亮的形状,枚举类型
+     */
+    private HighLight addHighLight(View view, int decorLayoutId, OnPosCallback onPosCallback, MyShape shape) {
+        ViewGroup parent = (ViewGroup) mAnchor;
+        RectF rect = new RectF(viewUtils.getLocationInView(parent, view));
+
+        ViewPosInfo viewPosInfo = new ViewPosInfo();
+        viewPosInfo.layoutId = decorLayoutId;
+        viewPosInfo.rectF = rect;
+        viewPosInfo.view = view;
+
+        if (onPosCallback == null && decorLayoutId != -1) {
+            throw new IllegalArgumentException("参数错误:OnPosCallback == null && decorLayoutId != -1");
+        }
+
+        MarginInfo marginInfo = new MarginInfo();
+        onPosCallback.getPos(parent.getWidth() - rect.right, parent.getHeight() - rect.bottom,
+                rect, marginInfo);
+        viewPosInfo.marginInfo = marginInfo;
+        viewPosInfo.myShape = shape;
+        viewPosInfo.onPosCallback = onPosCallback;
+
+        mViewRects.add(viewPosInfo);
+        return this;
+    }
+
+    /**
+     * 增加高亮布局 3
+     *
+     * @param rect          高亮布局的位置
+     * @param decorLayoutId 布局文件资源id
+     * @param onPosCallback 回调,用于设置位置
+     */
+    public HighLight addHighLight(RectF rect, int decorLayoutId, OnPosCallback onPosCallback) {
+        ViewGroup parent = (ViewGroup) mAnchor;
+
+        ViewPosInfo viewPosInfo = new ViewPosInfo();
+        viewPosInfo.layoutId = decorLayoutId;
+        viewPosInfo.rectF = rect;
+
+        if (onPosCallback == null && decorLayoutId != -1) {
+            throw new IllegalArgumentException("参数错误:OnPosCallback == null && decorLayoutId != -1");
+        }
+
+        MarginInfo marginInfo = new MarginInfo();
+        onPosCallback.getPos(parent.getWidth() - rect.right, parent.getHeight() - rect.bottom, rect, marginInfo);
+        viewPosInfo.marginInfo = marginInfo;
+        viewPosInfo.onPosCallback = onPosCallback;
+        mViewRects.add(viewPosInfo);
+
+        return this;
+    }
+
+
+    /**
+     * 将一个布局文件加到根布局上,默认点击移除视图
+     */
+    public HighLight addLayout(int layoutId) {
+        viewUtils.addView(layoutId);
+        viewUtils.setOnViewClickListener(new ViewUtils.OnViewClickListener() {
+            @Override
+            public void onClick(View view) {
+                if (intercept && (HighLight.this.clickCallback != null)) {
+                    HighLight.this.clickCallback.onClick();
+                }
+            }
+        });
+        return this;
+    }
+
+
+    /**
+     * 更新位置信息
+     */
+    public void updateInfo() {
+        ViewGroup parent = (ViewGroup) mAnchor;
+        for (ViewPosInfo viewPosInfo : mViewRects) {
+            viewPosInfo.onPosCallback.getPos(parent.getWidth() - viewPosInfo.rectF.right,
+                    parent.getHeight() - viewPosInfo.rectF.bottom,
+                    viewPosInfo.rectF, viewPosInfo.marginInfo);
+        }
+    }
+
+
+    /**
+     * 显示含有高亮区域的页面
+     */
+    public void show() {
+        if (mLightGuideView != null)
+            return;
+
+        LightGuideView lightGuideView = new LightGuideView(mContext, this, maskColor, mViewRects);
+        // 设置是否需要模糊边界和模糊边界的大小
+        lightGuideView.setIsBlur(this.shadow);
+        if (this.shadow) {
+            lightGuideView.setBlurWidth(this.blurSize);
+        }
+        // 设置边框的相关配置
+        lightGuideView.setIsNeedBorder(this.isNeedBorder);
+        if (this.isNeedBorder) {
+            lightGuideView.setBoderColor(this.borderColor);
+            lightGuideView.setBoderWidth(this.borderWidth);
+            lightGuideView.setMyType(this.myType);
+            // 是虚线才需要设置虚线样式
+            if (this.myType == MyType.DASH_LINE)
+                lightGuideView.setIntervals(this.intervals);
+        }
+        lightGuideView.setRadius(this.radius);
+        lightGuideView.setMaskColor(maskColor);
+        if (mAnchor.getClass().getSimpleName().equals("FrameLayout")) {
+            ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
+            ((ViewGroup) mAnchor).addView(lightGuideView, ((ViewGroup) mAnchor).getChildCount(), params);
+        } else {
+            FrameLayout frameLayout = new FrameLayout(mContext);
+            ViewGroup parent = (ViewGroup) mAnchor.getParent();
+            parent.removeView(mAnchor);
+            parent.addView(frameLayout, mAnchor.getLayoutParams());
+            ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
+            frameLayout.addView(mAnchor, params);
+            frameLayout.addView(lightGuideView);
+        }
+
+        //是否拦截点击事件
+        if (intercept) {
+            lightGuideView.setOnClickListener(v -> {
+                remove();
+                if (clickCallback != null) {
+                    clickCallback.onClick();
+                }
+            });
+        }
+
+        mLightGuideView = lightGuideView;
+    }
+
+
+    /**
+     * 移除含有高亮区域的页面
+     */
+    public void remove() {
+        if (mLightGuideView == null)
+            return;
+        ViewGroup parent = (ViewGroup) mLightGuideView.getParent();
+        if (parent instanceof RelativeLayout || parent instanceof FrameLayout) {
+            parent.removeView(mLightGuideView);
+        } else {
+            parent.removeView(mLightGuideView);
+            View origin = parent.getChildAt(0);
+            ViewGroup graParent = (ViewGroup) parent.getParent();
+            graParent.removeView(parent);
+            graParent.addView(origin, parent.getLayoutParams());
+        }
+        mLightGuideView = null;
+    }
+
+
+    /*****************-------Set设置-------********************/
+    /**
+     * 1. 绑定根布局,需要高亮显示部分区域时需要第一个调用的方法
+     * (如果是在Activity中调用,可以不调用)
+     */
+    public HighLight anchor(View anchor) {
+        mAnchor = anchor;   //需要增加高亮区域的根布局
+        return this;
+    }
+
+    /**
+     * 2. 设置是否需要拦截点击事件
+     */
+    public HighLight setIntercept(boolean intercept) {
+        this.intercept = intercept;
+        return this;
+    }
+
+    /**
+     * 3. 设置是否需要模糊化边框,默认不需要
+     */
+    public HighLight setShadow(boolean shadow) {
+        this.shadow = shadow;
+        return this;
+    }
+
+    /**
+     * 4. 设置背景颜色,默认 99000000
+     */
+    public HighLight setMaskColor(int maskColor) {
+        this.maskColor = maskColor;
+        return this;
+    }
+
+
+    /**
+     * 5. 设置边框类型,需要setIsNeedBorder(true),该方法才能生效
+     */
+    public HighLight setMyBroderType(MyType myType) {
+        this.myType = myType;
+        return this;
+    }
+
+    /**
+     * 6. 设置边框宽度,需要setIsNeedBorder(true),该方法才能生效;不需要转换单位,默认dp
+     */
+    public HighLight setBorderWidth(float borderWidth) {
+        this.borderWidth = borderWidth;
+        return this;
+    }
+
+    /**
+     * 7. 设置虚线边框的样式,需要setIsNeedBorder(true)并且边框类型为HighLight.MyType.DASH_LINE,该方法才能生效;不需要转换单位,默认dp
+     * <p>
+     * 必须是偶数长度,且>=2,指定了多少长度的实线之后再画多少长度的空白.
+     * 如在 new float[] { 1, 2, 4, 8}中,表示先绘制长度1的实线,再绘制长度2的空白,
+     * 再绘制长度4的实线,再绘制长度8的空白,依次重复
+     */
+    public HighLight setIntervals(float[] intervals) {
+        int length = intervals.length;
+        if ((length >= 2) && (length % 2 == 0)) {
+            this.intervals = intervals;
+        } else {
+            throw new IllegalArgumentException("元素的个数必须大于2并且是偶数");
+        }
+        return this;
+    }
+
+    /**
+     * 8. 设置是否需要边框
+     */
+    public HighLight setIsNeedBorder(boolean isNeedBorder) {
+        this.isNeedBorder = isNeedBorder;
+        return this;
+    }
+
+    /**
+     * 9. 设置模糊边界的宽度,需要setIsBlur(true),该方法才能生效
+     */
+    public HighLight setBlurSize(int blurSize) {
+        this.blurSize = blurSize;
+        return this;
+    }
+
+    /**
+     * 10. 设置圆角度数
+     */
+    public HighLight setRadius(int radius) {
+        this.radius = radius;
+        return this;
+    }
+
+    /**
+     * 11. 设置边框颜色,需要setIsNeedBorder(true),该方法才能生效
+     */
+    public HighLight setBorderColor(int borderColor) {
+        this.borderColor = borderColor;
+        return this;
+    }
+
+    /**
+     * 12. 一个场景可能有多个步骤的高亮。一个步骤完成之后再进行下一个步骤的高亮,
+     * 添加点击事件,将每次点击传给应用逻辑
+     *
+     * @param clickCallback 设置整个引导的点击事件
+     */
+    public HighLight setOnClickCallback(OnClickCallback clickCallback) {
+        this.clickCallback = clickCallback;
+        return this;
+    }
+
+
+    // 增加高亮View的回调
+    public interface OnPosCallback {
+        //封装了高亮View的位置信息
+        void getPos(float rightMargin, float bottomMargin, RectF rectF, MarginInfo marginInfo);
+    }
+
+    // 点击回调接口
+    public interface OnClickCallback {
+        //点击回调方法,要想点击有效果,必须设置intercept为TRUE
+        void onClick();
+    }
+
+}

+ 372 - 0
baselib/src/main/java/com/yingyangfly/baselib/widget/LightGuideView.java

@@ -0,0 +1,372 @@
+package com.yingyangfly.baselib.widget;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BlurMaskFilter;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.DashPathEffect;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import java.util.List;
+
+/**
+ * 核心View on 2017/11/11.
+ * <p>
+ * 1. 把自定义的xml引导说明布局文件 通过 inflate()变为View对象,然后添加到FrameLayout上;
+ * 2. 重写onDraw()方法,绘制半透明背景,然后根据设置的属性在需要高亮的控件周围绘制全透明的
+ * 指定形状达到高亮效果。
+ */
+
+public class LightGuideView extends FrameLayout {
+
+    //用于实现新绘制的像素与Canvas上对应位置已有的像素按照混合规则进行颜色混合
+    private static final PorterDuffXfermode MODE_DST_OUT = new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT);
+    private static final int DEFAULT_WIDTH_BLUR = 15;   //默认模糊边界的大小
+    private static final int DEFAULT_RADIUS = 6;        //默认圆角度数
+
+    private Context context;
+    private Paint mPaint;                               //绘制高亮区域的画笔
+    private Bitmap mMaskBitmap;                         //用于标识高亮区的图片
+
+    //虚线的排列方式,需要setIsNeedBorder(true)并且边框类型为HighLight.MyType.DASH_LINE,该样式才能生效
+    private float[] intervals;
+    private boolean isNeedBorder = true;                //是否需要边框,默认需要
+    private boolean isBlur = false;                     //是否需要模糊边界,默认不需要
+    private int maskColor = 0x99000000;                 //背景颜色
+    private int boderColor = maskColor;                 //边框颜色,默认和背景颜色一样
+    private float boderWidth = 3;                       //边框宽度,单位:dp,默认3dp
+    private int blurSize = DEFAULT_WIDTH_BLUR;          //模糊边界大小,默认15
+    private int radius = DEFAULT_RADIUS;                //圆角大小,默认6
+    private int phase = 1;                              //偏移量,直接使用1即可
+
+    private List<HighLight.ViewPosInfo> mViewRects;                 //用于保存高亮View的集合
+    private HighLight mHighLight;                                   //HighLight对象
+    private HighLight.MyType myType = HighLight.MyType.DASH_LINE;   //边框类型,默认虚线
+    private LayoutInflater mInflater;                               //视图填充器(打气筒)
+
+    public LightGuideView(Context context, HighLight highLight, int maskColor, List<HighLight.ViewPosInfo> viewRects) {
+        super(context);
+        this.context = context;
+        this.maskColor = maskColor;
+        mHighLight = highLight;
+        mInflater = LayoutInflater.from(context);
+        mViewRects = viewRects;
+
+        setWillNotDraw(false);
+        init();
+    }
+
+    /**
+     * 初始化一些配置参数
+     */
+    private void init() {
+        mPaint = new Paint();
+        mPaint.setDither(true);             //设置防抖动
+        mPaint.setAntiAlias(true);          //抗锯齿
+        mPaint.setStyle(Paint.Style.FILL);  //只绘制图形内容 (填充),STROKE 只绘制图形轮廓(描边)
+        mPaint.setStrokeWidth(5);
+
+        addViewForTip();
+        //初始化虚线的样式
+        intervals = new float[]{dip2px(4), dip2px(4)};
+    }
+
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int width = MeasureSpec.getSize(widthMeasureSpec);
+        int height = MeasureSpec.getSize(heightMeasureSpec);
+
+        //1.精确模式(MeasureSpec.EXACTLY):
+        //      尺寸的值是多少,那么这个组件的长或宽就是多少;
+        //      使用measureSpec中size的值作为宽高的精确值。
+        //2.最大模式(MeasureSpec.AT_MOST):
+        //      这个也就是父组件,能够给出的最大的空间,当前组件的长或宽最大只能为这么大,当然也可以比这个小。
+        //      使用measureSpec中size的值作为最大值,采用不超过这个值的最大允许值。
+        //3.未指定模式(MeasureSpec.UNSPECIFIED):当前组件,可以随便用空间,不受限制。
+        measureChildren(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
+                MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
+
+        setMeasuredDimension(width, height);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        canvas.drawBitmap(mMaskBitmap, 0, 0, null);
+
+        super.onDraw(canvas);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        if (changed) {
+            buildMask();       // 绘制高亮区域
+            updateTipPos();     //更新高亮位置
+        }
+    }
+
+
+    /**
+     * 将需要高亮的view增加到帧布局上方
+     */
+    private void addViewForTip() {
+        for (HighLight.ViewPosInfo viewPosInfo : mViewRects) {
+            View view = mInflater.inflate(viewPosInfo.layoutId, this, false);
+
+            LayoutParams params = buildTipLayoutParams(view, viewPosInfo);
+            if (params == null)
+                continue;
+            params.leftMargin = (int) viewPosInfo.marginInfo.leftMargin;
+            params.topMargin = (int) viewPosInfo.marginInfo.topMargin;
+            params.rightMargin = (int) viewPosInfo.marginInfo.rightMargin;
+            params.bottomMargin = (int) viewPosInfo.marginInfo.bottomMargin;
+            Log.e("Tag-LightView:", String.valueOf(viewPosInfo.marginInfo.leftMargin));
+
+            if (params.rightMargin != 0) {
+                params.gravity = Gravity.RIGHT;
+            } else {
+                params.gravity = Gravity.LEFT;
+            }
+            if (params.bottomMargin != 0) {
+                params.gravity |= Gravity.BOTTOM;   //|=符号,a|=b的意思就是把a和b按位或然后赋值给a
+            } else {
+                params.gravity |= Gravity.TOP;
+            }
+
+            addView(view, params);
+        }
+    }
+
+
+    /**
+     * 更新高亮位置
+     */
+    private void updateTipPos() {
+        for (int i = 0, n = getChildCount(); i < n; i++) {
+            View view = getChildAt(i);
+            HighLight.ViewPosInfo viewPosInfo = mViewRects.get(i);
+
+            LayoutParams params = buildTipLayoutParams(view, viewPosInfo);
+            if (params == null)
+                continue;
+            view.setLayoutParams(params);
+        }
+    }
+
+    //设置高亮区域参数
+    private LayoutParams buildTipLayoutParams(View view, HighLight.ViewPosInfo viewPosInfo) {
+        LayoutParams params = (LayoutParams) view.getLayoutParams();
+        if (params.leftMargin == (int) viewPosInfo.marginInfo.leftMargin &&
+                params.topMargin == (int) viewPosInfo.marginInfo.topMargin &&
+                params.rightMargin == (int) viewPosInfo.marginInfo.rightMargin &&
+                params.bottomMargin == (int) viewPosInfo.marginInfo.bottomMargin) {
+            return null;
+        }
+        params.leftMargin = (int) viewPosInfo.marginInfo.leftMargin;
+        params.topMargin = (int) viewPosInfo.marginInfo.topMargin;
+        params.rightMargin = (int) viewPosInfo.marginInfo.rightMargin;
+        params.bottomMargin = (int) viewPosInfo.marginInfo.bottomMargin;
+
+        if (params.rightMargin != 0) {
+            params.gravity = Gravity.RIGHT;
+        } else {
+            params.gravity = Gravity.LEFT;
+        }
+        if (params.bottomMargin != 0) {
+            params.gravity |= Gravity.BOTTOM;   //|=符号,a|=b的意思就是把a和b按位或然后赋值给a
+        } else {
+            params.gravity |= Gravity.TOP;
+        }
+        return params;
+    }
+
+    /**
+     * 绘制高亮区域
+     */
+    private void buildMask() {
+        mMaskBitmap = Bitmap.createBitmap(getMeasuredWidth(), getMeasuredHeight(), Bitmap.Config.ARGB_8888);
+        Canvas canvas = new Canvas(mMaskBitmap);
+        canvas.drawColor(maskColor);
+
+        mPaint.setXfermode(MODE_DST_OUT);//图像混合模式
+        mPaint.setColor(Color.parseColor("#00000000"));//透明
+
+        if (isBlur) {
+            mPaint.setMaskFilter(new BlurMaskFilter(this.blurSize, BlurMaskFilter.Blur.SOLID));
+        }
+
+        mHighLight.updateInfo();
+        for (HighLight.ViewPosInfo viewPosInfo : mViewRects) {
+            if (viewPosInfo.myShape != null) {
+                switch (viewPosInfo.myShape) {
+                    case CIRCULAR:
+                        float width = viewPosInfo.rectF.width();
+                        float height = viewPosInfo.rectF.height();
+                        float circle_center1;
+                        float circle_center2;
+                        double radius = Math.sqrt(Math.pow(width / 2, 2) + Math.pow(height / 2, 2));//勾股定理
+                        circle_center1 = width / 2;
+                        circle_center2 = height / 2;
+                        canvas.drawCircle(viewPosInfo.rectF.right - circle_center1, viewPosInfo.rectF.bottom - circle_center2,
+                                (int) radius, mPaint);
+                        if (isNeedBorder) {
+                            drawCircleBorder(canvas, viewPosInfo, circle_center1, circle_center2, (int) radius);
+                        }
+                        break;
+                    case RECTANGULAR:
+                        canvas.drawRoundRect(viewPosInfo.rectF, this.radius, this.radius, mPaint);
+                        if (isNeedBorder) {
+                            drawRectBorder(canvas, viewPosInfo);
+                        }
+                        break;
+                }
+            } else {
+                canvas.drawRoundRect(viewPosInfo.rectF, this.radius, this.radius, mPaint);
+                if (isNeedBorder) {
+                    drawRectBorder(canvas, viewPosInfo);
+                }
+            }
+        }
+    }
+
+
+    /**
+     * 绘制圆形边框
+     */
+    private void drawCircleBorder(Canvas canvas, HighLight.ViewPosInfo viewPosInfo, float circle_center1, float circle_center2, int radius) {
+        Paint paint = new Paint();
+        paint.reset();
+
+        if (this.myType == HighLight.MyType.DASH_LINE) {
+//            DashPathEffect是PathEffect类的一个子类,可以使paint画出类似虚线的样子,并且可以任意指定虚实的排列方式。
+//            float数组,必须是偶数长度,且>=2,指定了多少长度的实线之后再画多少长度的空白。
+            DashPathEffect pathEffect = new DashPathEffect(intervals, this.phase);
+            paint.setPathEffect(pathEffect);
+        }
+        paint.setStyle(Paint.Style.STROKE);
+        paint.setStrokeWidth(dip2px(boderWidth));
+        paint.setAntiAlias(true);
+        paint.setColor(boderColor);
+        Path path = new Path();
+        path.addCircle(viewPosInfo.rectF.right - circle_center1, viewPosInfo.rectF.bottom - circle_center2,
+                radius, Path.Direction.CW);//Path.Direction.CW--顺时针,Path.Direction.CCW--逆时针
+        canvas.drawPath(path, paint);
+    }
+
+
+    /**
+     * 绘制矩形边框
+     */
+    private void drawRectBorder(Canvas canvas, HighLight.ViewPosInfo viewPosInfo) {
+        Paint paint = new Paint();
+        paint.reset();
+        if (this.myType == HighLight.MyType.DASH_LINE) {
+            DashPathEffect pathEffect = new DashPathEffect(intervals, this.phase);
+            paint.setPathEffect(pathEffect);
+        }
+        paint.setStyle(Paint.Style.STROKE);
+        paint.setStrokeWidth(dip2px(boderWidth));
+        paint.setAntiAlias(true);
+        paint.setColor(boderColor);
+
+        Path path = new Path();
+        path.addRect(viewPosInfo.rectF, Path.Direction.CW);
+        canvas.drawPath(path, paint);
+    }
+
+    /*******************-------Set设置------**********************/
+    /**
+     * 设置是否需要边框
+     */
+    public void setIsNeedBorder(boolean isNeedBorder) {
+        this.isNeedBorder = isNeedBorder;
+    }
+
+    /**
+     * 设置边框颜色,需要setIsNeedBorder(true),该方法才能生效
+     */
+    public void setBoderColor(int boderColor) {
+        this.boderColor = boderColor;
+    }
+
+    /**
+     * 设置边框宽度,需要setIsNeedBorder(true),该方法才能生效;不需要转换单位,默认dp
+     */
+    public void setBoderWidth(float boderWidth) {
+        this.boderWidth = boderWidth;
+    }
+
+    /**
+     * 是否需要模糊边界
+     */
+    public void setIsBlur(boolean isBlur) {
+        this.isBlur = isBlur;
+    }
+
+    /**
+     * 设置模糊边界的宽度,需要setIsBlur(true),该方法才能生效
+     */
+    public void setBlurWidth(int blurSize) {
+        this.blurSize = blurSize;
+        if (isBlur) {
+            mPaint.setMaskFilter(new BlurMaskFilter(this.blurSize, BlurMaskFilter.Blur.SOLID));
+        }
+    }
+
+    /**
+     * 设置边框类型,需要setIsNeedBorder(true),该方法才能生效
+     */
+    public void setMyType(HighLight.MyType myType) {
+        this.myType = myType;
+    }
+
+    /**
+     * 设置虚线边框的样式,需要setIsNeedBorder(true)并且边框类型为HighLight.MyType.DASH_LINE,该方法才能生效;
+     * 不需要转换单位,默认dp
+     * <p>
+     * 必须是偶数长度,且>=2,指定了多少长度的实线之后再画多少长度的空白.
+     * 如在 new float[] { 1, 2, 4, 8}中,表示先绘制长度1的实线,再绘制长度2的空白,再绘制长度4的实线,再绘制长度8的空白,依次重复
+     */
+    public void setIntervals(float[] intervals) {
+        int length = intervals.length;
+        if (length >= 2 && (length % 2 == 0)) {
+            this.intervals = new float[length];
+            for (int i = 0; i < length; i++) {
+                this.intervals[i] = dip2px(intervals[i]);
+            }
+        } else {
+            throw new IllegalArgumentException("元素的个数必须大于2并且是偶数");
+        }
+    }
+
+    /**
+     * 设置圆角度数
+     */
+    public void setRadius(int radius) {
+        this.radius = radius;
+    }
+
+    /**
+     * 设置背景颜色
+     */
+    public void setMaskColor(int maskColor) {
+        this.maskColor = maskColor;
+    }
+
+    private int dip2px(float dpValue) {
+        float scale = context.getResources().getDisplayMetrics().density;
+        return (int) (dpValue * scale + 0.5f);
+    }
+
+
+}

+ 119 - 0
baselib/src/main/java/com/yingyangfly/baselib/widget/ViewUtils.java

@@ -0,0 +1,119 @@
+package com.yingyangfly.baselib.widget;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Rect;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+/**
+ * 操作View的工具类 on 2017/11/11.
+ * http://blog.csdn.net/itrenj/article/details/53890118
+ */
+
+public class ViewUtils {
+    private static final String FRAGMENT_CON = "NoSaveStateFrameLayout";
+
+    private static ViewUtils viewUtils;
+    private static Activity mActivity;
+    private OnViewClickListener clickListener;
+
+    public static ViewUtils getInsance(Activity activity) {
+        mActivity = activity;
+        if (viewUtils == null) {
+            viewUtils = new ViewUtils();
+        }
+        return viewUtils;
+    }
+
+
+    /**
+     * 在整个窗体上面增加一层布局
+     * @param layoutId 布局id
+     */
+    public void addView(int layoutId) {
+        final View view = View.inflate(mActivity, layoutId, null);
+        FrameLayout frameLayout = (FrameLayout) getRootView();
+        frameLayout.addView(view);
+
+        //设置整个布局的单击监听
+        view.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                removeView(view);
+                if (clickListener != null) {
+                    clickListener.onClick(view);
+                }
+            }
+        });
+    }
+
+
+    /**
+     * 移除View
+     * @param view 需要移除的视图
+     */
+    public void removeView(View view) {
+        FrameLayout frameLayout = (FrameLayout) getRootView();
+        frameLayout.removeView(view);
+    }
+
+    //@return 返回最顶层视图
+    public ViewGroup getDeCorView() {
+        return (ViewGroup) mActivity.getWindow().getDecorView();
+    }
+
+    // @return 返回内容区域根视图
+    private ViewGroup getRootView() {
+        return (ViewGroup) mActivity.findViewById(android.R.id.content);
+    }
+
+    /**
+     * 获取子View 在 父View中的位置
+     */
+    public Rect getLocationInView(View parent, View child) {
+        if (child == null || parent == null) {
+            throw new IllegalArgumentException("parent and child can not be null");
+        }
+        View decorView = null;
+        Context context = child.getContext();
+        if (context instanceof Activity) {
+            decorView = ((Activity) context).getWindow().getDecorView();
+        }
+        Rect result = new Rect();
+        Rect tmpRect = new Rect();
+
+        View tmp = child;
+        if (child == parent) {
+            child.getHitRect(result);
+            return result;
+        }
+
+        while (tmp != decorView && tmp != parent) {
+            tmp.getHitRect(tmpRect);
+            if (!tmp.getClass().equals(FRAGMENT_CON)) {
+                result.left += tmpRect.left;
+                result.top += tmpRect.top;
+            }
+            tmp = (View) tmp.getParent();
+        }
+
+        result.right = result.left + child.getMeasuredWidth();
+        result.bottom = result.top + child.getMeasuredHeight();
+        return result;
+    }
+
+    public void setOnViewClickListener(OnViewClickListener clickListener) {
+        this.clickListener = clickListener;
+    }
+
+    /**
+     * 单击视图监听,用于多个引导页面时连续调用
+     */
+    public interface OnViewClickListener{
+        //单击监听回调
+        void onClick(View view);
+    }
+
+}

+ 31 - 1
home/src/main/java/com/yingyangfly/home/activity/HomeActivity.kt

@@ -11,6 +11,8 @@ import com.yingyangfly.baselib.dialog.TipsDialog
 import com.yingyangfly.baselib.ext.setOnSingleClickListener
 import com.yingyangfly.baselib.ext.show
 import com.yingyangfly.baselib.ext.toast
+import com.yingyangfly.baselib.guideview.Guide
+import com.yingyangfly.baselib.guideview.GuideBuilder
 import com.yingyangfly.baselib.mvvm.BaseMVVMActivity
 import com.yingyangfly.baselib.net.BaseObserver
 import com.yingyangfly.baselib.net.MyRxScheduler
@@ -18,6 +20,7 @@ import com.yingyangfly.baselib.router.RouterUrlCommon
 import com.yingyangfly.baselib.utils.JumpUtil
 import com.yingyangfly.baselib.utils.User
 import com.yingyangfly.home.adapter.GameAdapter
+import com.yingyangfly.home.component.SimpleComponent
 import com.yingyangfly.home.entity.HomePageMsgBean
 import com.yingyangfly.home.entity.Record
 import com.yingyangfly.home.net.XHomeServiceFactory
@@ -146,7 +149,33 @@ class HomeActivity : BaseMVVMActivity<ActivityHomeBinding, HomeViewModel>() {
     }
 
     override fun initData() {
+        if (TextUtils.equals("", User.getFirstLogin())) {
 
+        }
+        binding.tvPlayPorpoise.post {
+            addHightView()
+        }
+    }
+
+    /**
+     * 首次登陆添加引导窗
+     */
+    private fun addHightView() {
+        val builder = GuideBuilder()
+        builder.setTargetView(binding.tvPlayPorpoise)
+            .setAlpha(150)
+            .setHighTargetCorner(20)
+            .setHighTargetPadding(10)
+        builder.setOnVisibilityChangedListener(object : GuideBuilder.OnVisibilityChangedListener {
+            override fun onShown() {
+            }
+
+            override fun onDismiss() {
+            }
+        })
+        builder.addComponent(SimpleComponent())
+        val guide: Guide = builder.createGuide()
+        guide.show(this)
     }
 
     override fun onResume() {
@@ -160,7 +189,8 @@ class HomeActivity : BaseMVVMActivity<ActivityHomeBinding, HomeViewModel>() {
         if (TextUtils.equals("0", User.getFirstLogin())) {
             //第一次登录弹窗欢迎
             User.saveFirstLogin("1")
-            val taskDesn = "欢迎" + User.getName() + "使用未来蓝豚康复平台!为您提供专业的认知康复支持和训练。帮助您提升认知能力,重建自信。小豚期待与您一同启程!"
+            val taskDesn =
+                "欢迎" + User.getName() + "使用未来蓝豚康复平台!为您提供专业的认知康复支持和训练。帮助您提升认知能力,重建自信。小豚期待与您一同启程!"
             showTaskDialog(taskDesn, "")
         } else {
             //获取任务状态弹窗

+ 37 - 0
home/src/main/java/com/yingyangfly/home/component/SimpleComponent.java

@@ -0,0 +1,37 @@
+package com.yingyangfly.home.component;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.LinearLayout;
+
+import com.yingyang.home.R;
+import com.yingyangfly.baselib.guideview.Component;
+
+public class SimpleComponent implements Component {
+
+    @Override
+    public View getView(LayoutInflater inflater) {
+        LinearLayout ll = (LinearLayout) inflater.inflate(R.layout.layout_play_with_blue_porpoise, null);
+        return ll;
+    }
+
+    @Override
+    public int getAnchor() {
+        return Component.ANCHOR_LEFT;
+    }
+
+    @Override
+    public int getFitPosition() {
+        return Component.FIT_END;
+    }
+
+    @Override
+    public int getXOffset() {
+        return 0;
+    }
+
+    @Override
+    public int getYOffset() {
+        return 10;
+    }
+}

+ 2 - 1
home/src/main/res/layout/activity_home.xml

@@ -19,6 +19,7 @@
 
 
     <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/homeLayout"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:background="@drawable/bg_train">
@@ -48,7 +49,7 @@
                     android:layout_marginEnd="@dimen/divider_31px"
                     android:background="@drawable/bg_home_title"
                     android:gravity="center"
-                    android:text='@{User.INSTANCE.name+",欢迎使用未来蓝豚"}'
+                    android:text='@{User.INSTANCE.name+",欢迎使用未来蓝豚"}'
                     android:textColor="@color/color_FF4A76FF"
                     android:textSize="@dimen/divider_24px"
                     app:layout_constraintBottom_toBottomOf="parent"

+ 14 - 0
home/src/main/res/layout/layout_play_with_blue_porpoise.xml

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    tools:ignore="ResourceName">
+
+    <androidx.appcompat.widget.AppCompatImageView
+        android:layout_width="525px"
+        android:layout_height="253px"
+        android:scaleType="centerInside"
+        android:background="@mipmap/icon_play_with_blue_porpoise" />
+
+</LinearLayout>

BIN
home/src/main/res/mipmap-xxhdpi/icon_play_with_blue_porpoise.png