Android——頁面Loading控件封裝

LoadingLayout效果

概述

在Android開發過程中通常在有網絡請求的頁面,需要設計加載中、加載失敗等UI效果,來提升用戶體驗。本文就此需求實現了一個簡單的LoadingLayout控件,可以比較方便的實現加載中、加載失敗、網絡錯誤等UI效果,並提供失敗點擊重試等操作。

實現思路

常用一般有以下幾種請求狀態:

  • LOADING_STATE 加載中狀態
  • LOAD_SUCCESS_STATE 加載成功狀態
  • LOAD_FAILURE_STATE 加載失敗狀態(包含超時、404、接口數據錯誤等)
  • NULL_DATA_STATE 沒有數據狀態(接口訪問成功,接口沒有數據)
  • NET_ERROR_STATE 網絡錯誤狀態(未連接網絡)

除加載成功狀態每種狀態都對應着一種UI效果對應各自View,不同狀態的View的展示和消失肯定是通過在ViewGroup添加和刪除View實現的,所以我們的LoadingLayout需要繼承一個ViewGroup,此ViewGroup我們選擇爲FrameLayout,不同狀態的View可以通過自定義屬性自由指定,然後通過開放接口實現切換UI的效果就可以了。

代碼實現

定義自定義屬性

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <item name="loading_layout_id" type="id" />

    <declare-styleable name="LoadingLayout">
        <attr name="loadingViewDrawable" format="reference"/>
        <attr name="loadFailureViewDrawable" format="reference"/>
        <attr name="netErrorViewDrawable" format="reference"/>
        <attr name="nullDataViewDrawable" format="reference"/>
    </declare-styleable>
</resources>

定義了一個默認ID和4中UI狀態的屬性。

java代碼實現

public class LoadingLayout extends FrameLayout {
    private static final String TAG = LoadingLayout.class.getSimpleName();

    /** 加載中 */
    public static final int LOADING_STATE = 0x001;
    /** 加載成功 */
    public static final int LOAD_SUCCESS_STATE = 0x002;
    /** 網絡錯誤 */
    public static final int NET_ERROR_STATE = 0x003;
    /** 加載失敗,超時、接口錯誤、404等 */
    public static final int LOAD_FAILURE_STATE = 0x004;
    /** 沒有數據 */
    public static final int NULL_DATA_STATE = 0x005;

    private View loadingView;       //loading 加載中。。
    private View netErrorView;      //網絡錯誤View
    private View loadFailureView;   //加載失敗View
    private View nullDataView;      //沒有數據View

    //定義註釋限定參數值,可以去掉
    @IntDef({LOADING_STATE, LOAD_SUCCESS_STATE, NET_ERROR_STATE, LOAD_FAILURE_STATE,NULL_DATA_STATE})
    public @interface LoadingState{}

    private final LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);

    public LoadingLayout(@NonNull Context context) {
        this(context, null);
    }

    public LoadingLayout(@NonNull  Context context, @NonNull  AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public LoadingLayout(@NonNull  Context context, @NonNull  AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        //指定默認ID
        if(getId() == -1){
            setId(R.id.loading_layout_id);
        }

        TypedArray a = context.obtainStyledAttributes(attrs,
                R.styleable.LoadingLayout, 0, 0);
        try{
            //解析自定義屬性,爲每種狀態指定默認效果
            int progressViewRes = a.getResourceId(R.styleable.LoadingLayout_loadingViewDrawable,
                    R.layout.default_loading_layout);
            int netErrorViewRes = a.getResourceId(R.styleable.LoadingLayout_netErrorViewDrawable,
                    R.layout.default_net_error_layout);
            int loadFailureViewRes = a.getResourceId(R.styleable.LoadingLayout_loadFailureViewDrawable,
                    R.layout.default_load_failure_layout);
            int nullDataViewRes = a.getResourceId(R.styleable.LoadingLayout_nullDataViewDrawable,
                    R.layout.default_null_data_layout);

            loadingView = LayoutInflater.from(context).inflate(progressViewRes, null, true);
            netErrorView = LayoutInflater.from(context).inflate(netErrorViewRes, null, true);
            loadFailureView = LayoutInflater.from(context).inflate(loadFailureViewRes, null, true);
            nullDataView = LayoutInflater.from(context).inflate(nullDataViewRes, null, true);
        } catch (Exception e){
            e.printStackTrace();
        }
        a.recycle();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        this.layoutParams.width = this.getMeasuredWidth();
        this.layoutParams.height = this.getMeasuredHeight();
    }


    /**
     * 通過狀態刷新界面顯示
     * @param state 當前狀態
     */
    public void refreshView(@LoadingState int state){
        refreshView(state, null);
    }

    /**
     * 通過狀態刷新界面顯示
     * @param state 當前狀態
     * @param onClickListener 點擊監聽
     */
    public void refreshView(@LoadingState int state, OnClickListener onClickListener){
        switch (state){
            case LOAD_SUCCESS_STATE:
                //加載成功,移除所有狀態View
                removeStateView();
                break;
            case LOADING_STATE:
                showLoading();
                break;
            case NET_ERROR_STATE:
                showNetErrorMessage(onClickListener);
                break;
            case LOAD_FAILURE_STATE:
                showLoadFailureMessage(onClickListener);
                break;
            case NULL_DATA_STATE:
                showNullDataMessage();
                break;
            default:
                Log.d(TAG, "state error");
                break;
        }
    }

    /**
     * 顯示loading
     */
    public void showLoading(){
        removeStateView();

        if(loadingView != null){
            this.addView(loadingView);
        } else{
            Log.d(TAG, "Please init loadingView first");
        }
    }

    /**
     * 顯示網絡錯誤提示
     */
    public void showNetErrorMessage(){
       showNetErrorMessage(null);
    }

    /**
     * 顯示網絡錯誤提示
     * @param onClickListener 指定點擊效果監聽
     */
    public void showNetErrorMessage(OnClickListener onClickListener){
        removeStateView();

        if(netErrorView != null){
            if(onClickListener != null){
                netErrorView.setOnClickListener(onClickListener);
            }
            this.addView(netErrorView);
        } else{
            Log.d(TAG, "Please init netErrorView first");
        }
    }

    /**
     * 顯示無數據提示
     */
    public void showNullDataMessage(){
        removeStateView();

        if(nullDataView != null){
            this.addView(nullDataView);
        } else{
            Log.d(TAG, "Please init nullDataView first");
        }
    }

    /**
     * 顯示加載失敗提示
     */
    public void showLoadFailureMessage(){
        showLoadFailureMessage(null);
    }

    /**
     * 顯示加載失敗提示
     * @param onClickListener 指定點擊效果監聽
     */
    public void showLoadFailureMessage(OnClickListener onClickListener){
        removeStateView();

        if(loadFailureView != null){
            if(onClickListener != null){
                loadFailureView.setOnClickListener(onClickListener);
            }

            this.addView(loadFailureView);
        } else{
            Log.d(TAG, "Please init loadFailureView first");
        }
    }

    /**
     * 移除所有狀態View
     */
    private void removeStateView(){
        if(loadingView != null){
            this.removeView(loadingView);
        }
        if(netErrorView != null){
            this.removeView(netErrorView);
        }
        if(loadFailureView != null){
            this.removeView(loadFailureView);
        }
        if(nullDataView != null){
            this.removeView(nullDataView);
        }
    }
}

代碼註釋比較詳細了,不過多解釋。

默認UI樣式

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/white">

    <ProgressBar
        android:layout_width="50dp"
        android:layout_height="50dp"
        style="@android:style/Widget.ProgressBar.Large"
        android:layout_centerInParent="true"
        android:indeterminateDrawable="@drawable/loading_anim"/>
</RelativeLayout>

loading_anim:

<rotate xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/loading"
    android:duration="100"
    android:fromDegrees="0"
    android:interpolator="@android:anim/linear_interpolator"
    android:pivotX="50%"
    android:pivotY="50%"
    android:toDegrees="720" />

其他的樣式比較簡單,就不一一列舉了。

使用

佈局:

    <com.zhong.loadingview.view.LoadingLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        app:layout_constraintBottom_toTopOf="@+id/net_error_btn"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" >

        <ImageView
            android:id="@+id/image_view"
            android:layout_width="200dp"
            android:layout_height="200dp"
            android:layout_gravity="center" />

    </com.zhong.loadingview.view.LoadingLayout>

需要在父佈局中引用自定義屬性LoadingLayout。

代碼實現:

在代碼中需要通過loadingLayout.showLoading();,loadingLayout.refreshView(state);等方法來控制狀態展示效果。

擴展

在寫完這個控件之後發現網上有好多類似的實現,好吧。。。至少說明我的實現思路應該是沒問題的。但是這種實現其實並不完美:

  • 每次使用時需要在父佈局中使用自定義佈局LoadingLayout略顯繁瑣;
  • 在某些特殊佈局中需要爲此效果單獨添加一層父佈局LoadingLayout,會在一定程度上造成資源上的浪費;
  • 因爲使用了自定義屬性和佈局指定默認UI樣式,也不利於維護和使用。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章