Android开发之高亮引导

看下图,今天的任务就是它了,app 的高亮引导的实现,找到几个github上面已经实现的库,下载下来源码对比分析实现原理,整理自己的知识体系。下面是其中一个的效果图(我用DialogFragment实现了引导但是并没有做高亮实现,补充说明一点:该篇博客最好配合源码对比查看,不然感觉有那么点抽象,不知所云)

下面是找到的四个库的链接地址

  • ShowcaseView

  • TourGuide

  • Highlight

  • ShowTipsView

ShowcaseView

Android开发之高亮引导_第1张图片

通过android studio导出shape包下uml不难发现,这些定制shape就是为高亮显示用的shape,updateTarget方法定义用于更新视图。

 /**
  * Update shape bounds if necessary
  */
 void updateTarget(Target target);

NoShape仅仅是实现了Shape,见名知其意,使用NoShape则不需要高亮显示,RectangleShape则矩形高亮,CircleShape圆形高亮,RectangleShape内部定义adjustToTarget变量用于判断是否调整范围,至于这里的onDraw没什么好说的,略过

 @Override
    public void updateTarget(Target target) {
        if (adjustToTarget) {
            Rect bounds = target.getBounds();
            height = bounds.height();
            if (fullWidth)
                width = Integer.MAX_VALUE;
            else width = bounds.width();
            init();
        }
    }

CircleShape相比较于RectangleShape的区别在于一个是用rect一个是radius,一个drawRect一个drawCircle,updateTaget方法也有所不同

  @Override
  public void updateTarget(Target target) {
      if (adjustToTarget)
         radius = getPreferredRadius(target.getBounds());
  }

  public static int getPreferredRadius(Rect bounds) {
      return Math.max(bounds.width(), bounds.height()) / 2;
  }

updateTarget方法都有用到Taget,这里的Taget只是一个范围测量目的new 出一个Rect,而MaterialShowcaseView自定义控件使用到的是Taget的实现类ViewTarget,内部实现代码比较简单,就我个人而言可以get到一点

// 获取在当前窗口内的绝对坐标
View.getLocationInWindow()  

// 获取在整个屏幕内的绝对坐标,注意这个值是要从屏幕顶端算起,也就是包括了通知栏的高度。
View.getLocationOnScreen()     

AnimationFactory 和 IAnimationFactory是关于View动画相关的定制,主要是fadeInView 和fadeOutView效果,供MaterialShowcaseView调用,IShowcaseListener和IDetachedListener定义的接口回调函数,ShowcaseConfig提供了初始化MaterialShowcaseView的基本配置,默认遮盖层颜色dd335075,默认高亮显示图形CircleShape,PrefsManager类只是一个简单的数据缓存辅助类

MaterialShowcaseSequence类提供了addSequenceItem和start 、showNextItem显示引导View视图的方法,addSequenceItem方法通过MaterialShowcaseView的Buidler构建实例,并根据上面提到的ShowcaseConfig非空进行初始化MaterialShowcaseView配置

 public MaterialShowcaseSequence addSequenceItem(View targetView, String title, String content, String dismissText) {

        MaterialShowcaseView sequenceItem = new MaterialShowcaseView.Builder(mActivity)
                .setTarget(targetView)
                .setTitleText(title)
                .setDismissText(dismissText)
                .setContentText(content)
                .build();

        if (mConfig != null) {
            sequenceItem.setConfig(mConfig);
        }

        mShowcaseQueue.add(sequenceItem);
        return this;
    }

这个类我们要理解透彻有必要了解这个类的具体用法:Queue< E >,当我第一次看到代码内部调用下面这些方法简直了一头雾水,没搞懂他具体实现在哪里,调用这些接口定义得方法何用

 mShowcaseQueue.add(sequenceItem);

 mShowcaseQueue.poll();

下来看看接口Queue< E >的具体定义


public interface Queue extends Collection {
    /**
     * 将指定的元素插入到该队列中,如果由于容量限制,如法插入成功返回false,插入队列数据不能为空
     */
    boolean add(E e);

    /**
     *如果不违反容量限制的话,将指定的元素插入到该队列中当使用容量限制该方法通常是最好的add方法添加,
     *能不能插入一个元素如果不能则通过抛出一个异常。
     *如果添加类型不对,抛出ClassCastException,防止它被添加到该队列
     *抛出NullPointerException异常如果指定元素为null,此队列不允许空元素
     */
    boolean offer(E e);

    /**
     *检索并删除该队列的头
     */
    E remove();

    /**
     *检索并删除该队列的头,如果该队列为空,则返回Null。
     */
    E poll();

    /**
     *检索,但不删除此队列的头。
     *抛出NoSuchElementException,如果队列为空
     */
    E element();

    /**
     *检索,但不删除此队列的头,如果该队列为空,则返回Null
     */
    E peek();
}

LinkedList对Queue做了具体实现,所以在MaterialShowcaseSequence构造函数里面实例Queue实例的是一个LinkedList,瞬间感觉上面方法的调用不再那么抽象了吧,这种感觉有木有??好吧原谅我吧,是我java基础不牢!!


 public MaterialShowcaseSequence(Activity activity) {
        mActivity = activity;
        mShowcaseQueue = new LinkedList<>();
    }

public class LinkedList<E> extends AbstractSequentialList<E> implements
        List<E>, Deque<E>, Queue<E>, Cloneable, Serializable {

          //.......略,详情参考源码...........
}

MaterialShowcaseSequence 调用start方法 之后poll方法干掉了队列里面当前的view,再次判断队列是否还有view有就需要上面提到的showNextItem方法,间接调用了接口回调和show方法,定制动画也在这里被调用,具体请参考源码。

最后再来一观MaterialShowcaseView,内部定义Builder构造函数初始化MaterialShowcaseView实例,提供基本属性配置,builder根据已配置属性选择shape,Builder模式的运用实在不想再说,就这样吧,这个自定义控件用到的核心有两点:addOnGlobalLayoutListener和PorterDuff.Mode,视图层级变化引起onGlobalLayout回调到setTaget,从而达到调整高亮显示位置变化,而高亮显示部分的绘制核心利用画笔Xfermode,下列是onDraw绘制核心代码

 @Override
 protected void onDraw(Canvas canvas) {
      super.onDraw(canvas);

      //........略过图片资源回收和条件判断...................

      // clear canvas
      mCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);

      // draw solid background
      mCanvas.drawColor(mMaskColour);

      // Prepare eraser Paint if needed
      if (mEraser == null) {
          mEraser = new Paint();
          mEraser.setColor(0xFFFFFFFF);
          mEraser.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
          mEraser.setFlags(Paint.ANTI_ALIAS_FLAG);
      }

      // draw (erase) shape
      mShape.draw(mCanvas, mEraser, mXPosition, mYPosition, mShapePadding);

      // Draw the bitmap on our views  canvas.
      canvas.drawBitmap(mBitmap, 0, 0, null);
  }

还有一些方法块的定义方便我们管理ShowcaseView的状态,以便以控制下次是否还弹出高亮引导显示resetSingleUse和resetAll,不得不感叹,一个Mode可以玩出这么多花样!!!


TourGuide 、ShowTipsView

这个库的实现原理一模一样,这个库的动画效果更好,还加了一个缩放的高亮指示动画,但是就代码层次而言,仅支持圆形高亮,太单一了,需求变更还得自己修改,代码的逻辑层次感觉没ShowcaseView库的清晰,效果图就不贴了,有兴趣自己下载运行,我果断放弃该库..

ShowTipsView库相比较于TourGuide要好不少,代码层次也清晰了不少,同样的实现原理同样的builder模式,保留意见(相比较于ShowcaseView,还是更喜欢前者,人就这样对“第一次”总是有那么一种难以言语的情绪)

Hightlight

洪洋的Hightlight库支持同时高亮显示多个View,定制显示布局,simple效果用的ImageView,效果非常漂亮当然你也可以用其他布局定制开发,下面是效果图:

Android开发之高亮引导_第2张图片

代码调用实例

 private void showTipMask()
    {
        mHightLight = new HighLight(MainActivity.this)//
                .anchor(findViewById(R.id.id_container))//如果是Activity上增加引导层,不需要设置anchor
        .addHighLight(R.id.id_btn_important_right,R.layout.info_gravity_right_up, new HighLight.OnPosCallback(){


            @Override
            public void getPos(float rightMargin, float bottomMargin, RectF rectF, HighLight.MarginInfo marginInfo) {
                marginInfo.rightMargin = rightMargin;
                marginInfo.topMargin = rectF.top + rectF.height();
            }
        })
        .addHighLight(R.id.id_btn_whoami, R.layout.info_gravity_left_down, new HighLight.OnPosCallback() {


            @Override
            public void getPos(float rightMargin, float bottomMargin, RectF rectF, HighLight.MarginInfo marginInfo) {
                marginInfo.leftMargin = rectF.right - rectF.width()/2;
                marginInfo.bottomMargin = bottomMargin + rectF.height();
            }
        })
        .setClickCallback(new HighLight.OnClickCallback() {
            @Override
            public void onClick() {
                Toast.makeText(MainActivity.this,"clicked",Toast.LENGTH_SHORT).show();
            }
        });

        mHightLight.show();
    }

这个库的内部实现原理也有点点区别,在layout调用buildMask方法得到需要显示 的bitmap,最后ondraw直接绘制bitmap,代码层次上面来说这个做法非常地PL,Xfermode也不同其他几个库PorterDuff.Mode.DST_OUT(貌似其他库都用CLEAR)

 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);
        mHighLight.updateInfo();
        for (HighLight.ViewPosInfo viewPosInfo : mViewRects)
        {
            canvas.drawRoundRect(viewPosInfo.rectF, DEFAULT_RADIUS, DEFAULT_RADIUS, mPaint);
        }
    }

小结

看了几个库还是做个小结,如果开发使用一个界面多个高亮引导或者要定制引导视图建议用Hightlight库,如果使用纯文字相关元素高亮引导,建议用ShowcaseView库,这块的知识点涉及到两点:画笔设置Xfermode和 ViewTreeObserver.OnGlobalLayoutListener,重新了解了Queue与MessageQueue相关的知识。

参考资料

http://blog.csdn.net/imyfriend/article/details/8564781

你可能感兴趣的:(Android)