() {
116 | @Override
117 | public Configuration createFromParcel(Parcel source) {
118 | Configuration conf = new Configuration();
119 | conf.mAlpha = source.readInt();
120 | conf.mFullingViewId = source.readInt();
121 | conf.mTargetViewId = source.readInt();
122 | conf.mFullingColorId = source.readInt();
123 | conf.mCorner = source.readInt();
124 | conf.mPadding = source.readInt();
125 | conf.mPaddingLeft = source.readInt();
126 | conf.mPaddingTop = source.readInt();
127 | conf.mPaddingRight = source.readInt();
128 | conf.mPaddingBottom = source.readInt();
129 | conf.mGraphStyle = source.readInt();
130 | conf.mAutoDismiss = source.readByte() == 1;
131 | conf.mOverlayTarget = source.readByte() == 1;
132 | return conf;
133 | }
134 |
135 | @Override
136 | public Configuration[] newArray(int size) {
137 | return new Configuration[size];
138 | }
139 | };
140 | }
141 |
--------------------------------------------------------------------------------
/guideview/src/main/java/com/binioter/guideview/DimenUtil.java:
--------------------------------------------------------------------------------
1 | package com.binioter.guideview;
2 |
3 | import android.content.Context;
4 |
5 | /**
6 | * Created by binIoter
7 | */
8 |
9 | public class DimenUtil {
10 |
11 | /** sp转换成px */
12 | public static int sp2px(Context context, float spValue) {
13 | float fontScale = context.getApplicationContext().getResources().getDisplayMetrics().scaledDensity;
14 | return (int) (spValue * fontScale + 0.5f);
15 | }
16 |
17 | /** px转换成sp */
18 | public static int px2sp(Context context, float pxValue) {
19 | float fontScale = context.getApplicationContext().getResources().getDisplayMetrics().density;
20 | return (int) (pxValue / fontScale + 0.5f);
21 | }
22 |
23 | /** dip转换成px */
24 | public static int dp2px(Context context, float dipValue) {
25 | float scale = context.getApplicationContext().getResources().getDisplayMetrics().scaledDensity;
26 | return (int) (dipValue * scale + 0.5f);
27 | }
28 |
29 | /** px转换成dip */
30 | public static int px2dp(Context context, float pxValue) {
31 | float scale = context.getApplicationContext().getResources().getDisplayMetrics().scaledDensity;
32 | return (int) (pxValue / scale + 0.5f);
33 | }
34 |
35 | }
--------------------------------------------------------------------------------
/guideview/src/main/java/com/binioter/guideview/Guide.java:
--------------------------------------------------------------------------------
1 | package com.binioter.guideview;
2 |
3 | import android.app.Activity;
4 | import android.content.Context;
5 | import android.view.KeyEvent;
6 | import android.view.MotionEvent;
7 | import android.view.View;
8 | import android.view.ViewGroup;
9 | import android.view.animation.Animation;
10 | import android.view.animation.AnimationUtils;
11 |
12 | /**
13 | * 遮罩系统的封装
14 | * * 外部需要调用{@link GuideBuilder}来创建该实例,实例创建后调用
15 | * * {@link #show(Activity)} 控制显示; 调用 {@link #dismiss()}让遮罩系统消失。
16 | *
17 | * Created by binIoter
18 | */
19 |
20 | public class Guide implements View.OnKeyListener, View.OnTouchListener {
21 |
22 | Guide() {
23 | }
24 |
25 | /**
26 | * 滑动临界值
27 | */
28 | private static final int SLIDE_THRESHOLD = 30;
29 | private Configuration mConfiguration;
30 | private MaskView mMaskView;
31 | private Component[] mComponents;
32 | // 根据locInwindow定位后,是否需要判断loc值非0
33 | private boolean mShouldCheckLocInWindow = true;
34 | private GuideBuilder.OnVisibilityChangedListener mOnVisibilityChangedListener;
35 | private GuideBuilder.OnSlideListener mOnSlideListener;
36 |
37 | void setConfiguration(Configuration configuration) {
38 | mConfiguration = configuration;
39 | }
40 |
41 | void setComponents(Component[] components) {
42 | mComponents = components;
43 | }
44 |
45 | void setCallback(GuideBuilder.OnVisibilityChangedListener listener) {
46 | this.mOnVisibilityChangedListener = listener;
47 | }
48 |
49 | public void setOnSlideListener(GuideBuilder.OnSlideListener onSlideListener) {
50 | this.mOnSlideListener = onSlideListener;
51 | }
52 |
53 | /**
54 | * 显示遮罩
55 | *
56 | * @param activity 目标Activity
57 | */
58 | public void show(Activity activity) {
59 | show(activity, null);
60 | }
61 |
62 | /**
63 | * 显示遮罩
64 | *
65 | * @param activity 目标Activity
66 | * @param overlay 遮罩层view
67 | */
68 | public void show(Activity activity, ViewGroup overlay) {
69 | mMaskView = onCreateView(activity, overlay);
70 | if (overlay == null) {
71 | overlay = (ViewGroup) activity.getWindow().getDecorView();
72 | }
73 | if (mMaskView.getParent() == null && mConfiguration.mTargetView != null) {
74 | overlay.addView(mMaskView);
75 | if (mConfiguration.mEnterAnimationId != -1) {
76 | Animation anim = AnimationUtils.loadAnimation(activity, mConfiguration.mEnterAnimationId);
77 | assert anim != null;
78 | anim.setAnimationListener(new Animation.AnimationListener() {
79 | @Override
80 | public void onAnimationStart(Animation animation) {
81 |
82 | }
83 |
84 | @Override
85 | public void onAnimationEnd(Animation animation) {
86 | if (mOnVisibilityChangedListener != null) {
87 | mOnVisibilityChangedListener.onShown();
88 | }
89 | }
90 |
91 | @Override
92 | public void onAnimationRepeat(Animation animation) {
93 |
94 | }
95 | });
96 | mMaskView.startAnimation(anim);
97 | } else {
98 | if (mOnVisibilityChangedListener != null) {
99 | mOnVisibilityChangedListener.onShown();
100 | }
101 | }
102 | }
103 | }
104 |
105 | public void clear() {
106 | if (mMaskView == null) {
107 | return;
108 | }
109 | final ViewGroup vp = (ViewGroup) mMaskView.getParent();
110 | if (vp == null) {
111 | return;
112 | }
113 | vp.removeView(mMaskView);
114 | onDestroy();
115 | }
116 |
117 | /**
118 | * 隐藏该遮罩并回收资源相关
119 | */
120 | public void dismiss() {
121 | if (mMaskView == null) {
122 | return;
123 | }
124 | final ViewGroup vp = (ViewGroup) mMaskView.getParent();
125 | if (vp == null) {
126 | return;
127 | }
128 | if (mConfiguration.mExitAnimationId != -1) {
129 | // mMaskView may leak if context is null
130 | Context context = mMaskView.getContext();
131 | assert context != null;
132 |
133 | Animation anim = AnimationUtils.loadAnimation(context, mConfiguration.mExitAnimationId);
134 | assert anim != null;
135 | anim.setAnimationListener(new Animation.AnimationListener() {
136 | @Override
137 | public void onAnimationStart(Animation animation) {
138 |
139 | }
140 |
141 | @Override
142 | public void onAnimationEnd(Animation animation) {
143 | vp.removeView(mMaskView);
144 | if (mOnVisibilityChangedListener != null) {
145 | mOnVisibilityChangedListener.onDismiss();
146 | }
147 | onDestroy();
148 | }
149 |
150 | @Override
151 | public void onAnimationRepeat(Animation animation) {
152 |
153 | }
154 | });
155 | mMaskView.startAnimation(anim);
156 | } else {
157 | vp.removeView(mMaskView);
158 | if (mOnVisibilityChangedListener != null) {
159 | mOnVisibilityChangedListener.onDismiss();
160 | }
161 | onDestroy();
162 | }
163 | }
164 |
165 | /**
166 | * 根据locInwindow定位后,是否需要判断loc值非0
167 | */
168 | public void setShouldCheckLocInWindow(boolean set) {
169 | mShouldCheckLocInWindow = set;
170 | }
171 |
172 | private MaskView onCreateView(Activity activity, ViewGroup overlay) {
173 | if (overlay == null) {
174 | overlay = (ViewGroup) activity.getWindow().getDecorView();
175 | }
176 | MaskView maskView = new MaskView(activity);
177 | maskView.setFullingColor(activity.getResources().getColor(mConfiguration.mFullingColorId));
178 | maskView.setFullingAlpha(mConfiguration.mAlpha);
179 | maskView.setHighTargetCorner(mConfiguration.mCorner);
180 | maskView.setPadding(mConfiguration.mPadding);
181 | maskView.setPaddingLeft(mConfiguration.mPaddingLeft);
182 | maskView.setPaddingTop(mConfiguration.mPaddingTop);
183 | maskView.setPaddingRight(mConfiguration.mPaddingRight);
184 | maskView.setPaddingBottom(mConfiguration.mPaddingBottom);
185 | maskView.setHighTargetGraphStyle(mConfiguration.mGraphStyle);
186 | maskView.setOverlayTarget(mConfiguration.mOverlayTarget);
187 | maskView.setOnKeyListener(this);
188 |
189 | // For removing the height of status bar we need the root content view's
190 | // location on screen
191 | int parentX = 0;
192 | int parentY = 0;
193 | if (overlay != null) {
194 | int[] loc = new int[2];
195 | overlay.getLocationInWindow(loc);
196 | parentX = loc[0];
197 | parentY = loc[1];
198 | }
199 |
200 | if (mConfiguration.mTargetView != null) {
201 | maskView.setTargetRect(Common.getViewAbsRect(mConfiguration.mTargetView, parentX, parentY));
202 | } else {
203 | // Gets the target view's abs rect
204 | View target = activity.findViewById(mConfiguration.mTargetViewId);
205 | if (target != null) {
206 | maskView.setTargetRect(Common.getViewAbsRect(target, parentX, parentY));
207 | }
208 | }
209 |
210 | if (mConfiguration.mOutsideTouchable) {
211 | maskView.setClickable(false);
212 | } else {
213 | maskView.setOnTouchListener(this);
214 | }
215 |
216 | // Adds the components to the mask view.
217 | for (Component c : mComponents) {
218 | maskView.addView(Common.componentToView(activity.getLayoutInflater(), c));
219 | }
220 |
221 | return maskView;
222 | }
223 |
224 | private void onDestroy() {
225 | mConfiguration = null;
226 | mComponents = null;
227 | mOnVisibilityChangedListener = null;
228 | mOnSlideListener = null;
229 | mMaskView.removeAllViews();
230 | mMaskView = null;
231 | }
232 |
233 | @Override
234 | public boolean onKey(View v, int keyCode, KeyEvent event) {
235 | if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) {
236 | if (mConfiguration != null && mConfiguration.mAutoDismiss) {
237 | dismiss();
238 | return true;
239 | } else {
240 | return false;
241 | }
242 | }
243 | return false;
244 | }
245 |
246 | float startY = -1f;
247 |
248 | @Override
249 | public boolean onTouch(View view, MotionEvent motionEvent) {
250 | if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
251 | startY = motionEvent.getY();
252 | } else if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
253 | if (startY - motionEvent.getY() > DimenUtil.dp2px(view.getContext(), SLIDE_THRESHOLD)) {
254 | if (mOnSlideListener != null) {
255 | mOnSlideListener.onSlideListener(GuideBuilder.SlideState.UP);
256 | }
257 | } else if (motionEvent.getY() - startY > DimenUtil.dp2px(view.getContext(), SLIDE_THRESHOLD)) {
258 | if (mOnSlideListener != null) {
259 | mOnSlideListener.onSlideListener(GuideBuilder.SlideState.DOWN);
260 | }
261 | }
262 | if (mConfiguration != null && mConfiguration.mAutoDismiss) {
263 | dismiss();
264 | }
265 | }
266 | return true;
267 | }
268 | }
269 |
--------------------------------------------------------------------------------
/guideview/src/main/java/com/binioter/guideview/GuideBuilder.java:
--------------------------------------------------------------------------------
1 | package com.binioter.guideview;
2 |
3 | import android.support.annotation.AnimatorRes;
4 | import android.support.annotation.IdRes;
5 | import android.support.annotation.IntRange;
6 | import android.view.View;
7 |
8 | import java.util.ArrayList;
9 | import java.util.List;
10 |
11 | /**
12 | *
13 | *
遮罩系统构建器
14 | * 本系统能够快速的为一个Activity里的任何一个View控件创建一个遮罩式的引导页。
15 | *
16 | * 工作原理
17 | * 首先它需要一个目标View或者它的id,我们通过findViewById来得到这个View,计算它在屏幕上的区域targetRect,参见
18 | * {@link #setTargetViewId(int)}与{@link #setTargetView(View)}通过这个区域,
19 | * 开始绘制一个覆盖整个Activity的遮罩,可以定义蒙板的颜色{@link #setFullingColorId(int)}和透明度
20 | * {@link #setAlpha(int)}。然而目标View的区域不会被绘制从而实现高亮的效果。
21 | *
22 | * 接下来是在相对于这个targetRect的区域绘制一些图片或者文字。我们把这样一张图片或者文字抽象成一个Component接口
23 | * {@link Component},设置文字或者图片等
24 | * {@link Component#getView(android.view.LayoutInflater)}
25 | * . 所有的图片文字都是相对于targetRect来定义的。可以设定额外的x,
26 | * {@link Component#getXOffset()} ;y偏移量,
27 | * {@link Component#getYOffset()}。
28 | *
29 | * 可以对遮罩系统设置可见状态的发生变化时的监听回调
30 | * {@link #setOnVisibilityChangedListener(OnVisibilityChangedListener)}
31 | *
32 | * 可以对遮罩系统设置开始和结束时的动画效果 {@link #setEnterAnimationId(int)}
33 | * {@link #setExitAnimationId(int)}
34 | *
35 | *
36 | * Created by binIoter
37 | **/
38 |
39 | public class GuideBuilder {
40 |
41 | public enum SlideState {
42 | UP,DOWN;
43 | }
44 |
45 | private Configuration mConfiguration;
46 |
47 | /**
48 | * Builder被创建后,不允许在对它进行更改
49 | */
50 | private boolean mBuilt;
51 |
52 | private List mComponents = new ArrayList();
53 | private OnVisibilityChangedListener mOnVisibilityChangedListener;
54 | private OnSlideListener mOnSlideListener;
55 |
56 | /**
57 | * 构造函数
58 | */
59 | public GuideBuilder() {
60 | mConfiguration = new Configuration();
61 | }
62 |
63 | /**
64 | * 设置蒙板透明度
65 | *
66 | * @param alpha [0-255] 0 表示完全透明,255表示不透明
67 | * @return GuideBuilder
68 | */
69 | public GuideBuilder setAlpha(@IntRange(from = 0, to = 255) int alpha) {
70 | if (mBuilt) {
71 | throw new BuildException("Already created. rebuild a new one.");
72 | } else if (alpha < 0 || alpha > 255) {
73 | alpha = 0;
74 | }
75 | mConfiguration.mAlpha = alpha;
76 | return this;
77 | }
78 |
79 | /**
80 | * 设置目标view
81 | */
82 | public GuideBuilder setTargetView(View v) {
83 | if (mBuilt) {
84 | throw new BuildException("Already created. rebuild a new one.");
85 | }
86 | mConfiguration.mTargetView = v;
87 | return this;
88 | }
89 |
90 | /**
91 | * 设置目标View的id
92 | *
93 | * @param id 目标View的id
94 | * @return GuideBuilder
95 | */
96 | public GuideBuilder setTargetViewId(@IdRes int id) {
97 | if (mBuilt) {
98 | throw new BuildException("Already created. rebuild a new one.");
99 | }
100 | mConfiguration.mTargetViewId = id;
101 | return this;
102 | }
103 |
104 | /**
105 | * 设置高亮区域的圆角大小
106 | *
107 | * @return GuideBuilder
108 | */
109 | public GuideBuilder setHighTargetCorner(int corner) {
110 | if (mBuilt) {
111 | throw new BuildException("Already created. rebuild a new one.");
112 | } else if (corner < 0) {
113 | mConfiguration.mCorner = 0;
114 | }
115 | mConfiguration.mCorner = corner;
116 | return this;
117 | }
118 |
119 | /**
120 | * 设置高亮区域的图形样式
121 | *
122 | * @return GuideBuilder
123 | */
124 | public GuideBuilder setHighTargetGraphStyle(int style) {
125 | if (mBuilt) {
126 | throw new BuildException("Already created. rebuild a new one.");
127 | }
128 | mConfiguration.mGraphStyle = style;
129 | return this;
130 | }
131 |
132 | /**
133 | * 设置蒙板颜色的资源id
134 | *
135 | * @param id 资源id
136 | * @return GuideBuilder
137 | */
138 | public GuideBuilder setFullingColorId(@IdRes int id) {
139 | if (mBuilt) {
140 | throw new BuildException("Already created. rebuild a new one.");
141 | }
142 | mConfiguration.mFullingColorId = id;
143 | return this;
144 | }
145 |
146 | /**
147 | * 是否在点击的时候自动退出蒙板
148 | *
149 | * @param b true if needed
150 | * @return GuideBuilder
151 | */
152 | public GuideBuilder setAutoDismiss(boolean b) {
153 | if (mBuilt) {
154 | throw new BuildException("Already created, rebuild a new one.");
155 | }
156 | mConfiguration.mAutoDismiss = b;
157 | return this;
158 | }
159 |
160 | /**
161 | * 是否覆盖目标
162 | *
163 | * @param b true 遮罩将会覆盖整个屏幕
164 | * @return GuideBuilder
165 | */
166 | public GuideBuilder setOverlayTarget(boolean b) {
167 | if (mBuilt) {
168 | throw new BuildException("Already created, rebuild a new one.");
169 | }
170 | mConfiguration.mOverlayTarget = b;
171 | return this;
172 | }
173 |
174 | /**
175 | * 设置进入动画
176 | *
177 | * @param id 进入动画的id
178 | * @return GuideBuilder
179 | */
180 | public GuideBuilder setEnterAnimationId(@AnimatorRes int id) {
181 | if (mBuilt) {
182 | throw new BuildException("Already created. rebuild a new one.");
183 | }
184 | mConfiguration.mEnterAnimationId = id;
185 | return this;
186 | }
187 |
188 | /**
189 | * 设置退出动画
190 | *
191 | * @param id 退出动画的id
192 | * @return GuideBuilder
193 | */
194 | public GuideBuilder setExitAnimationId(@AnimatorRes int id) {
195 | if (mBuilt) {
196 | throw new BuildException("Already created. rebuild a new one.");
197 | }
198 | mConfiguration.mExitAnimationId = id;
199 | return this;
200 | }
201 |
202 | /**
203 | * 添加一个控件
204 | *
205 | * @param component 被添加的控件
206 | * @return GuideBuilder
207 | */
208 | public GuideBuilder addComponent(Component component) {
209 | if (mBuilt) {
210 | throw new BuildException("Already created, rebuild a new one.");
211 | }
212 | mComponents.add(component);
213 | return this;
214 | }
215 |
216 | /**
217 | * 设置遮罩可见状态变化时的监听回调
218 | */
219 | public GuideBuilder setOnVisibilityChangedListener(
220 | OnVisibilityChangedListener onVisibilityChangedListener) {
221 | if (mBuilt) {
222 | throw new BuildException("Already created, rebuild a new one.");
223 | }
224 | mOnVisibilityChangedListener = onVisibilityChangedListener;
225 | return this;
226 | }
227 |
228 | /**
229 | * 设置手势滑动的监听回调
230 | */
231 | public GuideBuilder setOnSlideListener(
232 | OnSlideListener onSlideListener) {
233 | if (mBuilt) {
234 | throw new BuildException("Already created, rebuild a new one.");
235 | }
236 | mOnSlideListener = onSlideListener;
237 | return this;
238 | }
239 |
240 | /**
241 | * 设置遮罩系统是否可点击并处理点击事件
242 | *
243 | * @param touchable true 遮罩不可点击,处于不可点击状态 false 可点击,遮罩自己可以处理自身点击事件
244 | */
245 | public GuideBuilder setOutsideTouchable(boolean touchable) {
246 | mConfiguration.mOutsideTouchable = touchable;
247 | return this;
248 | }
249 |
250 | /**
251 | * 设置高亮区域的padding
252 | *
253 | * @return GuideBuilder
254 | */
255 | public GuideBuilder setHighTargetPadding(int padding) {
256 | if (mBuilt) {
257 | throw new BuildException("Already created. rebuild a new one.");
258 | } else if (padding < 0) {
259 | mConfiguration.mPadding = 0;
260 | }
261 | mConfiguration.mPadding = padding;
262 | return this;
263 | }
264 |
265 | /**
266 | * 设置高亮区域的左侧padding
267 | *
268 | * @return GuideBuilder
269 | */
270 | public GuideBuilder setHighTargetPaddingLeft(int padding) {
271 | if (mBuilt) {
272 | throw new BuildException("Already created. rebuild a new one.");
273 | } else if (padding < 0) {
274 | mConfiguration.mPaddingLeft = 0;
275 | }
276 | mConfiguration.mPaddingLeft = padding;
277 | return this;
278 | }
279 |
280 | /**
281 | * 设置高亮区域的顶部padding
282 | *
283 | * @return GuideBuilder
284 | */
285 | public GuideBuilder setHighTargetPaddingTop(int padding) {
286 | if (mBuilt) {
287 | throw new BuildException("Already created. rebuild a new one.");
288 | } else if (padding < 0) {
289 | mConfiguration.mPaddingTop = 0;
290 | }
291 | mConfiguration.mPaddingTop = padding;
292 | return this;
293 | }
294 |
295 | /**
296 | * 设置高亮区域的右侧padding
297 | *
298 | * @return GuideBuilder
299 | */
300 | public GuideBuilder setHighTargetPaddingRight(int padding) {
301 | if (mBuilt) {
302 | throw new BuildException("Already created. rebuild a new one.");
303 | } else if (padding < 0) {
304 | mConfiguration.mPaddingRight = 0;
305 | }
306 | mConfiguration.mPaddingRight = padding;
307 | return this;
308 | }
309 |
310 | /**
311 | * 设置高亮区域的底部padding
312 | *
313 | * @return GuideBuilder
314 | */
315 | public GuideBuilder setHighTargetPaddingBottom(int padding) {
316 | if (mBuilt) {
317 | throw new BuildException("Already created. rebuild a new one.");
318 | } else if (padding < 0) {
319 | mConfiguration.mPaddingBottom = 0;
320 | }
321 | mConfiguration.mPaddingBottom = padding;
322 | return this;
323 | }
324 |
325 | /**
326 | * 创建Guide,非Fragment版本
327 | *
328 | * @return Guide
329 | */
330 | public Guide createGuide() {
331 | Guide guide = new Guide();
332 | Component[] components = new Component[mComponents.size()];
333 | guide.setComponents(mComponents.toArray(components));
334 | guide.setConfiguration(mConfiguration);
335 | guide.setCallback(mOnVisibilityChangedListener);
336 | guide.setOnSlideListener(mOnSlideListener);
337 | mComponents = null;
338 | mConfiguration = null;
339 | mOnVisibilityChangedListener = null;
340 | mBuilt = true;
341 | return guide;
342 | }
343 |
344 | /**
345 | * 手势滑动监听
346 | */
347 | public static interface OnSlideListener {
348 |
349 | void onSlideListener(SlideState state);
350 | }
351 |
352 | /**
353 | * 遮罩可见发生变化时的事件监听
354 | */
355 | public static interface OnVisibilityChangedListener {
356 |
357 | void onShown();
358 |
359 | void onDismiss();
360 | }
361 | }
362 |
--------------------------------------------------------------------------------
/guideview/src/main/java/com/binioter/guideview/MaskView.java:
--------------------------------------------------------------------------------
1 | package com.binioter.guideview;
2 |
3 | import android.content.Context;
4 | import android.graphics.Bitmap;
5 | import android.graphics.Canvas;
6 | import android.graphics.Color;
7 | import android.graphics.Paint;
8 | import android.graphics.PorterDuff;
9 | import android.graphics.PorterDuffXfermode;
10 | import android.graphics.Rect;
11 | import android.graphics.RectF;
12 | import android.util.AttributeSet;
13 | import android.util.DisplayMetrics;
14 | import android.view.View;
15 | import android.view.ViewGroup;
16 | import android.view.WindowManager;
17 |
18 | import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
19 |
20 | /**
21 | * Created by binIoter
22 | */
23 |
24 | class MaskView extends ViewGroup {
25 | /**
26 | * 高亮区域
27 | */
28 | private final RectF mTargetRect = new RectF();
29 | /**
30 | * 蒙层区域
31 | */
32 | private final RectF mOverlayRect = new RectF();
33 |
34 | /**
35 | * 中间变量
36 | */
37 | private final RectF mChildTmpRect = new RectF();
38 | /**
39 | * 蒙层背景画笔
40 | */
41 | private final Paint mFullingPaint;
42 | private int mPadding = 0;
43 | private int mPaddingLeft = 0;
44 | private int mPaddingTop = 0;
45 | private int mPaddingRight = 0;
46 | private int mPaddingBottom = 0;
47 | /**
48 | * 是否覆盖目标区域
49 | */
50 | private boolean mOverlayTarget = false;
51 | /**
52 | * 圆角大小
53 | */
54 | private int mCorner = 0;
55 | /**
56 | * 目标区域样式,默认为矩形
57 | */
58 | private int mStyle = Component.ROUNDRECT;
59 | /**
60 | * 挖空画笔
61 | */
62 | private Paint mEraser;
63 | /**
64 | * 橡皮擦Bitmap
65 | */
66 | private Bitmap mEraserBitmap;
67 | /**
68 | * 橡皮擦Cavas
69 | */
70 | private Canvas mEraserCanvas;
71 |
72 | private boolean ignoreRepadding;
73 |
74 | private int mInitHeight;
75 | private int mChangedHeight = 0;
76 | private boolean mFirstFlag = true;
77 |
78 | public MaskView(Context context) {
79 | this(context, null, 0);
80 | }
81 |
82 | public MaskView(Context context, AttributeSet attrs) {
83 | this(context, attrs, 0);
84 | }
85 |
86 | public MaskView(Context context, AttributeSet attrs, int defStyle) {
87 | super(context, attrs, defStyle);
88 | //自我绘制
89 | setWillNotDraw(false);
90 |
91 | WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
92 | DisplayMetrics displayMetrics = new DisplayMetrics();
93 | wm.getDefaultDisplay().getRealMetrics(displayMetrics);
94 | int width = displayMetrics.widthPixels;
95 | int height = displayMetrics.heightPixels;
96 | mOverlayRect.set(0, 0, width, height);
97 | mEraserBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
98 | mEraserCanvas = new Canvas(mEraserBitmap);
99 | mFullingPaint = new Paint();
100 | mEraser = new Paint();
101 | mEraser.setColor(0xFFFFFFFF);
102 | //图形重叠时的处理方式,擦除效果
103 | mEraser.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
104 | //位图抗锯齿设置
105 | mEraser.setFlags(Paint.ANTI_ALIAS_FLAG);
106 | }
107 |
108 | @Override
109 | protected void onDetachedFromWindow() {
110 | super.onDetachedFromWindow();
111 | try {
112 | clearFocus();
113 | mEraserCanvas.setBitmap(null);
114 | mEraserBitmap = null;
115 | } catch (Exception e) {
116 | e.printStackTrace();
117 | }
118 | }
119 |
120 | @Override
121 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
122 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
123 | final int w = MeasureSpec.getSize(widthMeasureSpec);
124 | final int h = MeasureSpec.getSize(heightMeasureSpec);
125 | if (mFirstFlag) {
126 | mInitHeight = h;
127 | mFirstFlag = false;
128 | }
129 | if (mInitHeight > h) {
130 | mChangedHeight = h - mInitHeight;
131 | } else if (mInitHeight < h) {
132 | mChangedHeight = h - mInitHeight;
133 | } else {
134 | mChangedHeight = 0;
135 | }
136 | setMeasuredDimension(w, h);
137 | mOverlayRect.set(0, 0, w, h);
138 | resetOutPath();
139 |
140 | final int count = getChildCount();
141 | View child;
142 | for (int i = 0; i < count; i++) {
143 | child = getChildAt(i);
144 | if (child != null) {
145 | measureChild(child, widthMeasureSpec, heightMeasureSpec);
146 | }
147 | }
148 | }
149 |
150 | @Override
151 | protected void onLayout(boolean changed, int l, int t, int r, int b) {
152 | final int count = getChildCount();
153 | final float density = getResources().getDisplayMetrics().density;
154 | View child;
155 | for (int i = 0; i < count; i++) {
156 | child = getChildAt(i);
157 | if (child == null) {
158 | continue;
159 | }
160 | LayoutParams lp = (LayoutParams) child.getLayoutParams();
161 | if (lp == null) {
162 | continue;
163 | }
164 | switch (lp.targetAnchor) {
165 | case LayoutParams.ANCHOR_LEFT://左
166 | mChildTmpRect.right = mTargetRect.left;
167 | mChildTmpRect.left = mChildTmpRect.right - child.getMeasuredWidth();
168 | verticalChildPositionLayout(child, mChildTmpRect, lp.targetParentPosition);
169 | break;
170 | case LayoutParams.ANCHOR_TOP://上
171 | mChildTmpRect.bottom = mTargetRect.top;
172 | mChildTmpRect.top = mChildTmpRect.bottom - child.getMeasuredHeight();
173 | horizontalChildPositionLayout(child, mChildTmpRect, lp.targetParentPosition);
174 | break;
175 | case LayoutParams.ANCHOR_RIGHT://右
176 | mChildTmpRect.left = mTargetRect.right;
177 | mChildTmpRect.right = mChildTmpRect.left + child.getMeasuredWidth();
178 | verticalChildPositionLayout(child, mChildTmpRect, lp.targetParentPosition);
179 | break;
180 | case LayoutParams.ANCHOR_BOTTOM://下
181 | mChildTmpRect.top = mTargetRect.bottom;
182 | mChildTmpRect.bottom = mChildTmpRect.top + child.getMeasuredHeight();
183 | horizontalChildPositionLayout(child, mChildTmpRect, lp.targetParentPosition);
184 | break;
185 | case LayoutParams.ANCHOR_OVER://中心
186 | mChildTmpRect.left = ((int) mTargetRect.width() - child.getMeasuredWidth()) >> 1;
187 | mChildTmpRect.top = ((int) mTargetRect.height() - child.getMeasuredHeight()) >> 1;
188 | mChildTmpRect.right = ((int) mTargetRect.width() + child.getMeasuredWidth()) >> 1;
189 | mChildTmpRect.bottom = ((int) mTargetRect.height() + child.getMeasuredHeight()) >> 1;
190 | mChildTmpRect.offset(mTargetRect.left, mTargetRect.top);
191 | break;
192 | }
193 | //额外的xy偏移
194 | mChildTmpRect.offset((int) (density * lp.offsetX + 0.5f),
195 | (int) (density * lp.offsetY + 0.5f));
196 | child.layout((int) mChildTmpRect.left, (int) mChildTmpRect.top, (int) mChildTmpRect.right,
197 | (int) mChildTmpRect.bottom);
198 | }
199 | }
200 |
201 | private void horizontalChildPositionLayout(View child, RectF rect, int targetParentPosition) {
202 | switch (targetParentPosition) {
203 | case LayoutParams.PARENT_START:
204 | rect.left = mTargetRect.left;
205 | rect.right = rect.left + child.getMeasuredWidth();
206 | break;
207 | case LayoutParams.PARENT_CENTER:
208 | rect.left = (mTargetRect.width() - child.getMeasuredWidth()) / 2;
209 | rect.right = (mTargetRect.width() + child.getMeasuredWidth()) / 2;
210 | rect.offset(mTargetRect.left, 0);
211 | break;
212 | case LayoutParams.PARENT_END:
213 | rect.right = mTargetRect.right;
214 | rect.left = rect.right - child.getMeasuredWidth();
215 | break;
216 | }
217 | }
218 |
219 | private void verticalChildPositionLayout(View child, RectF rect, int targetParentPosition) {
220 | switch (targetParentPosition) {
221 | case LayoutParams.PARENT_START:
222 | rect.top = mTargetRect.top;
223 | rect.bottom = rect.top + child.getMeasuredHeight();
224 | break;
225 | case LayoutParams.PARENT_CENTER:
226 | rect.top = (mTargetRect.width() - child.getMeasuredHeight()) / 2;
227 | rect.bottom = (mTargetRect.width() + child.getMeasuredHeight()) / 2;
228 | rect.offset(0, mTargetRect.top);
229 | break;
230 | case LayoutParams.PARENT_END:
231 | rect.bottom = mTargetRect.bottom;
232 | rect.top = mTargetRect.bottom - child.getMeasuredHeight();
233 | break;
234 | }
235 | }
236 |
237 | private void resetOutPath() {
238 | resetPadding();
239 | }
240 |
241 | /**
242 | * 设置padding
243 | */
244 | private void resetPadding() {
245 | if (!ignoreRepadding) {
246 | if (mPadding != 0 && mPaddingLeft == 0) {
247 | mTargetRect.left -= mPadding;
248 | }
249 | if (mPadding != 0 && mPaddingTop == 0) {
250 | mTargetRect.top -= mPadding;
251 | }
252 | if (mPadding != 0 && mPaddingRight == 0) {
253 | mTargetRect.right += mPadding;
254 | }
255 | if (mPadding != 0 && mPaddingBottom == 0) {
256 | mTargetRect.bottom += mPadding;
257 | }
258 | if (mPaddingLeft != 0) {
259 | mTargetRect.left -= mPaddingLeft;
260 | }
261 | if (mPaddingTop != 0) {
262 | mTargetRect.top -= mPaddingTop;
263 | }
264 | if (mPaddingRight != 0) {
265 | mTargetRect.right += mPaddingRight;
266 | }
267 | if (mPaddingBottom != 0) {
268 | mTargetRect.bottom += mPaddingBottom;
269 | }
270 | ignoreRepadding = true;
271 | }
272 | }
273 |
274 | @Override
275 | protected LayoutParams generateDefaultLayoutParams() {
276 | return new LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
277 | }
278 |
279 | @Override
280 | protected void dispatchDraw(Canvas canvas) {
281 | final long drawingTime = getDrawingTime();
282 | try {
283 | View child;
284 | for (int i = 0; i < getChildCount(); i++) {
285 | child = getChildAt(i);
286 | drawChild(canvas, child, drawingTime);
287 | }
288 | } catch (NullPointerException e) {
289 |
290 | }
291 | }
292 |
293 | @Override
294 | protected void onDraw(Canvas canvas) {
295 | super.onDraw(canvas);
296 | if (mChangedHeight != 0) {
297 | mTargetRect.offset(0, mChangedHeight);
298 | mInitHeight = mInitHeight + mChangedHeight;
299 | mChangedHeight = 0;
300 | }
301 | mEraserBitmap.eraseColor(Color.TRANSPARENT);
302 | mEraserCanvas.drawColor(mFullingPaint.getColor());
303 | if (!mOverlayTarget) {
304 | switch (mStyle) {
305 | case Component.ROUNDRECT:
306 | mEraserCanvas.drawRoundRect(mTargetRect, mCorner, mCorner, mEraser);
307 | break;
308 | case Component.CIRCLE:
309 | mEraserCanvas.drawCircle(mTargetRect.centerX(), mTargetRect.centerY(),
310 | mTargetRect.width() / 2, mEraser);
311 | break;
312 | default:
313 | mEraserCanvas.drawRoundRect(mTargetRect, mCorner, mCorner, mEraser);
314 | break;
315 | }
316 | }
317 | canvas.drawBitmap(mEraserBitmap, mOverlayRect.left, mOverlayRect.top, null);
318 | }
319 |
320 | public void setTargetRect(Rect rect) {
321 | mTargetRect.set(rect);
322 | }
323 |
324 | public void setFullingAlpha(int alpha) {
325 | mFullingPaint.setAlpha(alpha);
326 | }
327 |
328 | public void setFullingColor(int color) {
329 | mFullingPaint.setColor(color);
330 | }
331 |
332 | public void setHighTargetCorner(int corner) {
333 | this.mCorner = corner;
334 | }
335 |
336 | public void setHighTargetGraphStyle(int style) {
337 | this.mStyle = style;
338 | }
339 |
340 | public void setOverlayTarget(boolean b) {
341 | mOverlayTarget = b;
342 | }
343 |
344 | public void setPadding(int padding) {
345 | this.mPadding = padding;
346 | }
347 |
348 | public void setPaddingLeft(int paddingLeft) {
349 | this.mPaddingLeft = paddingLeft;
350 | }
351 |
352 | public void setPaddingTop(int paddingTop) {
353 | this.mPaddingTop = paddingTop;
354 | }
355 |
356 | public void setPaddingRight(int paddingRight) {
357 | this.mPaddingRight = paddingRight;
358 | }
359 |
360 | public void setPaddingBottom(int paddingBottom) {
361 | this.mPaddingBottom = paddingBottom;
362 | }
363 |
364 | static class LayoutParams extends ViewGroup.LayoutParams {
365 |
366 | public static final int ANCHOR_LEFT = 0x01;
367 | public static final int ANCHOR_TOP = 0x02;
368 | public static final int ANCHOR_RIGHT = 0x03;
369 | public static final int ANCHOR_BOTTOM = 0x04;
370 | public static final int ANCHOR_OVER = 0x05;
371 |
372 | public static final int PARENT_START = 0x10;
373 | public static final int PARENT_CENTER = 0x20;
374 | public static final int PARENT_END = 0x30;
375 |
376 | public int targetAnchor = ANCHOR_BOTTOM;
377 | public int targetParentPosition = PARENT_CENTER;
378 | public int offsetX = 0;
379 | public int offsetY = 0;
380 |
381 | public LayoutParams(Context c, AttributeSet attrs) {
382 | super(c, attrs);
383 | }
384 |
385 | public LayoutParams(int width, int height) {
386 | super(width, height);
387 | }
388 |
389 | public LayoutParams(ViewGroup.LayoutParams source) {
390 | super(source);
391 | }
392 | }
393 | }
394 |
--------------------------------------------------------------------------------
/guideview/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/binIoter/GuideView/39d131a88bd2e7eb8b0b12cd353e3a11dcdaa59c/guideview/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/guideview/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/binIoter/GuideView/39d131a88bd2e7eb8b0b12cd353e3a11dcdaa59c/guideview/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/guideview/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/binIoter/GuideView/39d131a88bd2e7eb8b0b12cd353e3a11dcdaa59c/guideview/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/guideview/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/binIoter/GuideView/39d131a88bd2e7eb8b0b12cd353e3a11dcdaa59c/guideview/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/guideview/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/guideview/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 |
--------------------------------------------------------------------------------
/guideview/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | guideview
3 |
4 | Hello world!
5 | Settings
6 |
7 |
--------------------------------------------------------------------------------
/guideview/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':guideview'
2 |
--------------------------------------------------------------------------------