开篇
利用闲暇时间撸了一个功能引导组件,此组件将在0.4.4
版本里添加。小弟不才,一心只想打造一个能搞定一个应用的基础库。希望童鞋们多多支持!
- 引导波纹动画(可个性化配置,如颜色、动画速度、大小等)
- 引导画面的上按钮与需被引导的按钮同等样式(如大小、颜色、形状)
- 支持动态添加各种需要显示的view.
效果截屏
立即体验
扫描以下二维码下载体验App(从0.2.3
版本开始,体验App内嵌版本更新检测功能):
JSCKit库传送门:https://github.com/JustinRoom/JSCKit
简析源码
下面依次分享相关组件
一、GuideRippleView
一个实现循环水波纹动画的自定义view
其主要逻辑code
很简单(绘制、动画控制都在onDraw()
方法中)、没有什么难懂的东西,自行看代码理解:
- protected void onDraw(Canvas canvas)
@Override
protected void onDraw(Canvas canvas) {
//只有开启动画时,才进行绘制。
if (!isRunning)
return;
//这里是限制动画绘制区域
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) (startAlpha * (1 - radius / maxRadius) + .5f);
for (int i = 0; i < circleCount; i++) {
paint.setColor(colors[I]);
paint.setAlpha(alpha);
float tempRadius = radius - circleSpace * I;
if (tempRadius <= 0)
break;
canvas.drawCircle(maxRadius, maxRadius, tempRadius, paint);
}
//speed这是控制动画速度的参数
radius += speed;
if (radius > maxRadius)
radius = 0;
invalidate();
}
二、GuideLayout
用来控制被引导按钮的显示位置,以及添加其他的子view。
分析关键code
:
public void updateTargetLocation(@NonNull View target, int yOffset, int minRippleSize, int maxRippleSize, OnRippleViewUpdateLocationCallback callback) {
Bitmap bitmap = Bitmap.createBitmap(target.getDrawingCache());
int[] location = new int[2];
target.getLocationOnScreen(location);
targetRect.set(location[0], location[1], location[0] + target.getWidth(), location[1] + target.getHeight());
targetRect.offset(0, -yOffset);
ivTarget.setImageBitmap(bitmap);
MarginLayoutParams params = (MarginLayoutParams) ivTarget.getLayoutParams();
params.leftMargin = targetRect.left;
params.topMargin = targetRect.top;
ivTarget.setLayoutParams(params);
updateRippleLocation(targetRect, minRippleSize, maxRippleSize, callback);
}
private void updateRippleLocation(@NonNull Rect targetRect, int minRippleSize, int maxRippleSize, OnRippleViewUpdateLocationCallback callback) {
int size = Math.max(targetRect.width(), targetRect.height());
int min = Math.min(minRippleSize, maxRippleSize);
int max = minRippleSize + maxRippleSize - min;
if (min > 0) {
size = Math.max(size, minRippleSize);
}
if (max > 0) {
size = Math.min(size, maxRippleSize);
}
LayoutParams params = new LayoutParams(size, 0);
params.leftMargin = (targetRect.left + targetRect.right - size) / 2;
params.topMargin = (targetRect.top + targetRect.bottom - size) / 2;
if (guideRippleViewView == null) {
guideRippleViewView = new GuideRippleView(getContext());
addView(guideRippleViewView, params);
} else {
guideRippleViewView.setLayoutParams(params);
}
if (callback != null)
callback.onRippleViewUpdateLocation(guideRippleViewView);
}
target
——被引导的按钮。
详细逻辑步骤:
- 1、获取
target
在屏幕中的坐标位置 - 2、获取
target
的drawingCache
。在getDrawingCache()
之前我们需要调用target.setDrawingCacheEnabled(true)
,使用完drawingCache
完之后我们需要关掉target.setDrawingCacheEnabled(false)
,避免性能损耗。 - 3、新建一个
ImageView
并加载从target
的drawingCache
中生成的Bitmap
。 - 4、添加水波纹动画view。
三、4种展示功能引导方式
- 1、普通view方式GuidePopupView。
原理:
a、ViewGroup root = activity.findViewById(android.R.id.content)
b、root.addView(guideLayout, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
/**
* @param context context
* @param yOffset offset from top
* @param minRippleSize minimum ripple size
* @param maxRippleSize max ripple size
*/
public GuidePopupView(Context context, int yOffset, int minRippleSize, int maxRippleSize) {
this.minRippleSize = Math.min(minRippleSize, maxRippleSize);
this.maxRippleSize = Math.max(minRippleSize, maxRippleSize);
this.yOffset = yOffset;
guideLayout = new GuideLayout(context);
guideLayout.setOnClickListener(null);
guideLayout.setOnLongClickListener(null);
}
/**
* Before showing action, it must had attached target.
*/
public void show(@NonNull Activity activity) {
if (target == null)
throw new IllegalStateException("You need attach target first.");
View view = activity.findViewById(android.R.id.content);
if (view instanceof ViewGroup)
((ViewGroup) view).addView(guideLayout, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
}
使用示例
private void showGuidePopupView(final String key) {
final int showCount = SharePreferencesUtils.getInstance().getInt(key, 0);
if (showCount >= 3)
return;
ImageView imageView = new ImageView(this);
imageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
imageView.setImageResource(R.drawable.hand_o_up);
//
LinearLayout layout = new LinearLayout(this);
layout.setOrientation(LinearLayout.VERTICAL);
layout.setGravity(Gravity.CENTER_HORIZONTAL);
//
TextView textView = new TextView(this);
textView.setTextColor(Color.WHITE);
textView.setLineSpacing(0, 1.2f);
textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
textView.setText("点击可查看相关组件运行效果哦!");
layout.addView(textView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
//
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.topMargin = CompatResourceUtils.getDimensionPixelSize(this, R.dimen.space_16);
Button button = new Button(this);
button.setText("我知道了");
layout.addView(button, params);
new GuidePopupView(this)
.setMinRippleSize(WindowUtils.getActionBarSize(this))
.setMaxRippleSize(WindowUtils.getActionBarSize(this))
.setyOffset(WindowUtils.getStatusBarHeight(this) + WindowUtils.getActionBarSize(this))
.setBackgroundColor(0xB3000000)
.setOnRippleViewUpdateLocationCallback(new GuideLayout.OnRippleViewUpdateLocationCallback() {
@Override
public void onRippleViewUpdateLocation(@NonNull GuideRippleView rippleView) {
//可以根据你自己的需求修改提示动画的相关设置
int color = CompatResourceUtils.getColor(rippleView.getContext(), R.color.colorAccent);
rippleView.initCirculars(3, new int[]{color, color, color}, 20, .7f);
}
})
.attachTarget(recyclerView.getChildAt(4))
.removeAllCustomView()
.addCustomView(imageView, new GuideLayout.OnAddCustomViewCallback<ImageView>() {
@Override
public void onAddCustomView(GuideLayout guideLayout, @Nullable ImageView customView, @NonNull Rect targetRect) {
GuideLayout.LayoutParams params = new GuideLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.gravity = Gravity.END;
params.rightMargin = targetRect.width() / 2 - 10;
params.topMargin = targetRect.bottom + 12;
guideLayout.addView(customView, params);
ObjectAnimator animator = ObjectAnimator.ofFloat(customView, View.TRANSLATION_Y, 0, 32, 0)
.setDuration(1200);
animator.setRepeatCount(-1);
animator.start();
}
})
.addCustomView(layout, new GuideLayout.OnAddCustomViewCallback<LinearLayout>() {
@Override
public void onAddCustomView(GuideLayout guideLayout, @Nullable LinearLayout customView, @NonNull Rect targetRect) {
GuideLayout.LayoutParams params = new GuideLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.gravity = Gravity.CENTER_HORIZONTAL;
params.topMargin = targetRect.bottom + CompatResourceUtils.getDimensionPixelSize(guideLayout, R.dimen.space_64);
guideLayout.addView(customView, params);
}
})
.addTargetClickListener(new OnCustomClickListener() {
@Override
public void onCustomClick(@NonNull View view) {
int count = SharePreferencesUtils.getInstance().getInt(key, 0) + 1;
SharePreferencesUtils.getInstance().saveInt(key, count);
Intent mIntent = new Intent();
mIntent.setClass(MainActivity.this, CustomToastActivity.class);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mIntent.putExtra("transition", TransitionEnum.SLIDE.getLabel());
startActivity(mIntent, ActivityOptions.makeSceneTransitionAnimation(MainActivity.this).toBundle());
} else {
startActivity(mIntent);
}
}
})
.addCustomClickListener(button, null, true)
.show(this);
}
- 2、PopupWindow方式GuidePopupWindow。
原理:看小标题就知道了,这是一个PopupWindow。
/**
* @param context context
* @param yOffset offset from top
* @param minRippleSize minimum ripple size
* @param maxRippleSize max ripple size
*/
public GuidePopupWindow(Context context, int yOffset, int minRippleSize, int maxRippleSize) {
this.minRippleSize = Math.min(minRippleSize, maxRippleSize);
this.maxRippleSize = Math.max(minRippleSize, maxRippleSize);
this.yOffset = yOffset;
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
guideLayout = new GuideLayout(context);
guideLayout.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
mPopupWindow = new PopupWindow();
mPopupWindow.setContentView(guideLayout);
mPopupWindow.setWidth(metrics.widthPixels);
mPopupWindow.setHeight(metrics.heightPixels);
mPopupWindow.setFocusable(false);
mPopupWindow.setOutsideTouchable(true);
}
使用示例:
private void showGuidePopupWindow(final String key) {
final int showCount = SharePreferencesUtils.getInstance().getInt(key, 0);
if (showCount < 3) {
ImageView imageView = new ImageView(this);
imageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
imageView.setImageResource(R.drawable.hand_o_up);
//
LinearLayout layout = new LinearLayout(this);
layout.setOrientation(LinearLayout.VERTICAL);
layout.setGravity(Gravity.CENTER_HORIZONTAL);
//
TextView textView = new TextView(this);
textView.setTextColor(Color.WHITE);
textView.setLineSpacing(0, 1.2f);
textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
textView.setText("点击闪烁按钮可检测\n是否有版本更新哦!");
layout.addView(textView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
//
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.topMargin = CompatResourceUtils.getDimensionPixelSize(this, R.dimen.space_16);
Button button = new Button(this);
button.setText("我知道了");
layout.addView(button, params);
guidePopupWindow = new GuidePopupWindow(this)
.setMinRippleSize(WindowUtils.getActionBarSize(this))
.setMaxRippleSize(WindowUtils.getActionBarSize(this))
.setBackgroundColor(0xB3000000)
.setOnRippleViewUpdateLocationCallback(new GuideLayout.OnRippleViewUpdateLocationCallback() {
@Override
public void onRippleViewUpdateLocation(@NonNull GuideRippleView rippleView) {
//可以根据你自己的需求修改提示动画的相关设置
int color = CompatResourceUtils.getColor(rippleView.getContext(), R.color.colorAccent);
rippleView.initCirculars(3, new int[]{color, color, color}, 20, .7f);
}
})
.attachTarget(getActionMenuView())
.removeAllCustomView()
.addCustomView(imageView, new GuideLayout.OnAddCustomViewCallback<ImageView>() {
@Override
public void onAddCustomView(GuideLayout guideLayout, @Nullable ImageView customView, @NonNull Rect targetRect) {
GuideLayout.LayoutParams params = new GuideLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.gravity = Gravity.END;
params.rightMargin = targetRect.width() / 2 - 10;
params.topMargin = targetRect.bottom + 12;
guideLayout.addView(customView, params);
ObjectAnimator animator = ObjectAnimator.ofFloat(customView, View.TRANSLATION_Y, 0, 32, 0)
.setDuration(1200);
animator.setRepeatCount(-1);
animator.start();
}
})
.addCustomView(layout, new GuideLayout.OnAddCustomViewCallback<LinearLayout>() {
@Override
public void onAddCustomView(GuideLayout guideLayout, @Nullable LinearLayout customView, @NonNull Rect targetRect) {
GuideLayout.LayoutParams params = new GuideLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.gravity = Gravity.CENTER;
guideLayout.addView(customView, params);
}
})
.addTargetClickListener(new OnCustomClickListener() {
@Override
public void onCustomClick(@NonNull View view) {
int count = SharePreferencesUtils.getInstance().getInt(key, 0) + 1;
SharePreferencesUtils.getInstance().saveInt(key, count);
loadVersionInfo();
}
})
.addCustomClickListener(button, null, true);
guidePopupWindow.show();
}
}
- 3、Dialog方式GuideDialog。
原理:小标题说明这是一个Dialog。
/**
* @param context context
* @param yOffset offset from top
* @param minRippleSize minimum ripple size
* @param maxRippleSize max ripple size
*/
public GuideDialog(Context context, int yOffset, int minRippleSize, int maxRippleSize) {
super(context);
supportRequestWindowFeature(Window.FEATURE_NO_TITLE);
this.minRippleSize = Math.min(minRippleSize, maxRippleSize);
this.maxRippleSize = Math.max(minRippleSize, maxRippleSize);
this.yOffset = yOffset;
guideLayout = new GuideLayout(getContext());
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(guideLayout, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
//设置window背景,默认的背景会有Padding值,不能全屏。当然不一定要是透明,你可以设置其他背景,替换默认的背景即可。
getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
//一定要在setContentView之后调用,否则无效
getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
}
使用示例:
private void showGuideDialog(final String key) {
final int showCount = SharePreferencesUtils.getInstance().getInt(key, 0);
if (showCount >= 3)
return;
ImageView imageView = new ImageView(this);
imageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
imageView.setImageResource(R.drawable.hand_o_up);
//
LinearLayout layout = new LinearLayout(this);
layout.setOrientation(LinearLayout.VERTICAL);
layout.setGravity(Gravity.CENTER_HORIZONTAL);
//
TextView textView = new TextView(this);
textView.setTextColor(Color.WHITE);
textView.setLineSpacing(0, 1.2f);
textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
textView.setText("点击可查看相关组件运行效果哦!");
layout.addView(textView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
//
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.topMargin = CompatResourceUtils.getDimensionPixelSize(this, R.dimen.space_16);
Button button = new Button(this);
button.setText("我知道了");
layout.addView(button, params);
new GuideDialog(this)
.setCanceledOnTouchOutside1(false)
.setCancelable1(false)
.setMinRippleSize(WindowUtils.getActionBarSize(this))
.setMaxRippleSize(WindowUtils.getActionBarSize(this))
// .setBackgroundColor(0xB3000000)
.setOnRippleViewUpdateLocationCallback(new GuideLayout.OnRippleViewUpdateLocationCallback() {
@Override
public void onRippleViewUpdateLocation(@NonNull GuideRippleView rippleView) {
//可以根据你自己的需求修改提示动画的相关设置
int color = CompatResourceUtils.getColor(rippleView.getContext(), R.color.colorAccent);
rippleView.initCirculars(3, new int[]{color, color, color}, 20, .7f);
}
})
.attachTarget(recyclerView.getChildAt(4))
.removeAllCustomView()
.addCustomView(imageView, new GuideLayout.OnAddCustomViewCallback<ImageView>() {
@Override
public void onAddCustomView(GuideLayout guideLayout, @Nullable ImageView customView, @NonNull Rect targetRect) {
GuideLayout.LayoutParams params = new GuideLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.gravity = Gravity.END;
params.rightMargin = targetRect.width() / 2 - 10;
params.topMargin = targetRect.bottom + 12;
guideLayout.addView(customView, params);
ObjectAnimator animator = ObjectAnimator.ofFloat(customView, View.TRANSLATION_Y, 0, 32, 0)
.setDuration(1200);
animator.setRepeatCount(-1);
animator.start();
}
})
.addCustomView(layout, new GuideLayout.OnAddCustomViewCallback<LinearLayout>() {
@Override
public void onAddCustomView(GuideLayout guideLayout, @Nullable LinearLayout customView, @NonNull Rect targetRect) {
GuideLayout.LayoutParams params = new GuideLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.gravity = Gravity.CENTER_HORIZONTAL;
params.topMargin = targetRect.bottom + CompatResourceUtils.getDimensionPixelSize(guideLayout, R.dimen.space_64);
guideLayout.addView(customView, params);
}
})
.addTargetClickListener(new OnCustomClickListener() {
@Override
public void onCustomClick(@NonNull View view) {
int count = SharePreferencesUtils.getInstance().getInt(key, 0) + 1;
SharePreferencesUtils.getInstance().saveInt(key, count);
Intent mIntent = new Intent();
mIntent.setClass(MainActivity.this, CustomToastActivity.class);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mIntent.putExtra("transition", TransitionEnum.SLIDE.getLabel());
startActivity(mIntent, ActivityOptions.makeSceneTransitionAnimation(MainActivity.this).toBundle());
} else {
startActivity(mIntent);
}
}
})
.addCustomClickListener(button, null, true)
.show();
}
- 4、WindowManager添加View方式WindowManager。
此种方式有很多坑,不建议用此方式。
原理:
WindowManager manager = activity.getWindowManager();
manager.addView(View view, ViewGroup.LayoutParams params);
童鞋们给个💕吧!用起来也很简单!童鞋们不要被我一大堆示例代码吓着了。
篇尾
Wechat:eoy9527
。
在人类历史的长河中,真理因为像黄金一样重,总是沉于河底而很难被人发现,相反地,那些牛粪一样轻的谬误倒漂浮在上面到处泛滥。 —— 培根
网友评论