Android 最简单的自定义视图管理之一

339 阅读8分钟

作者: Jooyer, 时间: 2018.08.08

Github地址,欢迎点赞,fork

其实一般的APP 都是这么几个状态,本次效果和其他开源没有什么区别,只是在方法数量和类数量上,和我之前博客一样,比较少! 这是自我吹捧,哈哈!

只需要三个类 + 几个 XML 文件即可,即拷即用

接下来我们依次讲解:

  1. StatusManager
  2. RootStatusLayout
  3. OnRetryListener
  4. 其他几个 XML 文件

首先我们看看 StatusManager:

/**
 * Desc: 视图管理器
 * Author: Jooyer
 * Date: 2018-07-30
 * Time: 11:46
 */
class StatusManager(builder: Builder) {

    var mContext: Context
    var mNetworkErrorVs: ViewStub? = null
    var mNetworkErrorView:View? = null
    var mNetWorkErrorRetryViewId: Int = 0
    var mEmptyDataVs: ViewStub? = null
    var mEmptyDataView:View? = null
    var mEmptyDataRetryViewId: Int = 0
    var mErrorVs: ViewStub? = null
    var mErrorView:View? = null
    var mErrorRetryViewId: Int = 0
    var mLoadingLayoutResId: Int = 0
    var mContentLayoutResId: Int = 0
    var mRetryViewId: Int = 0
    var mContentLayoutView: View? = null
    var mRootFrameLayout: RootStatusLayout? = null


    /**
     * 显示loading
     */
    fun showLoading() {
        mRootFrameLayout?.showLoading()
    }

    /**
     * 显示内容
     */
    fun showContent() {
        mRootFrameLayout?.showContent()
    }

    /**
     * 显示空数据
     */
    fun showEmptyData() {
        mRootFrameLayout?.showEmptyData()
    }

    /**
     * 显示网络异常
     */
    fun showNetWorkError() {
        mRootFrameLayout?.showNetWorkError()
    }

    /**
     * 显示异常
     */
    fun showError() {
        mRootFrameLayout?.showError()
    }

    /**
     * 得到root 布局
     */
    fun getRootLayout(): View {
        return mRootFrameLayout!!
    }


    class Builder(val context: Context) {
        var loadingLayoutResId: Int = 0
        var contentLayoutResId: Int = 0
        var contentLayoutView: View? = null
        var netWorkErrorVs: ViewStub? = null
        var netWorkErrorRetryViewId: Int = 0
        var emptyDataVs: ViewStub? = null
        var emptyDataRetryViewId: Int = 0
        var errorVs: ViewStub? = null
        var errorRetryViewId: Int = 0
        var retryViewId: Int = 0
     //   var onShowHideViewListener: OnShowOrHideViewListener? = null
        var onRetryListener: OnRetryListener? = null

        fun loadingView(@LayoutRes loadingLayoutResId: Int): Builder {
            this.loadingLayoutResId = loadingLayoutResId
            return this
        }

        fun netWorkErrorView(@LayoutRes newWorkErrorId: Int): Builder {
            netWorkErrorVs = ViewStub(context)
            netWorkErrorVs!!.layoutResource = newWorkErrorId
            return this
        }

        fun emptyDataView(@LayoutRes noDataViewId: Int): Builder {
            emptyDataVs = ViewStub(context)
            emptyDataVs!!.layoutResource = noDataViewId
            return this
        }

        fun errorView(@LayoutRes errorViewId: Int): Builder {
            errorVs = ViewStub(context)
            errorVs!!.layoutResource = errorViewId
            return this
        }

        fun contentView(contentLayoutView: View): Builder {
            this.contentLayoutView = contentLayoutView
            return this
        }

        fun contentViewResId(@LayoutRes contentLayoutResId: Int): Builder {
            this.contentLayoutResId = contentLayoutResId
            return this
        }

        fun netWorkErrorRetryViewId(netWorkErrorRetryViewId: Int): Builder {
            this.netWorkErrorRetryViewId = netWorkErrorRetryViewId
            return this
        }

        fun emptyDataRetryViewId(emptyDataRetryViewId: Int): Builder {
            this.emptyDataRetryViewId = emptyDataRetryViewId
            return this
        }

        fun errorRetryViewId(errorRetryViewId: Int): Builder {
            this.errorRetryViewId = errorRetryViewId
            return this
        }

        fun retryViewId(retryViewId: Int): Builder {
            this.retryViewId = retryViewId
            return this
        }

        fun onRetryListener(onRetryListener: OnRetryListener): Builder {
            this.onRetryListener = onRetryListener
            return this
        }

        fun build(): StatusManager {
            return StatusManager(this)
        }
    }


    companion object {
        fun newBuilder(context: Context): Builder {
            return Builder(context)
        }
    }

    init {
        mContext = builder.context
        mLoadingLayoutResId = builder.loadingLayoutResId
        mNetworkErrorVs = builder.netWorkErrorVs
        mNetWorkErrorRetryViewId = builder.netWorkErrorRetryViewId
        mEmptyDataVs = builder.emptyDataVs
        mEmptyDataRetryViewId = builder.emptyDataRetryViewId
        mErrorVs = builder.errorVs
        mErrorRetryViewId = builder.errorRetryViewId
        mContentLayoutResId = builder.contentLayoutResId
        mRetryViewId = builder.retryViewId
        mContentLayoutView = builder.contentLayoutView
        mRootFrameLayout = RootStatusLayout(mContext)
        mRootFrameLayout!!.setStatusManager(this)
        mRootFrameLayout!!.setOnRetryListener(builder.onRetryListener)
    }
}

使用建造者模式, 初始化必要的布局信息和视图控件,同时使用 ViewStub 优化异常View

然后我们看看 RootStatusLayout :

/**
 * Desc: 视图管理布局控件
 * Author: Jooyer
 * Date: 2018-07-30
 * Time: 11:23
 */
class RootStatusLayout(context: Context?, attrs: AttributeSet?, defStyleAttr: Int)
    : ConstraintLayout(context, attrs, defStyleAttr) {

    constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)

    constructor(context: Context?) : this(context, null, 0)

    /**
     * loading 加载id
     */
    val LAYOUT_LOADING_ID = 1

    /**
     * 内容id
     */
    val LAYOUT_CONTENT_ID = 2

    /**
     * 异常id
     */
    val LAYOUT_ERROR_ID = 3

    /**
     * 网络异常id
     */
    val LAYOUT_NETWORK_ERROR_ID = 4

    /**
     * 空数据id
     */
    val LAYOUT_EMPTY_ID = 5

    /**
     * 存放布局集合
     */
    private val mLayoutViews = SparseArray<View>()

    /**
     * 视图管理器
     */
    private var mStatusLayoutManager: StatusManager? = null

    /**
     * 不同视图的切换
     */
//    private var onShowHideViewListener: OnShowOrHideViewListener? = null

    /**
     * 点击重试按钮回调
     */
    private var onRetryListener: OnRetryListener? = null


    fun setStatusManager(manager: StatusManager) {
        mStatusLayoutManager = manager
        addAllLayoutViewsToRoot()
    }

    private fun addAllLayoutViewsToRoot() {
        val params = ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT)

        if (0 != mStatusLayoutManager?.mContentLayoutResId) {
            addLayoutResId(mStatusLayoutManager?.mContentLayoutResId!!, LAYOUT_CONTENT_ID, params)
        } else if (null != mStatusLayoutManager?.mContentLayoutView) {
            addLayoutView(mStatusLayoutManager?.mContentLayoutView!!, LAYOUT_CONTENT_ID, params)
        }

        if (0 != mStatusLayoutManager?.mLoadingLayoutResId) {
            addLayoutResId(mStatusLayoutManager?.mLoadingLayoutResId!!, LAYOUT_LOADING_ID, params)
        }

        if (null != mStatusLayoutManager?.mEmptyDataVs) {
            addView(mStatusLayoutManager?.mEmptyDataVs, params)
        }

        if (null != mStatusLayoutManager?.mErrorVs) {
            addView(mStatusLayoutManager?.mErrorVs, params)
        }

        if (null != mStatusLayoutManager?.mNetworkErrorVs) {
            addView(mStatusLayoutManager?.mNetworkErrorVs, params)
        }

    }

    private fun addLayoutView(layoutView: View, layoutId: Int, param: ViewGroup.LayoutParams) {
        mLayoutViews.put(layoutId, layoutView)
        addView(layoutView, param)
    }

    private fun addLayoutResId(@LayoutRes layoutResId: Int, layoutId: Int, param: ViewGroup.LayoutParams) {
        val view: View = LayoutInflater.from(context)
                .inflate(layoutResId, null)
        mLayoutViews.put(layoutId, view)
        if (LAYOUT_LOADING_ID == layoutId) {
            view.visibility = View.GONE
        }
        addView(view, param)
    }

    /**
     * 显示loading
     */
    fun showLoading() {
        if (mLayoutViews.get(LAYOUT_LOADING_ID) != null)
            showHideViewById(LAYOUT_LOADING_ID)
    }

    /**
     * 显示内容
     */
    fun showContent() {
        if (mLayoutViews.get(LAYOUT_CONTENT_ID) != null)
            showHideViewById(LAYOUT_CONTENT_ID)
    }

    /**
     * 显示空数据
     */
    fun showEmptyData() {
        if (inflateLayout(LAYOUT_EMPTY_ID))
            showHideViewById(LAYOUT_EMPTY_ID)
    }

    /**
     * 显示网络异常
     */
    fun showNetWorkError() {
        if (inflateLayout(LAYOUT_NETWORK_ERROR_ID))
            showHideViewById(LAYOUT_NETWORK_ERROR_ID)
    }

    /**
     * 显示异常
     */
    fun showError() {
        if (inflateLayout(LAYOUT_ERROR_ID))
            showHideViewById(LAYOUT_ERROR_ID)
    }

    private fun showHideViewById(layoutId: Int) {
        for (i in 0 until mLayoutViews.size()) {
            val key = mLayoutViews.keyAt(i)
            val value = mLayoutViews[key]

            // 显示该 View
            if (layoutId == key) {
                value.visibility = View.VISIBLE
            } else {
                if (View.GONE != value.visibility) {
                    value.visibility = View.GONE
                }
            }
        }
    }

    fun setOnRetryListener(listener: OnRetryListener?) {
        onRetryListener = listener
    }

    /**
     * 加载 StubView
     */
    private fun inflateLayout(layoutId: Int): Boolean {
        var isShow = true
        when (layoutId) {
            LAYOUT_NETWORK_ERROR_ID -> {
                isShow = when {
                    null != mStatusLayoutManager?.mNetworkErrorView -> {
                        retryLoad(mStatusLayoutManager?.mNetworkErrorView!!, mStatusLayoutManager?.mNetWorkErrorRetryViewId!!)
                        mLayoutViews.put(layoutId, mStatusLayoutManager?.mNetworkErrorView!!)
                        return true
                    }
                    null != mStatusLayoutManager?.mNetworkErrorVs -> {
                        val view: View = mStatusLayoutManager?.mNetworkErrorVs!!.inflate()
                        mStatusLayoutManager?.mNetworkErrorView = view
                        retryLoad(view, mStatusLayoutManager?.mNetWorkErrorRetryViewId!!)
                        mLayoutViews.put(layoutId, view)
                        true
                    }
                    else -> false
                }
            }
            LAYOUT_ERROR_ID -> {
                isShow = when {
                    null != mStatusLayoutManager?.mErrorView -> {
                        retryLoad(mStatusLayoutManager?.mErrorView!!, mStatusLayoutManager?.mErrorRetryViewId!!)
                        mLayoutViews.put(layoutId, mStatusLayoutManager?.mErrorView!!)
                        return true
                    }
                    null != mStatusLayoutManager?.mErrorVs -> {
                        val view: View = mStatusLayoutManager?.mErrorVs!!.inflate()
                        mStatusLayoutManager?.mErrorView = view
                        retryLoad(view, mStatusLayoutManager?.mErrorRetryViewId!!)
                        mLayoutViews.put(layoutId, view)
                        true
                    }
                    else -> false
                }
            }
            LAYOUT_EMPTY_ID -> {
                isShow = when {
                    null != mStatusLayoutManager?.mEmptyDataView -> {
                        retryLoad(mStatusLayoutManager?.mEmptyDataView!!, mStatusLayoutManager?.mEmptyDataRetryViewId!!)
                        mLayoutViews.put(layoutId, mStatusLayoutManager?.mEmptyDataView!!)
                        return true
                    }
                    null != mStatusLayoutManager?.mEmptyDataVs -> {
                        val view: View = mStatusLayoutManager?.mEmptyDataVs!!.inflate()
                        mStatusLayoutManager?.mEmptyDataView = view
                        retryLoad(view, mStatusLayoutManager?.mEmptyDataRetryViewId!!)
                        mLayoutViews.put(layoutId, view)
                        true
                    }
                    else -> false
                }
            }
        }
        return isShow
    }

    /**
     *  加载重试按钮,并绑定监听 
     */
    private fun retryLoad(view: View, layoutResId: Int) {
        val retryView: View? = view.findViewById(
                if (0 != mStatusLayoutManager?.mRetryViewId!!) {
                    mStatusLayoutManager?.mRetryViewId!!
                } else {
                    layoutResId
                }) ?: return
        retryView?.setOnClickListener {
            onRetryListener?.onRetry()
        }
    }
}

加载必要是视图到布局中,并根据需要显示和隐藏相关 View,逻辑也是很简单,哈哈!

接着看看最后一个类,其实就是个回调...

/**
 * Desc: 数据异常处理时点击回调
 * Author: Jooyer
 * Date: 2018-07-30
 * Time: 11:22
 */
interface OnRetryListener{
    fun onRetry()
}

这个也占了一个类,我喜欢这样,两个字 --> 任性

最后就是几个我就一股脑都抛出来了,准备接招!!! 你喜欢可以随意定制,咋舒服咋来.

widget_empty_page.xml -----> 没有数据时使用

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical"
    android:visibility="visible">


    <ImageView
        android:id="@+id/empty_img_status"
        android:layout_width="@dimen/width_100"
        android:layout_height="@dimen/height_100"
        android:background="@android:color/holo_blue_bright"
        />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/padding_10"
        android:gravity="center"
        android:textSize="@dimen/textSize_16"
        android:text="暂时没有数据! "
        />


</LinearLayout>

widget_error_page.xml -----> 异常(数据解析异常,服务器500等) 时使用

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical"
    android:visibility="visible">

    <ImageView
        android:id="@+id/error_img_status"
        android:layout_width="@dimen/width_100"
        android:layout_height="@dimen/height_100"
        android:background="@android:color/holo_orange_dark"
        />

    <TextView
        android:id="@+id/error_text_status"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="@dimen/padding_10"
        android:layout_marginTop="@dimen/padding_10"
        android:text="出错了..."
        />

    <TextView
        android:id="@+id/tv_retry_status"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:clickable="true"
        android:gravity="center"
        android:paddingBottom="@dimen/padding_6"
        android:paddingLeft="@dimen/padding_18"
        android:paddingRight="@dimen/padding_18"
        android:paddingTop="@dimen/padding_6"
        android:text="点击重试! "
        />


</LinearLayout>

widget_nonetwork_page.xml -----> 网络异常时使用

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical"
    android:visibility="visible">

    <ImageView
        android:id="@+id/no_network_img_status"
        android:layout_width="@dimen/width_100"
        android:layout_height="@dimen/height_100"
        android:background="@android:color/holo_green_dark"
        />

    <TextView
        android:id="@+id/no_network_text_status"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="@dimen/padding_10"
        android:layout_marginTop="@dimen/padding_10"
        android:text="网络错误!..."
        />

    <TextView
        android:id="@+id/tv_retry_status"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:clickable="true"
        android:gravity="center"
        android:paddingBottom="@dimen/padding_6"
        android:paddingLeft="@dimen/padding_18"
        android:paddingRight="@dimen/padding_18"
        android:paddingTop="@dimen/padding_6"
        android:text="点击重试"
        />


</LinearLayout>

widget_progress_bar.xml ----->加载数据时使用

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:gravity="center"
              android:orientation="vertical">

    <ProgressBar
        android:id="@+id/progress_bar_status"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        />

</LinearLayout>

以上就是全部了,如果小伙伴发现有不能运行的,请看下面这个文件,它是在 values 内哦

dimens.xml -----> 定义了视图大小

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <dimen name="width_100">100dp</dimen>
    
    <dimen name="height_100">100dp</dimen>
    
    <dimen name="padding_18">18dp</dimen>
    <dimen name="padding_10">10dp</dimen>
    <dimen name="padding_6">6dp</dimen>
    
    <dimen name="textSize_16">16sp</dimen>

</resources>

这次真的是全部了, 下面来介绍用法.

一般我们都需要定义基类,所以本次的视图管理器也在基类中,请看:

BaseActivity

/**
 * Desc: Activity 基类
 * Author: Jooyer
 * Date: 2018-08-04
 * Time: 21:22
 */
abstract class BaseActivity :AppCompatActivity(), OnRetryListener {
    /**
     * 请求网络异常等界面管理
     */
    var mStatusManager: StatusManager? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        if (0 != getLayoutId()) {
            setContentView(initStatusManager(savedInstanceState))
        }
    }
	
    /**
     * Activity 布局文件 
     */
    abstract fun getLayoutId(): Int

    /**
     * 初始化 View
     */
    abstract fun initializedViews(savedInstanceState: Bundle?, contentView: View)


    private fun initStatusManager(savedInstanceState: Bundle?): View {
        if (0 != getLayoutId()) {
            val contentView = LayoutInflater.from(this)
                    .inflate(getLayoutId(), null)
            initializedViews(savedInstanceState, contentView)
            return if (useStatusManager()) {
                initialized(contentView)
            } else {
                contentView.visibility = View.VISIBLE
                contentView
            }
        }
        throw IllegalStateException("getLayoutId() 必须调用,且返回正常的布局ID")
    }


    private fun initialized(contentView: View): View {
        mStatusManager = StatusManager.newBuilder(this)
                .contentView(contentView)
                .loadingView(R.layout.widget_progress_bar)
                .emptyDataView(R.layout.widget_empty_page)
                .netWorkErrorView(R.layout.widget_nonetwork_page)
                .errorView(R.layout.widget_error_page)
                .retryViewId(R.id.tv_retry_status)  // 注意以上布局中如果有重试ID,则必须一样,ID名称随意,记得这里填写正确
                .onRetryListener(this)
                .build()
        mStatusManager?.showLoading()
        return mStatusManager?.getRootLayout()!!
    }


    /**
     *  是否使用视图布局管理器,默认不使用
     */
    open fun useStatusManager(): Boolean {
        return false
    }

    /**
     * 点击视图中重试按钮
     */
    override fun onRetry() {
        Toast.makeText(this,"如果需要点击重试,则重写 onRetry() 方法",
                Toast.LENGTH_SHORT).show()
    }

}

继续

BaseFragment

/**
 * Desc: Fragment 基类
 * Author: Jooyer
 * Date: 2018-08-04
 * Time: 21:23
 */
abstract class BaseFragment: Fragment(), OnRetryListener {

    /**
     * 请求网络异常等界面管理
     */
    var mStatusManager: StatusManager? = null

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return initStatusManager(inflater, container, savedInstanceState)
    }


    /**
     * Fragment 布局文件 
     */
    abstract fun getLayoutId(): Int

    abstract fun initializedViews(savedInstanceState: Bundle?, contentView: View)

    /**
     * 此函数开始数据加载的操作,且仅调用一次
     * 主要是加载动画,初始化展示数据的布局
     */
    private fun initStatusManager(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        if (0 != getLayoutId()) {
            val contentView = inflater.inflate(getLayoutId(), container, false)
            initializedViews(savedInstanceState, contentView)
            return if (useStatusManager()) {
                initialized(contentView)
            } else {
                contentView.visibility = View.VISIBLE
                contentView
            }

        }
        throw IllegalStateException("getLayoutId() 必须调用,且返回正常的布局ID")
    }


    private fun initialized(contentView: View): View {
        mStatusManager = StatusManager.newBuilder(contentView.context)
                .contentView(contentView)
                .loadingView(R.layout.widget_progress_bar)
                .emptyDataView(R.layout.widget_empty_page)
                .netWorkErrorView(R.layout.widget_nonetwork_page)
                .errorView(R.layout.widget_error_page)
                .retryViewId(R.id.tv_retry_status) // 注意以上布局中如果有重试ID,则必须一样,ID名称随意,记得这里填写正确
                .onRetryListener(this)
                .build()
        mStatusManager?.showLoading()
        return mStatusManager?.getRootLayout()!!
    }


    /**
     *  是否使用视图布局管理器,默认不使用
     */
    open fun useStatusManager(): Boolean {
        return false
    }

    /**
     * 点击视图中重试按钮
     */
    override fun onRetry() {
		Toast.makeText(this,"如果需要点击重试,则重写 onRetry() 方法",
                Toast.LENGTH_SHORT).show()
    }
}

PS: onRetry() 的 Toast 仅仅演示用的,具体逻辑,请重写此方法来处理...

最后看看在 Activity 的用法吧, Fragment 类似,如果小伙伴在 Fragment 中不会使用或者有其他疑问请留言!

class MainActivity : BaseActivity() {
    override fun getLayoutId(): Int {
        return R.layout.activity_main
    }

	// 仅仅演示如何通过 findViewById 找到控件,其实 Kotlin 不用这么麻烦
    override fun initializedViews(savedInstanceState: Bundle?, contentView: View) {
        val tv_main_activity = contentView.findViewById<TextView>(R.id.tv_main_activity)
    }

    // 如果使用 StateManager 必须重写下面方法,且返回 true
    // 如果返回 true 则会显示加载的 loading
    override fun useStatusManager(): Boolean {
        return true
    }

    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        menuInflater.inflate(R.menu.state_menu, menu)
        return true
    }

    // 下面展示了显示各个状态的方式
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when (item.itemId) {
            R.id.option_empty_page -> {
                mStatusManager?.showEmptyData()
                return true
            }
            R.id.option_error_page -> {
                mStatusManager?.showError()
                return true
            }
            R.id.option_loading_page -> {
                mStatusManager?.showLoading()
                return true
            }
            R.id.option_network_page -> {
                mStatusManager?.showNetWorkError()
                return true
            }
            R.id.option_content_page -> {
                mStatusManager?.showContent()
                return true
            }
            else -> return super.onOptionsItemSelected(item)
        }

    }

// 这个仅仅演示加载 Framgment ,和本例没有什么关系
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        supportFragmentManager.beginTransaction()
                .add(R.id.fl_container,MainFragment())
                .commit()
    }
}

哈哈,我觉得很简单啊,小伙伴们觉得呢?虽然简单不过它可以满足基本的需求哦!还可以随意定制!喜欢记得点赞,收藏,转发哈!

膜拜的大神:

实在抱歉,记得有看到过大神有类似思路的,只是想不起了,如果大神看到,请通知我,我会在这里写上大神的博客