美文网首页自定义控件android练习有意思的东西
原来新手引导ShowCase这么简单就可以实现了

原来新手引导ShowCase这么简单就可以实现了

作者: 皮球二二 | 来源:发表于2017-07-05 23:26 被阅读721次

    最近有一个需求,要求在app首页添加新手引导功能。如此机智的我一下子就想到了showcase这关键字,于是从github上一搜就找到一个700+star的FancyShowCaseView。那么我们今天就以它为基础,去实现自己的ShowCaseView
    我自己写的精简版也同步放出ShowCaseView

    效果展示

    效果1 效果2

    原理说明

    简单的看一下,showcase是悬浮在最上层同时还包含镂空高亮的区域的一个View。我们来拆解一下关键点:

    1. 悬浮在最高层:DecorView为整个Window界面的最顶层View,我们的自定义视图无疑要添加到DevorView上
    2. 镂空高亮:就是画一个新图层,然后两图层交集部分变成全透明,这个无疑要用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;
    }
    

    相关文章

      网友评论

      • GYLEE:请问为高亮加点击事件,在activity中直接添加,绘制后该点击时间还有效吗
        皮球二二: @BadMonkey 不冲突的
        GYLEE:@r17171709 求教: ShowCaseImageView 继承 ImageView ,那么最后这个自定义view应该是一张图片,这样的话还怎么捕获高亮部分的点击事件?
        皮球二二:高亮事件需要你添加到自定义View上,然后实现onTouch回调。activity需要把接口传到View里面去便于调用
      • GYLEE:现在高亮不支持点击吗
        皮球二二: @BadMonkey 好的
        GYLEE: @r17171709 嗯嗯,好的我去实现一下,有不明白的地方可以问你吗
        皮球二二: @BadMonkey 没有,你可以自己添加
      • 奋斗小青年Jerome:原理讲的比较好,晚点试一试
      • 陆地蛟龙:今晚尝试尝试
      • 请叫我四爷:直接做出来图片,不更简单?
        皮球二二: @请叫我四爷 是更简单,但是考虑到多dpi拉伸图片以及高亮位置会有偏差,还是拆开来做比较好

      本文标题:原来新手引导ShowCase这么简单就可以实现了

      本文链接:https://www.haomeiwen.com/subject/uyqchxtx.html