Android 狀態(tài)切換控件 EasyStateView

效果 GIF

簡單介紹一下這個控件程储,像我們在實際的開發(fā)過程中章鲤,經(jīng)常性的會遇到這樣的場景,比如進(jìn)入一個頁面先出來加載動畫斟或,然后請求數(shù)據(jù)集嵌,如果網(wǎng)絡(luò)異常就顯示網(wǎng)絡(luò)異常的布局,數(shù)據(jù)異常怜珍、數(shù)據(jù)為空也有相應(yīng)的布局凤粗,以及當(dāng)我們請求成功完畢數(shù)據(jù)后嫌拣,根據(jù)返回的數(shù)據(jù)值去區(qū)分不同VIP等級的用戶顯示不同的頁面,這里我放了兩張圖捶索,我的女神灰瞻,迪麗熱巴和俞飛鴻辅甥,就當(dāng)做我們在業(yè)務(wù)開發(fā)中的 Layout 燎竖,把布局全部寫在 xml构回,然后控制顯示隱藏就有點不優(yōu)雅了,基于這個問題脐供,就有了這個控件茁肠。

下面是自定義 View 的自定義屬性:

<declare-styleable name="EasyStateView">

        // 是否使用過渡動畫
        <attr name="esv_use_anim" format="boolean"/>

        // 加載動畫 View
        <attr name="esv_loadingView" format="reference" />

        // 數(shù)據(jù)異常垦梆,加載失敗 View
        <attr name="esv_errorDataView" format="reference" />

        // 網(wǎng)絡(luò)異常 View
        <attr name="esv_errorNetView" format="reference" />

        // 空白頁面 View
        <attr name="esv_emptyView" format="reference" />

        // 設(shè)置當(dāng)前顯示的 viewState
        <attr name="esv_viewState" format="enum">
            <enum name="content" value="0" />
            <enum name="loading" value="-1" />
            <enum name="error_data" value="-2" />
            <enum name="error_net" value="-3" />
            <enum name="empty" value="-4" />
        </attr>

    </declare-styleable>

Java代碼:

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;

public class EasyStateView extends FrameLayout {

    // 內(nèi)容 View
    public static final int VIEW_CONTENT = 0;
    // 加載 View
    public static final int VIEW_LOADING = -1;
    // 數(shù)據(jù)異常( 數(shù)據(jù)異常指原本應(yīng)該是有數(shù)據(jù)托猩,但是服務(wù)器返回了錯誤的、不符合格式的數(shù)據(jù) ) View
    public static final int VIEW_ERROR_DATA = -2;
    // 網(wǎng)絡(luò)異常 View
    public static final int VIEW_ERROR_NET = -3;
    // 數(shù)據(jù)為空 View
    public static final int VIEW_EMPTY = -4;
    // View 的 Tag 標(biāo)簽值
    private static final int VIEW_TAG = -5;
    // 用來存放 View
    private SparseArray<View> mViews;
    // 是否使用過渡動畫
    private boolean mUseAnim;
    // 是否處于動畫中
    private boolean isAniming;
    // 當(dāng)前顯示的 ViewTag
    private int mCurrentState;
    private Context mContext;
    private StateViewListener mListener;
    // content View 是否被添加到隊列
    private boolean isAddContent;

    public interface StateViewListener {
        void onStateChanged(int state);
    }

    public void setStateChangedListener(StateViewListener listener) {
        this.mListener = listener;
    }

    public EasyStateView(Context context) {
        this(context, null);
    }

    public EasyStateView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public EasyStateView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {
        mContext = context;
        mViews = new SparseArray<>();
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.EasyStateView);
        mCurrentState = typedArray.getInt(R.styleable.EasyStateView_esv_viewState, VIEW_CONTENT);
        int emptyResId = typedArray.getResourceId(R.styleable.EasyStateView_esv_emptyView, VIEW_TAG);
        if (emptyResId != VIEW_TAG) {
            View view = LayoutInflater.from(getContext()).inflate(emptyResId, this, false);
            addViewToHash(view, VIEW_EMPTY);
            addViewInLayout(view, -1, view.getLayoutParams());
        }
        int errorDataResId = typedArray.getResourceId(R.styleable.EasyStateView_esv_errorDataView, VIEW_TAG);
        if (errorDataResId != VIEW_TAG) {
            View view = LayoutInflater.from(getContext()).inflate(errorDataResId, this, false);
            addViewToHash(view, VIEW_ERROR_DATA);
            addViewInLayout(view, -1, view.getLayoutParams());
        }
        int errorNetResId = typedArray.getResourceId(R.styleable.EasyStateView_esv_errorNetView, VIEW_TAG);
        if (errorNetResId != VIEW_TAG) {
            View view = LayoutInflater.from(getContext()).inflate(errorNetResId, this, false);
            addViewToHash(view, VIEW_ERROR_NET);
            addViewInLayout(view, -1, view.getLayoutParams());
        }
        int loadingResId = typedArray.getResourceId(R.styleable.EasyStateView_esv_loadingView, VIEW_TAG);
        if (loadingResId != VIEW_TAG) {
            View view = LayoutInflater.from(getContext()).inflate(loadingResId, this, false);
            addViewToHash(view, VIEW_LOADING);
            addViewInLayout(view, -1, view.getLayoutParams());
        }
        mUseAnim = typedArray.getBoolean(R.styleable.EasyStateView_esv_use_anim, true);
        typedArray.recycle();
    }

    @Override
    public void addView(View child) {
        addContentV(child);
        super.addView(child);
    }

    private boolean isContentView(View child) {
        if (!isAddContent && null != child
                && null == child.getTag()) {
            return true;
        }
        return false;
    }

    private void addContentV(View child) {
        if (isContentView(child)) {
            addViewToHash(child, VIEW_CONTENT);
            isAddContent = true;
        }
    }

    private void addViewToHash(View child, int viewTag) {
        child.setTag(viewTag);
        if (viewTag != mCurrentState) {
            child.setVisibility(GONE);
        }
        mViews.put(viewTag, child);
    }

    @Override
    public void addView(View child, int index) {
        addContentV(child);
        super.addView(child, index);
    }

    @Override
    public void addView(View child, int index, ViewGroup.LayoutParams params) {
        addContentV(child);
        super.addView(child, index, params);
    }

    @Override
    public void addView(View child, ViewGroup.LayoutParams params) {
        addContentV(child);
        super.addView(child, params);
    }

    @Override
    public void addView(View child, int width, int height) {
        addContentV(child);
        super.addView(child, width, height);
    }

    @Override
    protected boolean addViewInLayout(View child, int index, ViewGroup.LayoutParams params) {
        addContentV(child);
        return super.addViewInLayout(child, index, params);
    }

    @Override
    protected boolean addViewInLayout(View child, int index, ViewGroup.LayoutParams params, boolean preventRequestLayout) {
        addContentV(child);
        return super.addViewInLayout(child, index, params, preventRequestLayout);
    }


    @Override
    protected Parcelable onSaveInstanceState() {
        Parcelable parcelable = super.onSaveInstanceState();
        int useAnim = 0;
        if (mUseAnim) {
            useAnim = 1;
        }
        return new SaveState(parcelable, mCurrentState, useAnim);
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        SaveState saveState = (SaveState) state;
        if (saveState.useAnim == 1) {
            mUseAnim = true;
        } else {
            mUseAnim = false;
        }
        // 因為應(yīng)用方向改變觸發(fā)重繪后他宛,重新初始化讀取的 ViewState 是不準(zhǔn)確的欠气,所以要隱藏掉
        if (saveState.viewState != mCurrentState) {
            getStateView(mCurrentState).setVisibility(GONE);
            showViewState(saveState.viewState);
        }
        super.onRestoreInstanceState(saveState.getSuperState());
    }

    private static class SaveState extends BaseSavedState {

        private int viewState;
        /**
         * 布爾值存儲居然沒有api预柒,只能存儲布爾數(shù)組,故改成 int 記錄
         * 1 使用動畫
         * 2 不使用動畫
         */
        private int useAnim;

        private SaveState(Parcel source) {
            super(source);
            viewState = source.readInt();
        }

        private SaveState(Parcelable superState, int viewState, int useAnim) {
            super(superState);
            this.viewState = viewState;
            this.useAnim = useAnim;
        }

        @Override
        public void writeToParcel(Parcel out, int flags) {
            super.writeToParcel(out, flags);
            out.writeInt(viewState);
            out.writeInt(useAnim);
        }

        public static final Parcelable.Creator<SaveState> CREATE = new Parcelable.Creator<SaveState>() {

            @Override
            public SaveState createFromParcel(Parcel source) {
                return new SaveState(source);
            }

            @Override
            public SaveState[] newArray(int size) {
                return new SaveState[size];
            }
        };
    }

    /**
     * 切換默認(rèn)狀態(tài)的 View
     *
     * @param state
     */
    public void showViewState(int state) {
        if (!checkState(state)) {
            showViewAnim(state, VIEW_TAG);
        }
    }

    /**
     * 切換 view 時用 loading view 過渡
     *
     * @param state
     */
    public void afterLoadingState(int state) {
        if (!checkState(state)) {
            if (mCurrentState == VIEW_LOADING) {
                showViewAnim(state, VIEW_TAG);
            } else {
                showViewAnim(VIEW_LOADING, state);
            }
        }
    }

    /**
     * 檢查狀態(tài)是否合法
     * true 表示不合法,不往下執(zhí)行
     * false 表示該狀態(tài)和當(dāng)前狀態(tài)不同淋袖,并合法數(shù)值狀態(tài)
     *
     * @param state
     * @return
     */
    private boolean checkState(int state) {
        if (state <= VIEW_TAG) {
            throw new RuntimeException("ViewState 不在目標(biāo)范圍");
        }
        if (state == mCurrentState) {
            return true;
        } else if (isAniming) {
            return true;
        }
        return false;
    }

    public void setUseAnim(boolean useAnim) {
        this.mUseAnim = useAnim;
    }

    private void showViewAnim(int showState, int afterState) {
        if (!isAniming) {
            isAniming = true;
        }
        View showView = getStateView(showState);
        if (null == showView) {
            isAniming = false;
            return;
        }
        View currentView = getStateView(mCurrentState);
        if (mUseAnim) {
            showAlpha(showState, afterState, showView, currentView);
        } else {
            currentView.setVisibility(GONE);
            if (showView.getAlpha() == 0) {
                showView.setAlpha(1f);
            }
            showView.setVisibility(VISIBLE);
            mCurrentState = showState;
            if (null != mListener) {
                mListener.onStateChanged(showState);
            }
            isAniming = false;
        }
    }

    /**
     * 參數(shù)依次為:顯示的狀態(tài)适贸、顯示之后的狀態(tài)碼拜姿、要顯示的 View、當(dāng)前的 View
     *
     * @param showState
     * @param afterState
     * @param showView
     * @param currentView
     */
    private void showAlpha(final int showState, final int afterState, final View showView,
                           final View currentView) {
        ObjectAnimator currentAnim = ObjectAnimator.ofFloat(currentView, "alpha", 1, 0);
        currentAnim.setDuration(250L);
        final ObjectAnimator showAnim = ObjectAnimator.ofFloat(showView, "alpha", 0, 1);
        showAnim.setDuration(250L);
        showAnim.addListener(new AnimatorListenerAdapter() {

            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                if (null != mListener) {
                    mListener.onStateChanged(showState);
                }
                if (afterState != VIEW_TAG) {
                    showViewAnim(afterState, VIEW_TAG);
                } else {
                    isAniming = false;
                }
            }
        });
        currentAnim.addListener(new AnimatorListenerAdapter() {

            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                currentView.setVisibility(GONE);
                showView.setVisibility(VISIBLE);
                showAnim.start();
                mCurrentState = showState;
            }
        });
        currentAnim.start();
    }

    public int getCurrentState() {
        return mCurrentState;
    }

    public View getStateView(int state) {
        if (state <= VIEW_TAG) {
            throw new RuntimeException("ViewState 不在目標(biāo)范圍");
        }
        return mViews.get(state);
    }

    public void addUserView(int state, int layId) {
        setUserDefView(state, null, layId);
    }

    public void addUserView(int state, View view) {
        setUserDefView(state, view, -1);
    }

    private void setUserDefView(int state, View view, int layId) {
        if (state <= 0) {
            throw new RuntimeException("自定義的 ViewState TAG 必須大于 0");
        }
        if (null == view && layId != -1) {
            view = LayoutInflater.from(mContext).inflate(layId, this, false);
        }
        addViewToHash(view, state);
        addViewInLayout(view, -1, view.getLayoutParams());
    }

}

簡單說明一下,繼承 FrameLayout 是因為幀布局是效率最高的布局壁却,添加 View 到布局中用的是addViewInLayout展东,這里解釋一下為什么不用addView,因為addView會觸發(fā) requestLayout盐肃,addViewInLayout會先添加進(jìn)去砸王,然后再統(tǒng)一觸發(fā)布局,這個控件的用法非常簡單谦铃,控件里面已經(jīng)內(nèi)置了很多常用的場景類型,你可以通過 addUserView()這個方法來添加你的 View瘪菌,目前只有一個過渡動畫嘹朗,后續(xù)考慮迭代。

項目的 Github 地址 https://github.com/MarkRaoAndroid/EasyStateView

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末疆栏,一起剝皮案震驚了整個濱河市壁顶,隨后出現(xiàn)的幾起案子溜歪,更是在濱河造成了極大的恐慌,老刑警劉巖调衰,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異米酬,居然都是意外死亡趋箩,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門跳芳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來竹勉,“玉大人,你說我怎么就攤上這事吓歇∶适洌” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵析命,是天一觀的道長逃默。 經(jīng)常有香客問我,道長软吐,這世上最難降的妖魔是什么吟税? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任肠仪,我火速辦了婚禮,結(jié)果婚禮上异旧,老公的妹妹穿的比我還像新娘。我一直安慰自己荤崇,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布倚喂。 她就那樣靜靜地躺著喜每,像睡著了一般带兜。 火紅的嫁衣襯著肌膚如雪吨灭。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天宅粥,我揣著相機(jī)與錄音吠冤,去河邊找鬼。 笑死郭变,一個胖子當(dāng)著我的面吹牛涯保,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播夕春,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼及志,長吁一口氣:“原來是場噩夢啊……” “哼速侈!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起勇劣,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤幻捏,失蹤者是張志新(化名)和其女友劉穎篡九,沒想到半個月后醋奠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體窜司,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡金刁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年尤蛮,在試婚紗的時候發(fā)現(xiàn)自己被綠了产捞。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片坯临。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖焰雕,靈堂內(nèi)的尸體忽然破棺而出矩屁,到底是詐尸還是另有隱情吝秕,我是刑警寧澤烁峭,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站但两,受9級特大地震影響谨湘,放射性物質(zhì)發(fā)生泄漏紧阔。R本人自食惡果不足惜擅耽,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧航夺,春花似錦、人聲如沸始衅。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至钳恕,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間厘肮,已是汗流浹背睦番。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留巩检,地道東北人。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像厦瓢,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子劳跃,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,577評論 2 353