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