Android 强大实用的功能引导组件
Posted Android编程精选
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 强大实用的功能引导组件相关的知识,希望对你有一定的参考价值。
作者丨JustinRoom
https://www.jianshu.com/p/c1aaddd93245
开篇
每一个新上线App的功能引导很重要,没有功能引导,我们往往需要花费大量的时间以及人力去培训客户,从公司层面上这无疑是增加了很大的开销。
利用闲暇时间封装了一个功能引导组件,使用方便。次组件已经发布到jCenter。希望童鞋们多多支持!
使用简洁方便
带有引导波纹动画(可个性化配置,如颜色、动画速度、大小等)
引导画面的上按钮与需被引导的按钮同等样式(如大小、颜色、形状、内容...)
支持动态添加各种需要显示的view.
GuidanceRippleView
https://github.com/JustinRoom/WheelViewDemo
下面依次分享相关组件
一、GuidanceRippleView
https://github.com/JustinRoom/GuidanceDemo/blob/master/guidanceLibrary/src/main/java/jsc/kit/guidance/GuidanceRippleView.java
一个实现循环水波纹动画的自定义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
https://github.com/JustinRoom/GuidanceDemo/blob/master/guidanceLibrary/src/main/java/jsc/kit/guidance/GuidanceLayout.java
用来控制被引导按钮的显示位置,以及添加其他的子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。
https://github.com/JustinRoom/GuidanceDemo/blob/master/guidanceLibrary/src/main/java/jsc/kit/guidance/GuidancePopupWindow.java
原理:
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。
https://github.com/JustinRoom/GuidanceDemo/blob/master/guidanceLibrary/src/main/java/jsc/kit/guidance/GuidanceDialog.java
原理:小标题说明这是一个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。
https://www.jianshu.com/p/c1aaddd93245
此种方式有很多坑,不建议用此方式。
原理:
WindowManager manager = activity.getWindowManager();
manager.addView(View view, ViewGroup.LayoutParams params);
童鞋们给个 以上是关于Android 强大实用的功能引导组件的主要内容,如果未能解决你的问题,请参考以下文章 译文:18个实用的JavaScript代码片段,助你快速处理日常编程任务 Android零基础入门第69节:ViewPager快速实现引导页