最近有一个需求,要求在app首页添加新手引导功能。如此机智的我一下子就想到了showcase这关键字,于是从github上一搜就找到一个700+star的FancyShowCaseView。那么我们今天就以它为基础,去实现自己的ShowCaseView
我自己写的精简版也同步放出ShowCaseView
效果展示
效果1 效果2原理说明
简单的看一下,showcase是悬浮在最上层同时还包含镂空高亮的区域的一个View。我们来拆解一下关键点:
- 悬浮在最高层:DecorView为整个Window界面的最顶层View,我们的自定义视图无疑要添加到DevorView上
- 镂空高亮:就是画一个新图层,然后两图层交集部分变成全透明,这个无疑要用PorterDuffXfermode的CLEAR实现
如果你说我早就想到了,那么OK,后面的文章你就可以不用看了,因为这玩意就是这么简单
实现镂空的ImageView
其实不一定非要是ImageView,任意View在onDraw方法里面都能达到相应的效果
该ImageView只具备镂空效果,所以这里只涉及到普通绘制部分。首先声明三个变量,这三个变量代表我们ImageView上的任意元素所使用到的Paint
// 设置背景Paint
Paint mBackgroundPaint;
// 设置高亮点清除中心Paint
Paint mErasePaint;
// 设置高亮点Paint
Paint mCircleBorderPaint;
随后在构造方法中进行初始化。mBackgroundPaint就是我们的背景绘制Paint,mErasePaint是镂空绘制Paint,mCircleBorderPaint就是我们上图在镂空分部添加虚线绘制Paint
mBackgroundPaint=new Paint();
mBackgroundPaint.setAntiAlias(true);
mErasePaint=new Paint();
mErasePaint.setAntiAlias(true);
mErasePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
mErasePaint.setAlpha(0xFF);
mCircleBorderPaint=new Paint();
mCircleBorderPaint.setAntiAlias(true);
mCircleBorderPaint.setColor(mFocusBorderColor);
mCircleBorderPaint.setStrokeWidth(mFocusBorderSize);
mCircleBorderPaint.setStyle(Paint.Style.STROKE);
mCircleBorderPaint.setStrokeJoin(Paint.Join.ROUND);
mCircleBorderPaint.setStrokeCap(Paint.Cap.ROUND);
mCircleBorderPaint.setPathEffect(new DashPathEffect(new float[] {10, 20}, 0));
配置完成之后,就是开始画了。在画之前我们先明确一下,镂空图形可以是任意的图形,比如圆形、圆角矩形等,所以这里先用一个枚举来给用户提供绘制选择。这里为了演示,仅提供圆形与圆角矩形2种
public enum FocusShape {
CIRCLE,
ROUNDED_RECTANGLE
}
有了图形之后,我们就要考虑使用者如何将他所希望的图形以对象的形式传递到onDraw()方法里面。这里我们就需要传递一个bean进来。这个bean就包括绘制时所需的各种参数
public class CalculatorBean {
FocusShape mFocusShape;
int mCircleCenterX;
int mCircleCenterY;
int mCircleRadius;
// 圆角矩形专用
int mFocusWidth;
int mFocusHeight;
}
通过set方法传进所有镂空部分的View
public void setmCalculatorBeen(ArrayList<CalculatorBean> mCalculatorBeen) {
this.mCalculatorBeen = mCalculatorBeen;
}
剩下就开始绘制了。先绘制整体的背景色,再完成镂空,并添加一定的点缀
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mBitmap == null) {
mBitmap= Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
// 把图片设置成半透明
mBitmap.eraseColor(0xb2000000);
}
canvas.drawBitmap(mBitmap, 0, 0, mBackgroundPaint);
if (mCalculatorBeen!=null) {
for (CalculatorBean calculatorBean : mCalculatorBeen) {
if (calculatorBean.getmFocusShape()==FocusShape.CIRCLE) {
drawCircle(canvas, calculatorBean);
}
else if (calculatorBean.getmFocusShape()==FocusShape.ROUNDED_RECTANGLE) {
drawRoundedRectangle(canvas, calculatorBean);
}
}
}
}
画圆的方法
private void drawCircle(Canvas canvas, CalculatorBean calculatorBean) {
// 绘制高亮
canvas.drawCircle(calculatorBean.getmCircleCenterX(), calculatorBean.getmCircleCenterY(), calculatorBean.getmCircleRadius()+ mAnimCounter*animMoveFactor, mErasePaint);
// 绘制其余部分
mPath.reset();
mPath.moveTo(calculatorBean.getmCircleCenterX(), calculatorBean.getmCircleCenterY());
mPath.addCircle(calculatorBean.getmCircleCenterX(), calculatorBean.getmCircleCenterY(), calculatorBean.getmCircleRadius()+ mAnimCounter*animMoveFactor, Path.Direction.CW);
canvas.drawPath(mPath, mCircleBorderPaint);
}
画圆角矩形的方法
private void drawRoundedRectangle(Canvas canvas, CalculatorBean calculatorBean) {
// 绘制高亮
int centerX=calculatorBean.getmCircleCenterX();
int centerY=calculatorBean.getmCircleCenterY();
float left=centerX-calculatorBean.getmFocusWidth()/2- mAnimCounter*animMoveFactor;
float top=centerY-calculatorBean.getmFocusHeight()/2- mAnimCounter*animMoveFactor;
float right=centerX+calculatorBean.getmFocusWidth()/2+ mAnimCounter*animMoveFactor;
float bottom=centerY+calculatorBean.getmFocusHeight()/2+ mAnimCounter*animMoveFactor;
canvas.drawRoundRect(new RectF(left, top, right, bottom), calculatorBean.getmCircleRadius(), calculatorBean.getmCircleRadius(), mErasePaint);
// 绘制其余部分
mPath.reset();
mPath.moveTo(calculatorBean.getmCircleCenterX(), calculatorBean.getmCircleCenterY());
mPath.addRoundRect(new RectF(left, top, right, bottom), calculatorBean.getmCircleRadius(), calculatorBean.getmCircleRadius(), Path.Direction.CW);
canvas.drawPath(mPath, mCircleBorderPaint);
}
添加动画效果,这里是通过改变圆形半径或者圆角矩形的长宽来达到动画效果
if (mAnimationEnabled) {
if (mAnimCounter==ANIM_COUNTER_MAX) {
mStep=-1;
}
else if (mAnimCounter==0) {
mStep=1;
}
mAnimCounter+=mStep;
postInvalidate();
}
绘制到DecerView上
我们用队列进行多个引导层的管理,一次性将所需要显示并切换的图层都添加进来
Queue<View> mQueue;
View currentView;
Activity context;
public ShowCaseView(Activity context) {
this.context = context;
}
public void addViews(ArrayList<View> views) {
mQueue=new LinkedList<>();
mQueue.addAll(views);
}
然后就是添加跟移除,这里每次移除完之后都会判断队列中如果还有未展示的,会接着继续展示出来
public void show() {
if (!mQueue.isEmpty()) {
currentView=mQueue.poll();
((ViewGroup) context.getWindow().getDecorView()).addView(currentView);
}
}
public void dismiss() {
if (currentView!=null) {
((ViewGroup) context.getWindow().getDecorView()).removeView(currentView);
}
show();
}
还有一种情况就是直接全部跳过
public void cancel() {
if (!mQueue.isEmpty()) {
mQueue.clear();
}
if (currentView!=null) {
((ViewGroup) context.getWindow().getDecorView()).removeView(currentView);
}
}
正式使用
只要获取到高亮指示的控件的坐标,即可对其进行高亮处理
public View a() {
ArrayList<CalculatorBean> beanArrayList=new ArrayList<>();
int[] location=new int[2];
btn_showcase.getLocationOnScreen(location);
CalculatorBean bean=new CalculatorBean();
bean.setmCircleCenterX(location[0]+btn_showcase.getMeasuredWidth()/2);
bean.setmCircleCenterY(location[1]+btn_showcase.getMeasuredHeight()/2);
bean.setmCircleRadius(150);
bean.setmFocusShape(FocusShape.CIRCLE);
beanArrayList.add(bean);
View view= LayoutInflater.from(ShowcaseActivity.this).inflate(R.layout.view_showcase, null, false);
ShowCaseImageView image_showcase= view.findViewById(R.id.image_showcase);
image_showcase.setmAnimationEnabled(true);
image_showcase.setmCalculatorBeen(beanArrayList);
image_showcase.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
showCaseView.dismiss();
}
});
return view;
}
网友评论