android 新手引导浮层的实现

作者: 许方镇 | 来源:发表于2016-06-19 23:01 被阅读15394次

源码: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时,显示:
```java
    @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后显示引导浮层](http:https://img.haomeiwen.com/i1903970/52b6175bdf4790e3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

相关文章

网友评论

  • 孙强Jimmy:作者的实现思路是值得借鉴的,但实现的代码存在问题:
    通过画笔的setXfermode来实现时,官方建议关闭硬件加速,即AndroidManifest中android:hardwareAccelerated="false",或者在自定义View中调用setLayerType(LAYER_TYPE_SOFTWARE, null);。而作者并没有关闭硬件加速,当我关闭之后效果发生了变化。虽然作者使用两个canvas实现了效果,但这并不是正确的使用方式,而且难以理解。当我关闭之后,只用一个canvas就可以实现了。
  • 稻草僧:使用dialog,自定义dialog样式即可
  • 蜗牛1:怎么动态设置箭头的位置啊大佬
  • heavyRain:https://github.com/A-Heavy-Rain/GuideView 我的实现,其实不需要新增bitmap 用onDraw里的canvas就搞定了。
  • bf04829b316a:做全局的点击取消引导页,现在出现的问题是,在引导页面出来之后,在不点击的情况下退出APP或者后台关掉APP,下次打开APP,引导页面不再显示了,这个怎么做设置,让引导页重新显示呢
  • 83f379da4db4:请问一下,如果是横屏的情况下怎么设置加载的引导页全屏。引导图片是另一个放有图片的布局。
  • ee0bcc7732c5:我引用的怎么会出问题?
  • fbff850cd0ee:compile 'com.github.huburt-Hu:NewbieGuide:v1.0.3'为什么会失败我是郁闷了,Failed to resolve
    garfield_jly:检查下项目的build.gradle是否添加

    allprojects {
    repositories {
    maven { url 'https://jitpack.io' }
    }
    }
    我刚试了下,可以导入的~~
  • F_83d3:【一个1080p屏幕大小的半透明的背景图bitmap大概需要3M以上】可是后面画的矩形大小超过屏幕大小了吧
  • VinPin:如果引导是在Fragment里,该怎么实现呢?
  • 7954fe233223:我也感觉在fragment中是不行的
  • e338b44cd5ea:如果这个引导是在fragment的话。就乱了。你这个只能在activity里面吧
  • 8797e0d3f45b:大神,遇到个问题,我需要实现4个总共,点击一个后,关闭这个再出来下一个,怎么实现啊~~
    5f58a9c13773:@kyolee 大神,你弄出来了没
    8797e0d3f45b:@许方镇 恩恩,谢谢大神,我去试试
    许方镇:@kyolee 设置一下setOnGuideChangedListener,引导蒙层消失后有回调,在onRemoved中再设置一个,只能这样了

本文标题:android 新手引导浮层的实现

本文链接:https://www.haomeiwen.com/subject/wfmbdttx.html