看下图,今天的任务就是它了,app 的高亮引导的实现,找到几个github上面已经实现的库,下载下来源码对比分析实现原理,整理自己的知识体系。下面是其中一个的效果图(我用DialogFragment实现了引导但是并没有做高亮实现,补充说明一点:该篇博客最好配合源码对比查看,不然感觉有那么点抽象,不知所云)
下面是找到的四个库的链接地址
ShowcaseView
TourGuide
Highlight
ShowTipsView
通过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可以玩出这么多花样!!!
这个库的实现原理一模一样,这个库的动画效果更好,还加了一个缩放的高亮指示动画,但是就代码层次而言,仅支持圆形高亮,太单一了,需求变更还得自己修改,代码的逻辑层次感觉没ShowcaseView库的清晰,效果图就不贴了,有兴趣自己下载运行,我果断放弃该库..
ShowTipsView库相比较于TourGuide要好不少,代码层次也清晰了不少,同样的实现原理同样的builder模式,保留意见(相比较于ShowcaseView,还是更喜欢前者,人就这样对“第一次”总是有那么一种难以言语的情绪)
洪洋的Hightlight库支持同时高亮显示多个View,定制显示布局,simple效果用的ImageView,效果非常漂亮当然你也可以用其他布局定制开发,下面是效果图:
代码调用实例
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