來摳個圖吧~——更優雅的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地址

相關文章
相關標籤/搜索