前言

之前在公司开发中,产品设计了一个新功能引导页面,不是app启动时的启动页,而是对新功能页面的某一个按钮或者控件进行高亮,显示一些提示信息,直接在页面上层弹出遮罩蒙层,引导新手用户一步步地熟悉操作,可能一页也可能有多页,部分引导区域还需要做到事件的穿透,部分不穿透。效果实现如下:

在这里插入图片描述

一、实现思路(解决难题的通用流程)

1. 找教程 / 项目

大多数情况下,在开发过程中遇到难题时,一般就两种方式(这个功能用处较少所以我采用第二种):

  1. 自己实现
  2. 使用第三方库来实现(github)

github 项目都是开源的,在开发中一定要学会在上面去 pull 项目,同时在自己的环境下运行起来,通过解析别人优秀的代码可以学到很多。就算是自己实现也可以通过 github 去借鉴和学习思路、灵感,以便找到解决办法。

2. 调用api / 优化代码

找到第三方库之后就是查看项目介绍、官方教程或者网上教程,去实践调用具体的方法 api。若部分不满足需求不得不需要改代码时,可以直接将整个项目拉下来,修改后再复制到自己项目中。

二、第三方库

我网上找了很多第三方库都不太满足需求,最后才找到一个勉强符合的(Highlight),具体为什么选择它我已经记不得了,大家可以结合自己需求去选择,没有最好,合适自己的就是好的。下面是比较好用的第三方引导库:

  1. NewbieGuide(作者:胡奚冰,郭霖(《第一行代码》作者)公众号曾转载过这个库,感兴趣的可以去看看。简洁链式调用,一行代码实现引导层的显示)
    https://github.com/huburt-Hu/NewbieGuide
    在这里插入图片描述

  2. GuideView(这有两个)
    基于DialogFragment实现的引导遮罩浮层视图的轻量级解决方案
    https://github.com/easilycoder/GuideView
    在这里插入图片描述

    最轻量级的新手引导库,能够快速为任何一个View创建一个遮罩层,支持单个页面,多个引导提示,支持为高亮区域设置不同的图形,支持引导动画。
    https://github.com/binIoter/GuideView
    在这里插入图片描述

  3. Highlight(作者:鸿洋,郭霖和鸿洋都是安卓领域的知名大神,一个用于app指向性功能高亮的库)
    https://github.com/hongyangAndroid/Highlight
    在这里插入图片描述

  4. 其他
    https://github.com/TakuSemba/Spotlight
    https://github.com/faruktoptas/FancyShowCaseView
    https://github.com/jaydenxiao2016/HighLightGuideView
    https://github.com/yilylong/UserGuideView


二、具体实现

1.添加依赖

implementation 'com.isanwenyu.highlight:highlight:1.8.0'

接下来就是熟悉方法 api

2. 方法 api

实例化

val highLight= HighLight(requireActivity())

链式方法:

  • enableNext():开启多页引导。多页引导时候调用,开启next模式并显示,然后next()方法显示下一个提示布局,直到删除自己
  • anchor(findViewById(R.id.id_container)):设置依附的根布局,需要在哪个view上加一层透明的蒙版,如果是Activity上增加引导层,不需要设置anchor,支持局部范围内去高亮某些View
  • intercept(true):拦截器。是否拦截遮罩的透明背景的点击事件,对应设置点击事件ClickCallback
  • setClickCallback:点击背景的事件
  • autoRemove(false):设置取消高亮。true点击遮罩背景就可以取消高亮View,false不能取消,默认true
  • addHighLight(R.id.tv_text, R.layout.xxx, new OnRightPosCallback(), new RectLightShape()):设置高亮控件,可以无限添加。
    第一个参数:设置当前页面的哪个控件高亮
    第二个参数:高亮提示的文字或者布局。比如 “我知道了” 或者显示一个弹窗
    第三个参数:设置提示文字在高亮按钮的什么位置,提供了上下左右,也可实现OnBaseCallback自定义。
    第四个参数:设置用什么样式包裹高亮的View,如RectLightShape(矩形)、CircleLightShape(圆形)、OvalLightShape(椭圆),也可实现BaseLightShape自定义。
  • setOnRemoveCallback:移除引导页时的回调
  • setOnShowCallback:显示引导页时的回调
  • setOnNextCallback:调转到下一页引导的回调。实现next()方法
  • setOnLayoutCallback:页面加载完成时的回调。可在onCreated方法中使用highLight,页面加载完成就自动显示
  • show():显示
  • next():显示下一页
  • isShowing():是否显示
  • isNext():是否开启next模式
  • remove():移除
  • getHightLightView():获取高亮布局

3. 使用示例

该库中的使用示例:

    /**
     * 显示 next模式 我知道了提示高亮布局
     * @param view id为R.id.iv_known的控件
     */
    public  void showNextKnownTipView(View view)
    {
        mHightLight = new HighLight(MainActivity.this)//
                .autoRemove(false)//设置背景点击高亮布局自动移除为false 默认为true
//                .intercept(false)//设置拦截属性为false 高亮布局不影响后面布局的滑动效果
                .intercept(true)//拦截属性默认为true 使下方ClickCallback生效
                .enableNext()//开启next模式并通过show方法显示 然后通过调用next()方法切换到下一个提示布局,直到移除自身
//                .setClickCallback(new HighLight.OnClickCallback() {
//                    @Override
//                    public void onClick() {
//                        Toast.makeText(MainActivity.this, "clicked and remove HightLight view by yourself", Toast.LENGTH_SHORT).show();
//                        remove(null);
//                    }
//                })
                .anchor(findViewById(R.id.id_container))//如果是Activity上增加引导层,不需要设置anchor
                .addHighLight(R.id.btn_rightLight,R.layout.info_known,new OnLeftPosCallback(45),new RectLightShape(0,0,15,0,0))//矩形去除圆角
                .addHighLight(R.id.btn_light,R.layout.info_known,new OnRightPosCallback(5),new BaseLightShape(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,5,getResources().getDisplayMetrics()), TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,5,getResources().getDisplayMetrics()),0) {
                    @Override
                    protected void resetRectF4Shape(RectF viewPosInfoRectF, float dx, float dy) {
                        //缩小高亮控件范围
                        viewPosInfoRectF.inset(dx,dy);
                    }

                    @Override
                    protected void drawShape(Bitmap bitmap, HighLight.ViewPosInfo viewPosInfo) {
                        //custom your hight light shape 自定义高亮形状
                        Canvas canvas = new Canvas(bitmap);
                        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
                        paint.setDither(true);
                        paint.setAntiAlias(true);
                        //blurRadius必须大于0
                        if(blurRadius>0){
                            paint.setMaskFilter(new BlurMaskFilter(blurRadius, BlurMaskFilter.Blur.SOLID));
                        }
                        RectF rectF = viewPosInfo.rectF;
                        canvas.drawOval(rectF, paint);
                    }
                })
                .addHighLight(R.id.btn_bottomLight,R.layout.info_known,new OnTopPosCallback(),new CircleLightShape())
                //第三个参数:设置高亮view的位置
				/**
				  * @param rightMargin 高亮view在anchor中的右边距
				  * @param bottomMargin 高亮view在anchor中的下边距
				  * @param rectF 高亮view的l,t,r,b,w,h都有
				  * @param marginInfo 设置你的布局的位置,一般设置l,t或者r,b
				  */
                .addHighLight(view,R.layout.info_known,new OnBottomPosCallback(10),new OvalLightShape(5,5,20))
                .setOnRemoveCallback(new HighLightInterface.OnRemoveCallback() {//监听移除回调 
                    @Override
                    public void onRemove() {
                        Toast.makeText(MainActivity.this, "The HightLight view has been removed", Toast.LENGTH_SHORT).show();

                    }
                })
                .setOnShowCallback(new HighLightInterface.OnShowCallback() {//监听显示回调
                    @Override
                    public void onShow(HightLightView hightLightView) {
                        Toast.makeText(MainActivity.this, "The HightLight view has been shown", Toast.LENGTH_SHORT).show();
                    }
                }).setOnNextCallback(new HighLightInterface.OnNextCallback() {
                    @Override
                    public void onNext(HightLightView hightLightView, View targetView, View tipView) {
                        // targetView 目标按钮 tipView添加的提示布局 可以直接找到'我知道了'按钮添加监听事件等处理
                        Toast.makeText(MainActivity.this, "The HightLight show next TipView,targetViewID:"+(targetView==null?null:targetView.getId())+",tipViewID:"+(tipView==null?null:tipView.getId()), Toast.LENGTH_SHORT).show();
                    }
                });
        mHightLight.show();
    }

我是在一个activity中包含三个fragment,分别显示三页引导,高亮的提示信息是一个弹窗,多页引导,页面加载后自动显示第一页:

//activity中
var hightLightOne: HighLight?=null //第一页
var hightLightTwo: HighLight?=null //第二页
var hightLightThree: HighLight?=null //第三页
//fragment中使用
private fun showGuideView() {
        val decorView = requireActivity().window.decorView

        mActivity.hightLightOne = HighLight(requireActivity())
            .autoRemove(false)
            .enableNext()
            .anchor(decorView) //fragment中使用
            .setOnLayoutCallback {
                mActivity.hightLightOne!!
                    .addHighLight(ll_guide_one, R.layout.guide_dialog_center, object : OnBaseCallback() {
                            override fun getPosition(rightMargin: Float, bottomMargin: Float, rectF: RectF, marginInfo: HighLight.MarginInfo) {
                            	//高亮部分和提示弹窗的距离:设置leftMargin和topMargin;或者rightMargin和bottomMargin
                                marginInfo.leftMargin = 0f
                                marginInfo.bottomMargin = bottomMargin + rectF.height()
                            }
                        },
                        RectLightShape() //高亮部分矩形
                    )
                    .setOnNextCallback(OnNextCallback { _, _, tipView ->
                    	//tipView 即 R.layout.score_guide_dialog_center 布局
                        tipView.findViewById<TextView>(R.id.tv_dialog_title).text="第一步" //标题
                        tipView.findViewById<TextView>(R.id.tv_dialog_content).text="我知道了" //内容
                        tipView.findViewById<TextView>(R.id.tv_dialog_order).text="1/3" //页码
                        tipView.findViewById<View>(R.id.btn_dialog_next).setOnClickListener { //点击下一步显示下一页
                            mActivity.hightLightTwo = HighLight(requireActivity())
                                ...... //这里和第一页类似
                                .setOnLayoutCallback {
                                    mActivity.hightLightTwo!!
                                        ...... //这里和第一页类似
                                        .setOnNextCallback(OnNextCallback { _, _, tipView ->
                                            ...... //这里和第一页类似
                                            //下一页
                                            tipView.findViewById<View>(R.id.btn_dialog_next).setOnClickListener {
                                                mActivity.hightLightTwo?.remove()
                                                (activity as xxxActivity).switchFragment(1) //切换fragment
                                                //显示第三页
                                                mActivity.hightLightThree?.show()
                                            }
                                            //上一页
                                            tipView.findViewById<View>(R.id.btn_dialog_back).setOnClickListener {
                                                mActivity.hightLightTwo?.remove()
                                                //显示第一页
                                                mActivity.hightLightOne?.show()
                                            }
                                            //关闭弹窗
                                            tipView.findViewById<View>(R.id.iv_dialog_dismiss).setOnClickListener {
                                                mActivity.hightLightTwo?.remove()
                                                requireActivity().finish()
                                            }

                                        })
                                    mActivity.hightLightTwo?.show()
                                }
                            mActivity.hightLightOne?.remove()
                        }
                   		//关闭弹窗
                        tipView.findViewById<View>(R.id.iv_dialog_dismiss).setOnClickListener {
                            mActivity.hightLightOne?.remove()
                            requireActivity().finish()
                        }
                    })
                mActivity.hightLightOne?.show()
            }
    }

guide_dialog_center.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="center_horizontal"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <androidx.cardview.widget.CardView
        android:id="@+id/cv_layout"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        app:cardElevation="0dp"
        app:cardBackgroundColor="#5083FC"
        app:cardCornerRadius="@dimen/dp_10">

        <LinearLayout
            android:layout_width="350dp"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:paddingBottom="20dp">

            <androidx.constraintlayout.widget.ConstraintLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="horizontal">

                <TextView
                    android:paddingTop="10dp"
                    android:paddingStart="20dp"
                    android:id="@+id/tv_dialog_title"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:paddingBottom="@dimen/dp_10"
                    android:text="标题"
                    android:textColor="@color/white"
                    android:textSize="20sp"
                    app:layout_constraintBottom_toBottomOf="parent"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintTop_toTopOf="parent" />
				<!-- 关闭弹窗按钮 -->
                <ImageView
                    android:id="@+id/iv_dialog_dismiss"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:padding="@dimen/dp_10"
                    android:src="@mipmap/guide_cross"
                    app:layout_constraintEnd_toEndOf="parent"
                    app:layout_constraintTop_toTopOf="parent" />
            </androidx.constraintlayout.widget.ConstraintLayout>


            <TextView
                android:paddingStart="20dp"
                android:paddingEnd="20dp"
                android:id="@+id/tv_dialog_content"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:lineSpacingExtra="3dp"
                android:paddingBottom="@dimen/dp_10"
                android:text="内容"
                android:textColor="@color/white"
                android:textSize="16sp" />

            <androidx.constraintlayout.widget.ConstraintLayout
                android:paddingStart="20dp"
                android:paddingEnd="20dp"
                android:layout_width="match_parent"
                android:layout_height="wrap_content">

                <TextView
                    android:id="@+id/tv_dialog_order"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="1/3"
                    android:textColor="@color/white"
                    android:textSize="20sp"
                    app:layout_constraintBottom_toBottomOf="parent"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintTop_toTopOf="parent" />
				<!-- 圆角按钮 -->
                <Button
                    android:id="@+id/btn_dialog_back"
                    android:layout_width="wrap_content"
                    android:layout_height="35dp"
                    android:layout_marginEnd="10dp"
                    android:background="@drawable/score_button_theme_white"
                    android:text="上一步"
                    android:textColor="@color/white"
                    app:layout_constraintBottom_toBottomOf="parent"
                    app:layout_constraintEnd_toStartOf="@id/btn_dialog_next"
                    app:layout_constraintTop_toTopOf="parent" />
				
                <Button
                    android:id="@+id/btn_dialog_next"
                    android:layout_width="wrap_content"
                    android:layout_height="35dp"
                    android:background="@drawable/score_button_bg_white"
                    android:text="下一步"
                    android:textColor="@color/blueColor"
                    app:layout_constraintBottom_toBottomOf="parent"
                    app:layout_constraintEnd_toEndOf="parent"
                    app:layout_constraintTop_toTopOf="parent" />
            </androidx.constraintlayout.widget.ConstraintLayout>
        </LinearLayout>
    </androidx.cardview.widget.CardView>

	<!-- 指向箭头 -->
    <ImageView
        android:id="@+id/iv_dialog_arrow"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/guide_arrow_long"
        android:layout_marginTop="-15dp"
        android:layout_centerHorizontal="true"
        android:layout_below="@id/cv_layout"/>
</RelativeLayout>

实现图:

在这里插入图片描述


总结

代码稍微有点冗余了,但调用是正确的,如果有更好的优化想法的也可以给我反馈,相互学习。调用api接口这在实际开发中是非常重要的,在调用第三方库或者说官方提供的api,如何去快速使用甚至去改造它。代码中很多思路都是相似的,完全可以做到举一反三,触类旁通,跟上不断迭代的技术脚步。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐