android 新手引导浮层的实现

源码:xufangzhen/NewbieGuide
转载请标明出处:http://www.jianshu.com/p/5aa96683d0dc

前言

这个模块写了很早了,在实际项目中更新了很多次,但是太懒了,博客里没有跟着更新,看到阅读人数这么多,在回头看看自己写的代码这么烂,都不好意思了,于是决定更新一下。PS:看了评价里有人说放在fragment里显示会乱,我表示不知道为什么他们会这么想,在fragment显示正常的。
更新(2016-11-22):

  • 使用建筑者模式构造引导浮层
  • 支持在onCreate()等方法(View还未加载时)中设置并显示引导浮层
  • 支持高亮洞洞的额外扩大和缩小

本来在我的项目中使用的新手引导浮层是这个TourGuide开源项目,这个新手引导项目功能比较多,一部分功能用不到,用到的地方只能大体上满足视觉的样式,不能一模一样。因此决定重构一遍,满足自己项目中的新手引导,本人项目中新手引导页的样式如下图所示:

新手引导.png

实现的功能

  1. 选中的view高亮可以有任意多个,形状有矩形,圆形,椭圆形
  2. 指示箭头或者其他图片可以在任意位置,可以有任意多个
  3. 文字和我知道了按钮可以在任意位置(默认我知道了在文字下方,两者水平居中,上下可调)
  4. 点击我知道了引导浮层消失
  5. 可设置点击任何位置引导浮层消失(默认点击消失)
  6. 浮层出现和消失可以有回调接口,可以延迟出现

实现的原理

1. 浮层的位置,放在activity的DecorView里,DecorView为FrameLayout的子类。

DecorView为整个Window界面的最顶层View。
DecorView只有一个子元素为LinearLayout,代表整个Window界面。
LinearLayout里有两个FrameLayout子元素,分别是标题栏和内容。

可通过以下代码获取,不清楚可参考这篇文章Android DecorView浅析

mParentView = (FrameLayout) mActivity.getWindow().getDecorView();

2. 引导浮层布局及上面的元素

  1. 浮层为相对布局,除了高亮的地方和半透明的背景其余都是通过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;
    }
  1. 高亮洞洞的绘制
    思路一,本人想了一个比较巧妙的方法,拿圆形的高亮洞洞来说,画一个镂空的圆形,你会发现当画笔足够粗到把屏幕都遮起来的时候,刚好中心的小圆没有画到是高亮的,这个方法只需要调整好画笔的粗度和圆形的直径就可以了,矩形也是可垟,调整好边长和画笔的粗度就可以实现了,不过最后发现椭圆是不行的, 所以总结下这个方法只能一个高亮洞洞,而且只能圆形或矩形。
    画笔粗到可以遮挡屏幕时,中间的地方就高亮的洞洞

    所以最后还是使用和TourGuide同样的方法,通过画笔的setXfermode来实现,即当两个画布上都绘制了图片是,可以控制最终显示的样式,有取重叠部分,有去除重叠部分的等等,这个有16中规则,具体下图:
    setXfermode属性

    具体用法可以参考两篇文章 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);
        }
    }
滚动到item6后显示引导浮层
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容