开篇
每一个新上线App的功能引导很重要,没有功能引导,我们往往需要花费大量的时间以及人力去培训客户,从公司层面上这无疑是增加了很大的开销。
利用闲暇时间封装了一个功能引导组件,使用方便。次组件已经发布到jCenter。希望童鞋们多多支持!
功能盘点
使用简洁方便
带有引导波纹动画(可个性化配置,如颜色、动画速度、大小等)
引导画面的上按钮与需被引导的按钮同等样式(如大小、颜色、形状、内容...)
支持动态添加各种需要显示的view.
效果截屏
GuidanceRippleView
GuidanceLayout
立即体验
扫描以下二维码下载体验App(体验App内嵌版本更新检测功能):
[开源库传送门:https://github.com/JustinRoom/GuidanceDemo)
简析源码
下面依次分享相关组件
一、GuidanceRippleView
一个实现循环水波纹动画的自定义view
其主要逻辑code很简单(绘制、动画控制都在onDraw()方法中)、没有什么难懂的东西,自行看代码理解:
protected void onDraw(Canvas canvas)
@Override
protected void onDraw(Canvas canvas) { if (speed <= 0) {
speed = space / frameCountPerSecond;
} if (clipWidth > 0 && clipHeight > 0) { int clipLeft = (getWidth() - clipWidth) / 2; int clipTop = (getHeight() - clipHeight) / 2;
canvas.clipRect(clipLeft, clipTop, clipLeft + clipWidth, clipTop + clipHeight);
} float maxRadius = getWidth() / 2.0f; int alpha = (int) (0xFF * (1 - radius / maxRadius) + .5f); for (int i = 0; i < count; i++) {
paint.setColor(colors[i]);
paint.setAlpha(alpha); float tempRadius = radius - space * i; if (tempRadius > 0)
canvas.drawCircle(maxRadius, maxRadius, tempRadius, paint);
}
radius += speed; if (radius > maxRadius)
radius = 0; if (isRunning)
invalidate();
}控制动画相关方法:
public void start() { if (isRunning) return;
radius = 0;
isRunning = true;
invalidate();
} public void stop() {
radius = 0;
isRunning = false;
invalidate();
} public void pause() {
isRunning = false;
} public void resume() {
isRunning = true;
invalidate();
}二、GuidanceLayout
用来控制被引导按钮的显示位置,以及添加其他的子view。
分析关键code:
/**
* Update the target view's location.
*
* @param targetView target
* @param l the left margin
* @param t the top margin
* @param rippleViewSize the size of {@link #rippleViewView}
* @param rippleClipToTarget true, clip {@link #rippleViewView} to {@link #targetRect} area.
*/
public void updateTargetViewLocation(@NonNull View targetView, int l, int t, int rippleViewSize, boolean rippleClipToTarget, OnRippleViewLocationUpdatedCallback callback) {
Bitmap bitmap = ViewDrawingCacheUtils.getDrawingCache(targetView);
updateTargetViewLocation(bitmap, l, t, rippleViewSize, rippleClipToTarget, callback);
} /**
* Update the target view's location.
*
* @param targetView target
* @param l the left margin
* @param t the top margin
* @param listener listener for initializing {@link #rippleViewView}'s size
* @param rippleClipToTarget true, clip {@link #rippleViewView} to {@link #targetRect} area.
*/
public void updateTargetViewLocation(@NonNull View targetView, int l, int t, OnInitRippleViewSizeListener listener, boolean rippleClipToTarget, OnRippleViewLocationUpdatedCallback callback) {
Bitmap bitmap = ViewDrawingCacheUtils.getDrawingCache(targetView); int size = listener == null ? getResources().getDimensionPixelSize(R.dimen.guidance_default_ripple_size) : listener.onInitializeRippleViewSize(bitmap);
updateTargetViewLocation(bitmap, l, t, size, rippleClipToTarget, callback);
} public void updateTargetViewLocation(Bitmap bitmap, int l, int t, int rippleViewSize, boolean rippleClipToTarget, OnRippleViewLocationUpdatedCallback callback) {
curStepIndex++; if (bitmap == null) return;
targetRect.set(l, t, l + bitmap.getWidth(), t + bitmap.getHeight());
ViewGroup.LayoutParams params = targetView.getLayoutParams();
params.width = targetRect.width();
params.height = targetRect.height(); if (params instanceof MarginLayoutParams) {
((MarginLayoutParams) params).leftMargin = targetRect.left;
((MarginLayoutParams) params).topMargin = targetRect.top;
}
targetView.setLayoutParams(params);
targetView.setImageBitmap(bitmap);
updateRippleViewLocation(rippleViewSize, callback); if (rippleClipToTarget)
rippleViewView.setClip(targetRect.width(), targetRect.height()); else
rippleViewView.setClip(-1, -1);
} /**
* Update ripple view's location.
*
* @param size size
* @param callback call back when the ripple view's location was updated.
*/
private void updateRippleViewLocation(int size, OnRippleViewLocationUpdatedCallback callback) { if (size < 0) throw new IllegalArgumentException("Bad params:size is less than zero.");
ViewGroup.LayoutParams params = rippleViewView.getLayoutParams();
params.width = size;
params.height = size; if (params instanceof MarginLayoutParams) {
((MarginLayoutParams) params).leftMargin = (targetRect.left + targetRect.right - size) / 2;
((MarginLayoutParams) params).topMargin = (targetRect.top + targetRect.bottom - size) / 2;
}
rippleViewView.setLayoutParams(params); if (callback != null)
callback.onRippleViewLocationUpdated(rippleViewView, targetRect);
}targetView——需要被引导的view。
详细逻辑步骤:
1、获取
targetView在屏幕中的坐标位置2、获取
targetView的drawingCache。
方法一:获取
targetView的drawingCache。
public static Bitmap getDrawingCache(@NonNull View view) {
view.setDrawingCacheEnabled(true);
Bitmap bitmap = view.getDrawingCache();
view.setDrawingCacheEnabled(false); return bitmap;
}方法二:让
targetView在我们自己创建的画布上画一遍。
public static Bitmap getDrawingCache(@NonNull View view) { int width = view.getWidth(); int height = view.getHeight(); if (width + height == 0) {
view.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
width = view.getMeasuredWidth();
height = view.getMeasuredHeight();
}
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
view.draw(canvas); return bitmap;
}3、新建一个
ImageView并加载第2部中获取到的Bitmap。4、添加水波纹动画view。
三、3种展示功能引导方式
1、普通view方式GuidancePopupWindow。
原理:
a、ViewGroup root = activity.findViewById(android.R.id.content)
b、root.addView(guideLayout, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
public GuidancePopupWindow(@NonNull Activity activity) { this(activity, 0x99000000);
} public GuidancePopupWindow(@NonNull Activity activity, @ColorInt int backgroundColor) { this.activity = activity;
guidanceLayout = new GuidanceLayout(activity);
guidanceLayout.setId(R.id.guidance_default_layout_id);
guidanceLayout.setBackgroundColor(backgroundColor);
guidanceLayout.setTargetClickListener(new View.OnClickListener() { @Override
public void onClick(View v) { if (listener == null || !listener.onTargetClick(guidanceLayout))
dismiss();
}
});
} public void show() {
show(SHOW_IN_CONTENT);
} public void show(@ShowType int showType) { this.curShowType = showType; switch (curShowType) { case SHOW_IN_CONTENT: //找到根布局中id为android.R.id.content的ViewGroup
//添加GuidanceLayout控件
FrameLayout contentLayout = activity.findViewById(android.R.id.content); if (!isGuidanceLayoutAdded(guidanceLayout)) {
contentLayout.addView(guidanceLayout);
} break; case SHOW_IN_WINDOW: //此模式下很多权限方面的坑,不建议使用
WindowManager.LayoutParams params = new WindowManager.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
} else {
params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
}
activity.getWindow().getWindowManager().addView(guidanceLayout, params); break;
}
}
TYPE_APPLICATION_OVERLAY、TYPE_SYSTEM_ALERT需要android.permission.SYSTEM_ALERT_WINDOW权限,在6.0系统及以下,只要在AndroidManifest.xml文件中声名即可;在6.0以上系统中,我们需要主动申请权限,申请方法如下:
public static boolean checkOverlayPermission(@NonNull FragmentActivity activity, int requestCode){ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return true; if (!Settings.canDrawOverlays(activity)) {
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
intent.setData(Uri.parse("package:" + activity.getPackageName()));
activity.startActivityForResult(intent, requestCode);
} return true;
}使用示例
private void showContentGuidance() { final GuidancePopupWindow popupWindow = new GuidancePopupWindow(getActivity());
popupWindow.setTargetClickListener(new OnTargetClickListener() { @Override
public boolean onTargetClick(GuidanceLayout layout) {
Toast.makeText(layout.getContext(), "clicked me", Toast.LENGTH_SHORT).show(); switch (layout.getCurStepIndex()) { case 0:
layout.removeAllCustomViews();
showStep(layout, R.id.item_layout_1); return true; case 1:
layout.removeAllCustomViews();
showStep(layout, R.id.item_layout_2); return true; case 2:
layout.removeAllCustomViews();
showStep(layout, R.id.item_layout_3); return true; default: return false;
}
}
});
popupWindow.show();
GuidanceLayout guidanceLayout = popupWindow.getGuidanceLayout();
showStep(guidanceLayout, R.id.item_layout_0);
}2、Dialog方式GuidanceDialog。
原理:小标题说明这是一个Dialog。
super(context);
setCancelable(false);
setCanceledOnTouchOutside(false);
} public GuidanceDialog(@NonNull Context context, int themeResId) { super(context, themeResId);
setCancelable(false);
setCanceledOnTouchOutside(false);
} @Override
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
supportRequestWindowFeature(Window.FEATURE_NO_TITLE);
guidanceLayout = new GuidanceLayout(getContext());
guidanceLayout.setTargetClickListener(new View.OnClickListener() { @Override
public void onClick(View v) { if (listener == null || !listener.onTargetClick(guidanceLayout))
dismiss();
}
});
setContentView(guidanceLayout, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); if (getWindow() != null) { //设置window背景,默认的背景会有Padding值,不能全屏。当然不一定要是透明,你可以设置其他背景,替换默认的背景即可。
getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); //一定要在setContentView之后调用,否则无效
getWindow().setLayout(width, height);
}
}使用示例:
private void showGuidanceDialog() { final GuidanceDialog dialog = new GuidanceDialog(getContext());
dialog.setTargetClickListener(new OnTargetClickListener() { @Override
public boolean onTargetClick(GuidanceLayout layout) {
Toast.makeText(layout.getContext(), "clicked me", Toast.LENGTH_SHORT).show(); switch (layout.getCurStepIndex()) { case 0:
showStep(layout, R.id.item_layout_1); return true; case 1:
showStep(layout, R.id.item_layout_2); return true; case 2:
showStep(layout, R.id.item_layout_3); return true; default: return false;
}
}
});
dialog.show();
GuidanceLayout guidanceLayout = dialog.getGuidanceLayout(); if (guidanceLayout == null) return;
showStep(guidanceLayout, R.id.item_layout_0);
}公共方法:
private void showStep(GuidanceLayout layout, int targetViewId) {
layout.removeAllCustomViews();
showStep(layout, getView().findViewById(targetViewId));
} private void showStep(GuidanceLayout guidanceLayout, View target) {
Context context = guidanceLayout.getContext(); int statusBarHeight = ViewDrawingCacheUtils.getStatusBarHeight(context); int actionBarHeight = ViewDrawingCacheUtils.getActionBarSize(context); int[] location = ViewDrawingCacheUtils.getWindowLocation(target);
guidanceLayout.updateTargetViewLocation(
target, location[0],
location[1] - statusBarHeight, new GuidanceLayout.OnInitRippleViewSizeListener() { @Override
public int onInitializeRippleViewSize(@NonNull Bitmap bitmap) { return bitmap.getHeight();
}
}, true, new GuidanceLayout.OnRippleViewLocationUpdatedCallback() { @Override
public void onRippleViewLocationUpdated(@NonNull GuidanceRippleView rippleView, @NonNull Rect targetRect) {
}
});
ImageView imageView = new ImageView(guidanceLayout.getContext());
imageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
imageView.setImageResource(R.drawable.hand_o_up);
guidanceLayout.addCustomView(imageView, new GuidanceLayout.OnCustomViewAddListener<ImageView>() { @Override
public void onViewInit(@NonNull ImageView customView, @NonNull FrameLayout.LayoutParams params, @NonNull Rect targetRect) {
customView.measure(0, 0);
params.topMargin = targetRect.bottom + 12;
params.leftMargin = targetRect.left - (customView.getMeasuredWidth() - targetRect.width()) / 2;
} @Override
public void onViewAdded(@NonNull ImageView customView, @NonNull Rect targetRect) {
ObjectAnimator animator = ObjectAnimator.ofFloat(customView, View.TRANSLATION_Y, 0, 32, 0)
.setDuration(1200);
animator.setRepeatCount(-1);
animator.start();
}
}, null);
}3、WindowManager添加View方式WindowManager。
此种方式有很多坑,不建议用此方式。
原理:
WindowManager manager = activity.getWindowManager();manager.addView(View view, ViewGroup.LayoutParams params);
作者:JustinRoom
链接:https://www.jianshu.com/p/c1aaddd93245