开篇
每一个新上线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
共同學習,寫下你的評論
評論加載中...
作者其他優質文章