源碼:xufangzhen/NewbieGuide
轉(zhuǎn)載請標(biāo)明出處:http://www.reibang.com/p/5aa96683d0dc
前言
這個(gè)模塊寫了很早了孤钦,在實(shí)際項(xiàng)目中更新了很多次血当,但是太懶了塌碌,博客里沒有跟著更新,看到閱讀人數(shù)這么多梆造,在回頭看看自己寫的代碼這么爛涡驮,都不好意思了沾凄,于是決定更新一下。PS:看了評價(jià)里有人說放在fragment里顯示會(huì)亂鳄抒,我表示不知道為什么他們會(huì)這么想闯捎,在fragment顯示正常的。
更新(2016-11-22):
- 使用建筑者模式構(gòu)造引導(dǎo)浮層
- 支持在onCreate()等方法(View還未加載時(shí))中設(shè)置并顯示引導(dǎo)浮層
- 支持高亮洞洞的額外擴(kuò)大和縮小
本來在我的項(xiàng)目中使用的新手引導(dǎo)浮層是這個(gè)TourGuide開源項(xiàng)目许溅,這個(gè)新手引導(dǎo)項(xiàng)目功能比較多瓤鼻,一部分功能用不到,用到的地方只能大體上滿足視覺的樣式贤重,不能一模一樣茬祷。因此決定重構(gòu)一遍,滿足自己項(xiàng)目中的新手引導(dǎo)并蝗,本人項(xiàng)目中新手引導(dǎo)頁的樣式如下圖所示:
實(shí)現(xiàn)的功能
- 選中的view高亮可以有任意多個(gè)祭犯,形狀有矩形,圓形滚停,橢圓形
- 指示箭頭或者其他圖片可以在任意位置沃粗,可以有任意多個(gè)
- 文字和我知道了按鈕可以在任意位置(默認(rèn)我知道了在文字下方,兩者水平居中键畴,上下可調(diào))
- 點(diǎn)擊我知道了引導(dǎo)浮層消失
- 可設(shè)置點(diǎn)擊任何位置引導(dǎo)浮層消失(默認(rèn)點(diǎn)擊消失)
- 浮層出現(xiàn)和消失可以有回調(diào)接口最盅,可以延遲出現(xiàn)
實(shí)現(xiàn)的原理
1. 浮層的位置,放在activity的DecorView里起惕,DecorView為FrameLayout的子類涡贱。
DecorView為整個(gè)Window界面的最頂層View。
DecorView只有一個(gè)子元素為LinearLayout惹想,代表整個(gè)Window界面盼产。
LinearLayout里有兩個(gè)FrameLayout子元素,分別是標(biāo)題欄和內(nèi)容勺馆。
可通過以下代碼獲取戏售,不清楚可參考這篇文章Android DecorView淺析
mParentView = (FrameLayout) mActivity.getWindow().getDecorView();
2. 引導(dǎo)浮層布局及上面的元素
- 浮層為相對布局,除了高亮的地方和半透明的背景其余都是通過addView的方式添加進(jìn)去草穆,通過設(shè)置margin來調(diào)整添加子view的位置灌灾。
比如箭頭元素的添加,offsetX(offsetY)負(fù)數(shù)則從右邊(下邊)開始偏移,CENTER為居中悲柱,方便設(shè)置具體位置
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;
}
- 高亮洞洞的繪制
思路一锋喜,本人想了一個(gè)比較巧妙的方法,拿圓形的高亮洞洞來說盼铁,畫一個(gè)鏤空的圓形枝恋,你會(huì)發(fā)現(xiàn)當(dāng)畫筆足夠粗到把屏幕都遮起來的時(shí)候,剛好中心的小圓沒有畫到是高亮的可款,這個(gè)方法只需要調(diào)整好畫筆的粗度和圓形的直徑就可以了炉奴,矩形也是可垟逼庞,調(diào)整好邊長和畫筆的粗度就可以實(shí)現(xiàn)了,不過最后發(fā)現(xiàn)橢圓是不行的瞻赶, 所以總結(jié)下這個(gè)方法只能一個(gè)高亮洞洞赛糟,而且只能圓形或矩形。
所以最后還是使用和TourGuide同樣的方法璧南,通過畫筆的setXfermode來實(shí)現(xiàn),即當(dāng)兩個(gè)畫布上都繪制了圖片是师逸,可以控制最終顯示的樣式司倚,有取重疊部分,有去除重疊部分的等等篓像,這個(gè)有16中規(guī)則对湃,具體下圖:
具體用法可以參考兩篇文章 Android中Xfermode簡單用法和詳解Paint的setXfermode。簡單地說遗淳,TourGuide做法就是在一個(gè)畫布上畫了一個(gè)屏幕大小背景拍柒,在另一個(gè)畫布上畫了一個(gè)圓形,因?yàn)橹丿B了屈暗,所以去除了重疊的部分拆讯,高亮洞洞就顯示出來了。
本人針對這個(gè)做法做了點(diǎn)優(yōu)化养叛,即畫了一個(gè)和洞洞所需要一樣大小的圖片而不是屏幕一樣大小种呐,如果有兩個(gè)洞洞,則畫了一個(gè)能同時(shí)容下兩個(gè)洞洞的矩形大小的圖片弃甥,這樣顯示的結(jié)果最終會(huì)變成下面這樣:
高亮洞洞都顯示出來了爽室,但是只畫了一部分的背景,首先這樣做的目的是為了用較少的內(nèi)存去完成(一個(gè)1080p屏幕大小的半透明的背景圖bitmap大概需要3M以上)那剩余的空白部分該怎么填充呢淆攻,一個(gè)方法是用四個(gè)view去填充上阔墩,這個(gè)做法是可行的,但是麻煩瓶珊,其實(shí)一個(gè)比較簡單的做法我已經(jīng)在上面提到了啸箫,就是思路一中說的畫一個(gè)矩形,調(diào)整好畫筆的粗度和長寬即可填充完伞芹,關(guān)鍵代碼如下忘苛。
@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. 注意點(diǎn)
- 在準(zhǔn)備調(diào)用對某個(gè)View高亮的引導(dǎo)層方法時(shí)蝉娜,需要確定這個(gè)view是已經(jīng)加載完成了,即可以獲取長和寬扎唾,在activity的onCreate和onResume等方法都不是正在加載完view的地方召川,真正加載完view的是onWindowFocusChanged,所以要注意調(diào)用時(shí)機(jī)胸遇,適當(dāng)延遲荧呐。
- 最后本人寫了一個(gè)manager來管理不同地方的新手引導(dǎo)浮層,通過SharedPreferences來保存狀態(tài)狐榔。
每次使用的時(shí)候都需要判斷下是夠顯示過了坛增,如下:
if(NewbieGuideManager.isNeverShowed(this, NewbieGuideManager.TYPE_COLLECT)) {
new NewbieGuideManager(this, NewbieGuideManager.TYPE_COLLECT).addView
(mCollect, HoleBean.TYPE_CIRCLE).addView(mTitleTv, HoleBean
.TYPE_RECTANGLE).show();
}
有時(shí)候在list滾動(dòng)到某個(gè)位置時(shí)获雕,會(huì)顯示出某個(gè)引導(dǎo)浮層薄腻,這個(gè)要注意listview快速滾動(dòng)的情況,這里提供一種寫法届案,在onScroll當(dāng)滾動(dòng)到position為6的item時(shí)庵楷,顯示:
@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);
}
}