源码:xufangzhen/NewbieGuide
转载请标明出处:http://www.jianshu.com/p/5aa96683d0dc
前言
这个模块写了很早了,在实际项目中更新了很多次,但是太懒了,博客里没有跟着更新,看到阅读人数这么多,在回头看看自己写的代码这么烂,都不好意思了,于是决定更新一下。PS:看了评价里有人说放在fragment里显示会乱,我表示不知道为什么他们会这么想,在fragment显示正常的。
更新(2016-11-22):
- 使用建筑者模式构造引导浮层
- 支持在onCreate()等方法(View还未加载时)中设置并显示引导浮层
- 支持高亮洞洞的额外扩大和缩小
本来在我的项目中使用的新手引导浮层是这个TourGuide开源项目,这个新手引导项目功能比较多,一部分功能用不到,用到的地方只能大体上满足视觉的样式,不能一模一样。因此决定重构一遍,满足自己项目中的新手引导,本人项目中新手引导页的样式如下图所示:
实现的功能
- 选中的view高亮可以有任意多个,形状有矩形,圆形,椭圆形
- 指示箭头或者其他图片可以在任意位置,可以有任意多个
- 文字和我知道了按钮可以在任意位置(默认我知道了在文字下方,两者水平居中,上下可调)
- 点击我知道了引导浮层消失
- 可设置点击任何位置引导浮层消失(默认点击消失)
- 浮层出现和消失可以有回调接口,可以延迟出现
实现的原理
1. 浮层的位置,放在activity的DecorView里,DecorView为FrameLayout的子类。
DecorView为整个Window界面的最顶层View。
DecorView只有一个子元素为LinearLayout,代表整个Window界面。
LinearLayout里有两个FrameLayout子元素,分别是标题栏和内容。
可通过以下代码获取,不清楚可参考这篇文章Android DecorView浅析
mParentView = (FrameLayout) mActivity.getWindow().getDecorView();
2. 引导浮层布局及上面的元素
- 浮层为相对布局,除了高亮的地方和半透明的背景其余都是通过addView的方式添加进去,通过设置margin来调整添加子view的位置。
比如箭头元素的添加,offsetX(offsetY)负数则从右边(下边)开始偏移,CENTER为居中,方便设置具体位置
public NewbieGuide addIndicateImg(int id, int offsetX, int offsetY) {
ImageView arrowImg = new ImageView(mActivity);
arrowImg.setImageResource(id);
mGuideView.addView(arrowImg, getLp(offsetX, offsetY));
return this;
}```
```java
private RelativeLayout.LayoutParams getLp(int offsetX, int offsetY) {
RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(ViewGroup
.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
//水平方向
if (offsetX == CENTER) {
lp.addRule(RelativeLayout.CENTER_HORIZONTAL, RelativeLayout.TRUE);
} else if (offsetX < 0) {
lp.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, RelativeLayout.TRUE);
lp.rightMargin = -offsetX;
} else {
lp.leftMargin = offsetX;
}
//垂直方向
if (offsetY == CENTER) {
lp.addRule(RelativeLayout.CENTER_VERTICAL, RelativeLayout.TRUE);
} else if (offsetY < 0) {
lp.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM, RelativeLayout.TRUE);
lp.bottomMargin = -offsetY;
} else {
lp.topMargin = offsetY;
}
return lp;
}
- 高亮洞洞的绘制
思路一,本人想了一个比较巧妙的方法,拿圆形的高亮洞洞来说,画一个镂空的圆形,你会发现当画笔足够粗到把屏幕都遮起来的时候,刚好中心的小圆没有画到是高亮的,这个方法只需要调整好画笔的粗度和圆形的直径就可以了,矩形也是可垟,调整好边长和画笔的粗度就可以实现了,不过最后发现椭圆是不行的, 所以总结下这个方法只能一个高亮洞洞,而且只能圆形或矩形。
所以最后还是使用和TourGuide同样的方法,通过画笔的setXfermode来实现,即当两个画布上都绘制了图片是,可以控制最终显示的样式,有取重叠部分,有去除重叠部分的等等,这个有16中规则,具体下图:
具体用法可以参考两篇文章 Android中Xfermode简单用法和详解Paint的setXfermode。简单地说,TourGuide做法就是在一个画布上画了一个屏幕大小背景,在另一个画布上画了一个圆形,因为重叠了,所以去除了重叠的部分,高亮洞洞就显示出来了。
本人针对这个做法做了点优化,即画了一个和洞洞所需要一样大小的图片而不是屏幕一样大小,如果有两个洞洞,则画了一个能同时容下两个洞洞的矩形大小的图片,这样显示的结果最终会变成下面这样:
高亮洞洞都显示出来了,但是只画了一部分的背景,首先这样做的目的是为了用较少的内存去完成(一个1080p屏幕大小的半透明的背景图bitmap大概需要3M以上)那剩余的空白部分该怎么填充呢,一个方法是用四个view去填充上,这个做法是可行的,但是麻烦,其实一个比较简单的做法我已经在上面提到了,就是思路一中说的画一个矩形,调整好画笔的粗度和长宽即可填充完,关键代码如下。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if(mHoleList != null && mHoleList.size() > 0) {
mPaint.setXfermode(pdf);
mPaint.setMaskFilter(bmf);
mPaint.setStyle(Paint.Style.FILL);
for (HoleBean hole : mHoleList) {
switch (hole.getType()) {
case HoleBean.TYPE_CIRCLE:
mCanvas.drawCircle(hole.getCenterX() - mBitmapRect.left, hole
.getCenterY() - mBitmapRect.top, hole.getRadius(),
mPaint);
break;
case HoleBean.TYPE_RECTANGLE:
mCanvas.drawRect(modifyRect(hole.getRectF()), mPaint);
break;
case HoleBean.TYPE_OVAL:
mCanvas.drawOval(modifyRect(hole.getRectF()), mPaint);
break;
}
}
canvas.drawBitmap(mBitmap, mBitmapRect.left, mBitmapRect.top, null);
//绘制剩余空间的矩形
mPaint.setXfermode(null);
mPaint.setMaskFilter(null);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(mStrokeWidth + 0.1f);
canvas.drawRect(fillRect(mBitmapRect), mPaint);
}
}
3. 注意点
- 在准备调用对某个View高亮的引导层方法时,需要确定这个view是已经加载完成了,即可以获取长和宽,在activity的onCreate和onResume等方法都不是正在加载完view的地方,真正加载完view的是onWindowFocusChanged,所以要注意调用时机,适当延迟。
- 最后本人写了一个manager来管理不同地方的新手引导浮层,通过SharedPreferences来保存状态。
每次使用的时候都需要判断下是够显示过了,如下:
if(NewbieGuideManager.isNeverShowed(this, NewbieGuideManager.TYPE_COLLECT)) {
new NewbieGuideManager(this, NewbieGuideManager.TYPE_COLLECT).addView
(mCollect, HoleBean.TYPE_CIRCLE).addView(mTitleTv, HoleBean
.TYPE_RECTANGLE).show();
}
有时候在list滚动到某个位置时,会显示出某个引导浮层,这个要注意listview快速滚动的情况,这里提供一种写法,在onScroll当滚动到position为6的item时,显示:
@Override
public void onScroll(final AbsListView view, int firstVisibleItem, int
visibleItemCount, int totalItemCount) {
if(firstVisibleItem >= 6 && NewbieGuideManager.isNeverShowed(this,
NewbieGuideManager.TYPE_LIST) && !isShow) {
isShow = true;
mListView.smoothScrollToPosition(6);
mListView.postDelayed(new Runnable() {
@Override
public void run() {
mListView.setSelection(6);
mListView.post(new Runnable() {
@Override
public void run() {
new NewbieGuideManager(MainActivity.this,
NewbieGuideManager.TYPE_LIST).addView(view
.getChildAt(0).findViewById(R.id.logo), HoleBean
.TYPE_RECTANGLE).show();
}
});
}
}, 200);
}
}