在我们开发一个新的应用程序或者对一个应用程序进行迭代改动较大时,大多数APP都会在用户第一次使用这些新功能的时候,通过一定的方法来告诉、指导用户发现、使用这些新的功能,而这个方法就是操作引导。通常情况下,我们见到的APP中出现的操作引导大概分为2种,一种就是在第一次打开应用的时候首先进入的不是首页,而是一个引导页面,这个引导页面大多数情况下是一个ViewPager,然后通过几张图片做一个操作引导;另外一种方式就是引导用户使用某一个功能,基本表现形式就是当用户第一次进入到一个包含新功能的页面时,在页面上覆盖一层包含功能说明的阴影,然后将功能按钮或图标高亮显示。今天这篇博客提供的是第二种操作引导。
首先来一张图,看一下效果:
实现上面效果图的代码已经上传,优快云下载 Github下载。这是一个Android studio项目,下载之后直接使用Android studio打开即可,不要导入。下载的文件包含一个HeightLight库和一个使用实例,直接运行实例就能得到以上效果。
在库文件中主要包含了几个类,对View进行操作的工具类,继承至FrameLayout的高亮显示的核心类和一个提供给开发者使用的类(这个类避免开发者直接操作核心类,所有的属性通过这个类在传递给核心类),最后还提供了一个打印Log的工具类。
操作View的工具类:
public class ViewUtils {
...
private OnViewClickListener clickLisstener;
/**
* 设置点击监听
*
* @param clickLisstener
*/
public void setOnViewClickListener(OnViewClickListener clickLisstener) {
this.clickLisstener = clickLisstener;
}
...
/**
* 在整个窗体上面增加一层布局
*
* @param layoutId 布局id
*/
public void addView(int layoutId) {
final View view = View.inflate(mActivity, layoutId, null);
FrameLayout frameLayout = (FrameLayout) getRootView();
frameLayout.addView(view);
// 设置整个布局的单击监听
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
...
}
});
}
/**
* 移除View
*
* @param view 需要移出的视图
*/
public void removeView(View view) {
FrameLayout frameLayout = (FrameLayout) getRootView();
frameLayout.removeView(view);
}
/**
* 获取子View在父View中的位置
*
* @param parent 父View
* @param child 子View
* @return Rect对象
*/
public Rect getLocationInView(View parent, View child) {
...
}
/**
* 单击视图监听,用于多个引导页面时连续调用
*/
public interface OnViewClickListener {
/**
* 单击监听回调
*
* @param view
*/
void onClick(View view);
}
}
这个类当中封装了一些对View操作的方法,不仅可以在这里使用,在其他需要对View进行操作的地方同样可以使用。
下面是核心类中的一些方法:
public HightLightView(Context context, HighLight highLight, int maskColor, List<HighLight.ViewPosInfo> viewRects) {
super(context);
...
setWillNotDraw(false);
init();// 初始化参数
}
/**
* 初始化一些配置参数
*/
private void init() {
...
// 初始化虚线的样式
intervals = new float[]{dip2px(4), dip2px(4)};
}
/**
* 将需要高亮的View增加到帧布局上方
*/
private void addViewForTip() {
for (HighLight.ViewPosInfo viewPosInfo : mViewRects) {
View view = mInflater.inflate(viewPosInfo.layoutId, this, false);
FrameLayout.LayoutParams lp = buildTipLayoutParams(view,
...
addView(view, lp);
}
}
/**
* 绘制高亮区域
*/
private void buildMask() {
mMaskBitmap = Bitmap.createBitmap(getMeasuredWidth(),getMeasuredHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(mMaskBitmap);
canvas.drawColor(maskColor);
mPaint.setXfermode(MODE_DST_OUT);
mPaint.setColor(Color.parseColor("#00000000"));
if (isBlur)
mPaint.setMaskFilter(new BlurMaskFilter(this.blurSize,BlurMaskFilter.Blur.SOLID));
mHighLight.updateInfo();
for (HighLight.ViewPosInfo viewPosInfo : mViewRects) {
if (viewPosInfo.myShape != null) {
switch (viewPosInfo.myShape) {
case CIRCULAR:// 圆形
float width = viewPosInfo.rectF.width();
float height = viewPosInfo.rectF.height();
float circle_center1;
float circle_center2;
double radius = Math.sqrt(Math.pow(width / 2, 2)+ Math.pow(height / 2, 2));
circle_center1 = width / 2;
circle_center2 = height / 2;
canvas.drawCircle(viewPosInfo.rectF.right - circle_center1,
viewPosInfo.rectF.bottom - circle_center2,
(int) radius, mPaint);
if (isNeedBorder)
drawCircleBorder(canvas, viewPosInfo, circle_center1, circle_center2, (int) radius);
break;
case RECTANGULAR:
canvas.drawRoundRect(viewPosInfo.rectF, this.radius,this.radius, mPaint);
if (isNeedBorder) drawRectBorder(canvas, viewPosInfo);
break;
default:
break;
}
} else {
canvas.drawRoundRect(viewPosInfo.rectF, this.radius,this.radius, mPaint);
if (isNeedBorder) drawRectBorder(canvas, viewPosInfo);
}
}
}
/**
* 绘制圆形边框
*
* @param canvas
* @param viewPosInfo
* @param circle_center1
* @param circle_center2
* @param radius
*/
private void drawCircleBorder(Canvas canvas, HighLight.ViewPosInfo viewPosInfo, float circle_center1, float circle_center2, int radius) {
...
}
/**
* 绘制矩形边框
*
* @param canvas
* @param viewPosInfo
*/
private void drawRectBorder(Canvas canvas, HighLight.ViewPosInfo viewPosInfo) {
...
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
measureChildren(
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),//
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
setMeasuredDimension(width, height);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right,int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (changed) {
buildMask();
updateTipPos();
}
}
private void updateTipPos() {
for (int i = 0, n = getChildCount(); i < n; i++) {
View view = getChildAt(i);
HighLight.ViewPosInfo viewPosInfo = mViewRects.get(i);
LayoutParams lp = buildTipLayoutParams(view, viewPosInfo);
if (lp == null)
continue;
view.setLayoutParams(lp);
}
}
private LayoutParams buildTipLayoutParams(View view, HighLight.ViewPosInfo viewPosInfo) {
LayoutParams lp = (LayoutParams) view.getLayoutParams();
...
return lp;
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawBitmap(mMaskBitmap, 0, 0, null);
super.onDraw(canvas);
}
核心类是一个继承至FrameLayout的视图类,它主要的操作
1.把自定义的xml引导说明布局文件通过inflate()变为View对象,然后添加到FrameLayout上;
2.重写onDraw()方法,绘制半透明背景,然后根据设置的属性在需要高亮的控件周围绘制全透明的指定形状达到高亮效果。
在提供给用户操作的类当中,首先封装了一些枚举和接口:
/**
* 表示需要高亮的形状,圆形、矩形
*/
public enum MyShape {
/**
* 圆形
*/
CIRCULAR,
/**
* 矩形
*/
RECTANGULAR
}
/**
* 边框的样式,实线、虚线
*/
public enum MyType {
/**
* 实线
*/
FULL_LINE,
/**
* 虚线
*/
DASH_LINE
}
/**
* 封装了需要高亮View的信息
*/
public static class ViewPosInfo {
public int layoutId = -1;
public RectF rectF;
public MarginInfo marginInfo;
public View view;
public OnPosCallback onPosCallback;
public MyShape myShape;
}
/**
* 封装了左上右下的边距
*/
public static class MarginInfo {
public float topMargin;
public float leftMargin;
public float rightMargin;
public float bottomMargin;
}
/**
* 增加高亮View的回调
*/
public interface OnPosCallback {
/**
* 增加高亮View的回调方法,封装了高亮View的位置信息
*
* @param rightMargin
* @param bottomMargin
* @param rectF
* @param marginInfo
*/
void getPos(float rightMargin, float bottomMargin, RectF rectF, MarginInfo marginInfo);
}
/**
* 点击回调接口
*/
public interface OnClickCallback {
/**
* 点击回调方法,要想点击有效果,必须设置intercept为TRUE
*/
void onClick();
}
这些枚举主要是用于指定相关属性和提供回调。
然后提供了一些方法用来设置属性(设置属性的方法不贴出来了),提供了一些重载方法用来增加高亮区域的布局:
/**
* 增加高亮的布局
*
* @param viewId 需要高亮的控件id
* @param decorLayoutId 布局文件资源id
* @param onPosCallback 回调,用于设置位置
* @return
*/
public HighLight addHighLight(int viewId, int decorLayoutId, OnPosCallback onPosCallback) {
ViewGroup parent = (ViewGroup) mAnchor;
View view = parent.findViewById(viewId);
addHighLight(view, decorLayoutId, onPosCallback);
return this;
}
这里只贴出来了一个简单的方法,更多的重载方法可以 优快云下载 Github下载 下载源码查看,这个方法里面会通过空间的id找到控件的位置,将位置封装到ViewPosInfo中,并把所有需要高亮显示的控件位置信息添加到mViewRects集合中保存起来,然后在核心类当中就可以通过ViewPosInfo 中的位置信息来绘制高亮区域。
最后如果需要显示引导,需要调用show()方法:
/**
* 显示含有高亮区域的页面
*/
public void show() {...}
更多的代码和使用案例的代码就不在贴出来的,可以直接优快云下载 Github下载查看。