Android 状态切换控件 EasyStateView

效果 GIF

简单介绍一下这个控件,像我们在实际的开发过程中,经常性的会遇到这样的场景,比如进入一个页面先出来加载动画,然后请求数据,如果网络异常就显示网络异常的布局,数据异常、数据为空也有相应的布局,以及当我们请求成功完毕数据后,根据返回的数据值去区分不同VIP等级的用户显示不同的页面,这里我放了两张图,我的女神,迪丽热巴和俞飞鸿,就当做我们在业务开发中的 Layout ,把布局全部写在 xml,然后控制显示隐藏就有点不优雅了,基于这个问题,就有了这个控件。

下面是自定义 View 的自定义属性:

<declare-styleable name="EasyStateView">

        // 是否使用过渡动画
        <attr name="esv_use_anim" format="boolean"/>

        // 加载动画 View
        <attr name="esv_loadingView" format="reference" />

        // 数据异常,加载失败 View
        <attr name="esv_errorDataView" format="reference" />

        // 网络异常 View
        <attr name="esv_errorNetView" format="reference" />

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

        // 设置当前显示的 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 {

    // 内容 View
    public static final int VIEW_CONTENT = 0;
    // 加载 View
    public static final int VIEW_LOADING = -1;
    // 数据异常( 数据异常指原本应该是有数据,但是服务器返回了错误的、不符合格式的数据 ) View
    public static final int VIEW_ERROR_DATA = -2;
    // 网络异常 View
    public static final int VIEW_ERROR_NET = -3;
    // 数据为空 View
    public static final int VIEW_EMPTY = -4;
    // View 的 Tag 标签值
    private static final int VIEW_TAG = -5;
    // 用来存放 View
    private SparseArray<View> mViews;
    // 是否使用过渡动画
    private boolean mUseAnim;
    // 是否处于动画中
    private boolean isAniming;
    // 当前显示的 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;
        }
        // 因为应用方向改变触发重绘后,重新初始化读取的 ViewState 是不准确的,所以要隐藏掉
        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,只能存储布尔数组,故改成 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];
            }
        };
    }

    /**
     * 切换默认状态的 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);
            }
        }
    }

    /**
     * 检查状态是否合法
     * true 表示不合法,不往下执行
     * false 表示该状态和当前状态不同,并合法数值状态
     *
     * @param state
     * @return
     */
    private boolean checkState(int state) {
        if (state <= VIEW_TAG) {
            throw new RuntimeException("ViewState 不在目标范围");
        }
        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;
        }
    }

    /**
     * 参数依次为:显示的状态、显示之后的状态码、要显示的 View、当前的 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 不在目标范围");
        }
        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会触发 requestLayout,addViewInLayout会先添加进去,然后再统一触发布局,这个控件的用法非常简单,控件里面已经内置了很多常用的场景类型,你可以通过 addUserView()这个方法来添加你的 View,目前只有一个过渡动画,后续考虑迭代。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343