来抠个图吧~——更优雅的Android UI界面控件高亮的实现

背景

在咱们的开发过程当中,经常遇到这样的问题,咱们的APP开发中要在某个页面去加一些新功能的引导,最经常使用的就是将整个页面作成一个相似于Dialog背景的蒙层,而后将想提示用户的位置高亮出来,最后加一些元素在上面,那么大概效果就是这样:java

image

乍一看很简单嘛,设计师切个纯图展现不就行了嘛? 其实咱们以前的功能都是这么作的: 须要展现用户引导页的时候用一个设计师给的纯图覆盖在当前页面.android

可是这样虽然又不是不能用,但其实一直会存在几个问题:git

  1. 设计师一套16:9的图没法适配全部比例的屏幕,其余纵横比的机型会出现拉伸的状况.
  2. 高分辨率手机但一套图模糊,可是多图又会增大APk包大小

带着这个问题,咱们去和设计师沟通了一番,后来设计无心间一句话引发了个人思考“既然多图适配这么麻烦,你是否能够把那块控件抠出来呢?”github

预期效果:

在不使用纯图的前提下实现一个全屏的蒙层上制定的一个或者多个View的高亮canvas

可行性分析:

最初尝试的方案A:
  1. 首先在整个界面画出一个半透明的全屏蒙层
  2. 经过View.getDrawingCache() 获取该目标View的bitmap缓存
  3. 获取该View在屏幕中的位置,在该位置放置一个ImageView去展现以前拿到的Bitmap缓存,即达到了高亮View的效果

效果: 发现部分View是能够经过该方案实现高亮的,可是会有几个的问题:数组

  1. 不少时候,咱们看到的View 实际上是层叠的,它本身自己没背景颜色,而背景就绘制在它的Parent中,咱们获取它的DrawingCache 只能拿到一个没有背景的View缓存图,而这个结果确定不是咱们那想要的.
  2. 若是View经过Shape指定了背景的话,经过这个方式没法获取背景的圆角或者圆形,只能获得一个矩形的图
  3. 这个获取View,bitmap的方法在不一样机型下有些兼容性问题,部分低端机型下会出现卡顿的状况
最终选择的方案B:
  1. 首先在整个界面画出一个半透明的全屏蒙层
  2. 找到View在屏幕中的位置,和它当前的大小,直接在蒙层上绘出这个大小的矩形,若是它是有设置背景的,根据它背景的类型,获取到相关的ShapeDrawable,而后判断它当前的形状而后咱们绘制跟它背景如出一辙的形状,而后将这块区域“镂空”便可!

那如何镂空呢? 咱们先来看看最终实现效果,后面咱们来说实现原理:缓存

image

而实现上述效果,仅仅须要一行代码:bash

private void showInitGuide() {
    new Curtain(SimpleGuideActivity.this)
            .with(findViewById(R.id.iv_guide_first))
            .with(findViewById(R.id.btn_shape_circle))
            .with(findViewById(R.id.btn_shape_custom))
            .show();
    }
复制代码

Curtain(窗帘)

大体能实现以下功能:markdown

  • 一行代码完成某个View,或者多个View的高亮展现
  • 一样支持基于AapterView(如ListView、GridView等) 或RecyclerView 的item以及item中元素的高亮
  • 自动识别圆角背景,也能够自定义任何你想要的形状
  • 若是依次按顺序去高亮一些列View,提供流式操做

设计流程

接下来我来分解一下主要设计思路,一步步达到咱们想要的效果:app

在蒙层上“镂空一块区域”

回想一下: 咱们最开始经过接触CircleImageView,了解到View绘制过程当中,图层层叠有16种叠加效果:

image

那么咱们绘制的图层1不就是半透明的背景,而图层2就是咱们的View的形状区域,咱们只要找到一个叠加公共区域透明的效果是否是就是实现了镂空的效果了?因此这边我选择了DstOut效果,因此核心代码以下:

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawBackGround(canvas);
        drawHollowFields(canvas);
    }
    /** * 画一个半透明的背景 */
    private void drawBackGround(Canvas canvas) {
        mPaint.setXfermode(null);
        mPaint.setColor(mCurtainColor);
        canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);
    }
    /** * 画出透明区域 */
    private void drawHollowFields(Canvas canvas) {
        mPaint.setColor(Color.WHITE);
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
        //测试画一个圆
        canvas.drawCircle(getWidth()/2,getHeight()/4,300, mPaint);
    }
复制代码

效果以下,是否是已经镂空了?

image

固然,这里就是核心的逻辑了,实际上咱们须要高亮的是咱们的View,下面咱们来一步步设计实现它:

  1. 由于咱们是要把View镂空,因此,咱们须要写一个类,包含咱们的View,以及它的大小和区域,咱们叫他HollowInfo:
public class HollowInfo {
    
    /** * 目标View 用于定位透明区域 */
    public View targetView;

    /** * 可自定义区域大小 */
    public Rect targetBound;
   
}
复制代码

这边列出了最核心的两个属性,第一个是咱们核心的的View,咱们须要根据它在屏幕上的位置肯定咱们绘制的起点,第二个是绘制的区域,咱们可使用View本身的的宽高,也能够自定义它的大小.

2.有了咱们的基本绘制实体类,我来定义咱们的画板,它主要作两件事:

  • 根据指定颜色绘制整个屏幕大小的半透明蒙层
  • 在蒙层上绘制指定大小的镂空区域
public class GuideView extends View {

    private HollowInfo[] mHollows;

    private int mCurtainColor = 0x88000000;

    private Paint mPaint;

    public GuideView(@NonNull Context context) {
        super(context, null);
        init();
    }

    private void init() {
        mPaint = new Paint(ANTI_ALIAS_FLAG);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //固然是全屏大小
        setMeasuredDimension(getScreenWidth(getContext()), getScreenHeight(getContext()));
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int count;
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
            count = canvas.saveLayer(0, 0, getWidth(), getHeight(), null);
        } else {
            count = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
        }
        drawBackGround(canvas);
        drawHollowFields(canvas);
        canvas.restoreToCount(count);
    }

    private void drawBackGround(Canvas canvas) {
        mPaint.setXfermode(null);
        mPaint.setColor(mCurtainColor);
        canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);
    }
    /** * 绘制全部镂空区域 */
    private void drawHollowFields(Canvas canvas) {
        mPaint.setColor(Color.WHITE);
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
        //可能有多个View 须要高亮, 因此遍历数组
        for (HollowInfo mHollow : mHollows) {
            drawSingleHollow(mHollow, canvas);
        }
    }
    
    private void drawSingleHollow(HollowInfo info, Canvas canvas) {
        if (mHollows.length <= 0) {
            return;
        }
        info.targetBound = new Rect();
        //获取View的边界方框
        info.targetView.getDrawingRect(info.targetBound);
        int[] viewLocation = new int[2];
        info.targetView.getLocationOnScreen(viewLocation);
        info.targetBound.left = viewLocation[0];
        info.targetBound.top = viewLocation[1];
        info.targetBound.right += info.targetBound.left;
        info.targetBound.bottom += info.targetBound.top;
        //要减去状态栏的高度
        info.targetBound.top -= getStatusBarHeight(getContext());
        info.targetBound.bottom -= getStatusBarHeight(getContext());
        //绘制镂空区域
        realDrawHollows(info, canvas);
    }

    private void realDrawHollows(HollowInfo info, Canvas canvas) {
        canvas.drawRect(info.targetBound, mPaint);
    }
}
复制代码

效果以下:

image

到目前咱们已经把图片ImageView高亮了,彷佛已经完成了,可是咱们细看一下,它下面有两个设置了Shape的按钮,分别是圆形和圆角的,而咱们代码中只绘制了矩形,因此确定是没办法适配圆角的,那怎么办呢?

对!,咱们能够从View的backGround入手,由于咱们能设置各类shape的Drawable实际上就是GradientDrawable,咱们能够同过判断它的类型,而后经过反射获取咱们想要的属性,咱们修改realDrawHollows代码以下:

/** * 绘制镂空区域 */
    private void realDrawHollows(HollowInfo info, Canvas canvas) {
        if (!drawHollowSpaceIfMatched(info, canvas)) {
            //没有匹配上,默认降级方案:画一个矩形
            canvas.drawRect(info.targetBound, mPaint);
        }
    }

    private boolean drawHollowSpaceIfMatched(HollowInfo info, Canvas canvas) {
        //android shape backGround
        Drawable drawable = info.targetView.getBackground();
        if (drawable instanceof GradientDrawable) {
            drawGradientHollow(info, canvas, drawable);
            return true;
        }
        return false;
    }

    private void drawGradientHollow(HollowInfo info, Canvas canvas, Drawable drawable) {
        Field fieldGradientState;
        Object mGradientState = null;
        int shape = GradientDrawable.RECTANGLE;
        try {
            fieldGradientState = Class.forName("android.graphics.drawable.GradientDrawable").getDeclaredField("mGradientState");
            fieldGradientState.setAccessible(true);
            mGradientState = fieldGradientState.get(drawable);
            Field fieldShape = mGradientState.getClass().getDeclaredField("mShape");
            fieldShape.setAccessible(true);
            shape = (int) fieldShape.get(mGradientState);
        } catch (Exception e) {
            e.printStackTrace();
        }
        float mRadius = 0;
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
            mRadius = ((GradientDrawable) drawable).getCornerRadius();
        } else {
            try {
                Field fieldRadius = mGradientState.getClass().getDeclaredField("mRadius");
                fieldRadius.setAccessible(true);
                mRadius = (float) fieldRadius.get(mGradientState);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        if (shape == GradientDrawable.OVAL) {
            canvas.drawOval(new RectF(info.targetBound.left, info.targetBound.top, info.targetBound.right, info.targetBound.bottom), mPaint);
        } else {
            float rad = Math.min(mRadius,
                    Math.min(info.targetBound.width(), info.targetBound.height()) * 0.5f);
            canvas.drawRoundRect(new RectF(info.targetBound.left, info.targetBound.top, info.targetBound.right, info.targetBound.bottom), rad, rad, mPaint);
        }
    }
复制代码

在获取到背景类型时候,我若是肯定了是咱们想要的GradientDrawable以后,咱们就去获取它的形状实际类型,是椭圆仍是圆角,再获取它的圆角度数,能拿到直接拿,拿不到经过反射的方式,最后绘制出相应的形状便可.

固然,咱们View的背景多是一个Selector,因此咱们须要外加一层判断:取它当前的第一个:

private boolean drawHollowSpaceIfMatched(HollowInfo info, Canvas canvas) {
        //android shape backGround
        Drawable drawable = info.targetView.getBackground();
        if (drawable instanceof GradientDrawable) {
            drawGradientHollow(info, canvas, drawable);
            return true;
        }
        //android selector backGround
        if (drawable instanceof StateListDrawable) {
            if (drawable.getCurrent() instanceof GradientDrawable) {
                drawGradientHollow(info, canvas, drawable.getCurrent());
                return true;
            }
        }
        return false;
    }
复制代码

咱们再来看看这么作以后的效果:

image

支持自定义

虽然咱们能本身适配View的背景,可能不能包含全部Drawable的,好比RippleDrawable,并且实际业务场景确定很复杂,也许产品须要特别的高亮形状?一个好的代码确定要有拓展的能力,咱们可否将图形的方法自定义?,接下来咱们自定义一个Shape:

public interface Shape {

    /** * 画你想要的任何形状 */
    void drawShape(Canvas canvas, Paint paint, HollowInfo info);

}

复制代码

在HolloInfo中增长Shape,由用户在构建HolloInfo时候传入:

public class HollowInfo {
    
    /** * 目标View 用于定位透明区域 */
    public View targetView;

    /** * 可自定义区域大小 */
    public Rect targetBound;
    
     /** * 指定的形状 */
    public Shape shape;
   
}
复制代码

再来补充咱们的drawHollowSpaceIfMatched方法:若是用户指定了形状的话,咱们优先画形状,不然再自动适配它的背景:

private boolean drawHollowSpaceIfMatched(HollowInfo info, Canvas canvas) {
        //user custom shape
        if (null != info.shape) {
            info.shape.drawShape(canvas, mPaint, info);
            return true;
        }
        //android shape backGround
        Drawable drawable = info.targetView.getBackground();
        if (drawable instanceof GradientDrawable) {
            drawGradientHollow(info, canvas, drawable);
            return true;
        }
        //android selector backGround
        if (drawable instanceof StateListDrawable) {
            if (drawable.getCurrent() instanceof GradientDrawable) {
                drawGradientHollow(info, canvas, drawable.getCurrent());
                return true;
            }
        }
        return false;
    }
复制代码

我如今自定义一个圆角的形状:

public class RoundShape implements Shape {

    private float radius;

    public RoundShape(float radius) {
        this.radius = radius;
    }

    @Override
    public void drawShape(Canvas canvas, Paint paint, HollowInfo info) {
        canvas.drawRoundRect(new RectF(info.targetBound.left, info.targetBound.top, info.targetBound.right, info.targetBound.bottom), radius, radius, paint);
    }
}

   private void showInitGuide() {
        new Curtain(SimpleGuideActivity.this)
                //自定义高亮形状
                .withShape(findViewById(R.id.btn_shape_circle), new RoundShape(12)).show();
    }

复制代码

咱们设置给一个圆形的View 那么效果以下:

image

因此,只要自定义了Shape,形状交给你,想怎么自定义都行~

到这里有朋友问了...那我除了高亮View以外,还须要添加一些文字,或者可交互的元素(好比按钮)怎么办呢?

  • 很简单嘛! 咱们在咱们的蒙层View中再盖上一层去展现额外的元素不就行了!,如今咱们只须要给这些元素找一个载体便可~
寻找合适的载体:

由于咱们是一个引导页的蒙层,因此我第一时间想到的就是Dialog,

  • 第一方面,dialog构建方便,咱们只须要本身构建View填充给它,而后将dialog设为全屏切透明便可
  • 第二方面,dialog 能够自动和回退键交互,咱们不须要额外本身处理,更符合用户操做习惯.

固然构建Dialog,咱们固然推荐DialogFragment,方便管理横竖屏的状况,也是谷歌推荐的作法, 那么核心代码以下:

public class GuideDialogFragment extends DialogFragment {

    private static final int MAX_CHILD_COUNT = 2;

    private static final int GUIDE_ID = 0x3;

    private FrameLayout contentView;

    private Dialog dialog;

    private int topLayoutRes = 0;

    private GuideView guideView;

    public void show() {
        FragmentActivity activity = (FragmentActivity) guideView.getContext();
        guideView.setId(GUIDE_ID);
        this.contentView = new FrameLayout(activity);
        this.contentView.addView(guideView);
        if (topLayoutRes != 0) {
            updateTopView();
        }
        //定义一个全透明主题的Dialog
        dialog = new AlertDialog.Builder(activity, R.style.TransparentDialog)
                .setView(contentView)
                .create();
        show(activity.getSupportFragmentManager(), GuideDialogFragment.class.getSimpleName());
    }

    void updateContent() {
        contentView.removeAllViews();
        contentView.addView(guideView);
        if (contentView.getChildCount() == MAX_CHILD_COUNT) {
            contentView.removeViewAt(1);
        }
        //将自定义的View 布局加载入contentView的顶层达到层叠的效果
        LayoutInflater.from(contentView.getContext()).inflate(topLayoutRes, contentView, true);
    }

    /** * 防止出现状态丢失 */
    @Override
    public void show(FragmentManager manager, String tag) {
        try {
            super.show(manager, tag);
        } catch (Exception e) {
            manager.beginTransaction()
                    .add(this, tag)
                    .commitAllowingStateLoss();
        }
    }
    
    @NonNull
    @Override
    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
        return dialog;
    }


    @Override
    public void onDestroyView() {
        super.onDestroyView();
        if (dialog != null) {
            dialog = null;
        }
    }

    private void updateTopView() {
        if (contentView.getChildCount() == MAX_CHILD_COUNT) {
            contentView.removeViewAt(1);
        }
        LayoutInflater.from(contentView.getContext()).inflate(topLayoutRes, contentView, true);
    }
}

复制代码

代码很简单,核心就是建立一个Dialog,将咱们的透明的View和顶层包含其余元素的TopView放入Dialog的contentView中再展现出来~

只有两个细节点我提一下:

  1. DialogFragment 源码的show中默认使用commit提交Fragment的事务,在一些Activity界面重建的状况下可能出现状态丢失的异常,咱们try/catch住并从新实现保证逻辑的正常执行:
@Override
public void show(FragmentManager manager, String tag) {
    try {
        super.show(manager, tag);
    } catch (Exception e) {
        manager.beginTransaction()
                .add(this, tag)
                .commitAllowingStateLoss();
    }
}

复制代码
  1. 全屏透明的Dialog咱们使用Theme便可实现:
<style name="TransparentDialog" parent="@android:style/Theme.Dialog"> <item name="android:windowIsFloating">false</item> <item name="android:windowNoTitle">true</item> <item name="android:windowIsTranslucent">true</item> <item name="android:windowAnimationStyle">@null</item> <item name="android:windowBackground">@android:color/transparent</item> </style>
复制代码

载体咱们就作好了,接下来就是设计调用API了:

设计调用APi:

最终咱们交给用户使用的时候无非就只剩下这么几件事了:

  1. 指定要高亮的View,若是有特殊需求形状等也一并加入
  2. 指定显示在顶层的布局
  3. 像Dialog同样按需设置回调,设置回退键等

代码细节我精简了一下,大体就是一个构建者模式:

public class Curtain {

    SparseArray<HollowInfo> hollows;

    boolean cancelBackPressed = true;

    int topViewId;

    FragmentActivity activity;

    public Curtain(Fragment fragment) {
        this(fragment.getActivity());
    }

    public Curtain(FragmentActivity activity) {
        this.activity = activity;
        this.hollows = new SparseArray<>();
    }

    /** * @param which 页面上任一要高亮的view */
    public Curtain with(@NonNull View which) {
        getHollowInfo(which);
        return this;
    }

    /** * 设置自定义形状 * * @param which 目标view * @param shape 形状 */
    public Curtain withShape(@NonNull View which, Shape shape) {
        getHollowInfo(which).setShape(shape);
        return this;
    }

    /** * 自定义的引导页蒙层上层的元素 */
    public Curtain setTopView(@LayoutRes int layoutId) {
        this.topViewId = layoutId;
        return this;
    }

    public void show() {
        //载体dialog
        GuideDialogFragment guider = new GuideDialogFragment();
        guider.setTopViewRes(topViewId);
        //半透明蒙层View
        GuideView guideView = new GuideView(activity);
        //将透明区域设置蒙层VIew
        addHollows(guideView);
        guider.setGuideView(guideView);
        guider.show();
    }

    void addHollows(GuideView guideView) {
        HollowInfo[] tobeDraw = new HollowInfo[hollows.size()];
        for (int i = 0; i < hollows.size(); i++) {
            tobeDraw[i] = hollows.valueAt(i);
        }
        guideView.setHollowInfo(tobeDraw);
    }

    private HollowInfo getHollowInfo(View which) {
        HollowInfo info = hollows.get(which.hashCode());
        if (null == info) {
            info = new HollowInfo(which);
            info.targetView = which;
            hollows.append(which.hashCode(), info);
        }
        return info;
    }
}
复制代码

咱们能够看到经过构建者模式将一个个View封装为咱们最开始定义的HollowInfo,放入SparseArray,而后经过Show方法建立咱们的蒙层View,再构建咱们的载体,将他们合并起来.

咱们来个最终版调用: 先写一个顶部修饰TopView布局: view_guide_1.xml

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:background="#66000000" tools:ignore="HardcodedText">

    <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="140dp" android:layout_marginTop="300dp" android:text="自动识别View背景形状,也能够本身指定和定义高亮形状" android:textColor="#FFFFFF" android:textSize="18sp" android:textStyle="bold" />

</FrameLayout>
复制代码

而后结合起来使用:

private void showInitGuide() {
        new Curtain(SimpleGuideActivity.this)
                .with(findViewById(R.id.iv_guide_first))
                .setTopView(R.layout.view_guide_1)
                .show();
    }
复制代码

效果以下:

image

固然也能支持展现回调,在TopView中设置点击事件等等,细节上能够看看没有精简过的源码,这里就不贴出来了~

CurtainFlow

curtain实现了咱们一次高亮一个或者多个View的状况,可是实际业务场景每每很复杂,须要第一次高亮ViewA ,结束以后高亮ViewB,和ViewC,而后每次描述的文字或者元素都不同,以下:

image

咱们将每一步的Curtain对象放入一个流对象来管理,能够灵活进退,自由惯例,能够有效减小方法嵌套:

定义接口:

public interface CurtainFlowInterface {

    /** * 到下个 * 若是下个没有,即等于 finish() */
    void push();

    /** * 回到上个 */
    void pop();

    /** * 按照id 去某个节点 * * @param curtainId */
    void toCurtainById(int curtainId);

    /** * 找到当前展现curtain 中到view元素 */
    <T extends View> T findViewInCurrentCurtain(@IdRes int id);

    /** * 结束 */
    void finish();

}

复制代码

定了接口咱们大体知道能提供什么功能了,实现的话,咱们只须要吧Curtain对象放入其中进行管理便可,咱们看下使用流程:

/** * 第一步 高亮一个View */
    private static final int ID_STEP_1 = 1;

    /** * 第二步 高亮一个带圆形的View */
    private static final int ID_STEP_2 = 2;

    /** * 第三步 为一个View指定自定义的透明形状 */
    private static final int ID_STEP_3 = 3;

  private Curtain getStepOneGuide() {
        return new Curtain(CurtainFlowGuideActivity.this)
                .with(findViewById(R.id.iv_guide_first))
                .setTopView(R.layout.view_guide_flow1);
    }

    private Curtain getStepTwoGuide() {
        return new Curtain(CurtainFlowGuideActivity.this)
                .with(findViewById(R.id.btn_shape_circle))
                .setTopView(R.layout.view_guide_flow2);
    }

    private Curtain getStepThreeGuide() {
        return new Curtain(CurtainFlowGuideActivity.this)
                //自定义高亮形状
                .withShape(findViewById(R.id.btn_shape_custom), new RoundShape(12))
                //自定义高亮形状的Padding
                .withPadding(findViewById(R.id.btn_shape_custom), 24)
                .setTopView(R.layout.view_guide_flow3);
    }

复制代码

配合咱们的FLow:

private void showInitGuide() {
        new CurtainFlow.Builder()
                .with(ID_STEP_1, getStepOneGuide())
                .with(ID_STEP_2, getStepTwoGuide())
                .with(ID_STEP_3, getStepThreeGuide())
                .create()
                .start(new CurtainFlow.CallBack() {
                    @Override
                    public void onProcess(int currentId, final CurtainFlowInterface curtainFlow) {
                        switch (currentId) {
                            case ID_STEP_2:
                                //回到上个
                                curtainFlow.findViewInCurrentCurtain(R.id.tv_to_last)
                                        .setOnClickListener(new View.OnClickListener() {
                                            @Override
                                            public void onClick(View v) {
                                                curtainFlow.pop();
                                            }
                                        });
                                break;
                            case ID_STEP_3:
                                curtainFlow.findViewInCurrentCurtain(R.id.tv_to_last)
                                        .setOnClickListener(new View.OnClickListener() {
                                            @Override
                                            public void onClick(View v) {
                                                curtainFlow.pop();
                                            }
                                        });
                                //从新来一遍,即回到第一步
                                curtainFlow.findViewInCurrentCurtain(R.id.tv_retry)
                                        .setOnClickListener(new View.OnClickListener() {
                                            @Override
                                            public void onClick(View v) {
                                                curtainFlow.toCurtainById(ID_STEP_1);
                                            }
                                        });
                                break;
                        }
                        //去下一个
                        curtainFlow.findViewInCurrentCurtain(R.id.tv_to_next)
                                .setOnClickListener(new View.OnClickListener() {
                                    @Override
                                    public void onClick(View v) {
                                        curtainFlow.push();
                                    }
                                });
                    }

                    @Override
                    public void onFinish() {
                        Toast.makeText(CurtainFlowGuideActivity.this, "all flow ended", Toast.LENGTH_SHORT).show();
                    }
                });
    }

复制代码

CurtainFlow的实现源码我就不贴出来具体分析了,大体就是吧Curtain对象按照经过咱们在静态常量中定义的ID和和Curtain对象经过SparseArray管理起来,而后依次取出展现,你们有兴趣能够看看源码~

总结:

  • 一行代码完成某个View,或者多个View的高亮展现
  • 一样支持基于AapterView(如ListView、GridView等) 或RecyclerView 的item以及item中元素的高亮
  • 自动识别圆角背景,也能够自定义任何你想要的形状
  • 若是依次按顺序去高亮一些列View,提供流式操做

Github地址

相关文章
相关标签/搜索