天天看點

仿支付寶更多功能的實作

類似于功能中心這樣的設計經常看到,公司這塊的項目剛好有需求,自己仿着支付寶做了一下,實作的思路比較多,這裡說下我的思路

先看下效果:

仿支付寶更多功能的實作

主要實作功能如下:

1. tab與recyclerview支援滑動關聯

2. 我的應用支援拖拽排序

3. 支援動态添加删除

總體上與支付寶是差不多的

思路如下:

布局分析:

仿支付寶更多功能的實作

布局層級如上圖:

xml如下:

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"

    >

    <com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay"
        android:background="@color/white"
        >

        <!--app:layout_scrollFlags="scroll|exitUntilCollapsed"-->
        <LinearLayout
            android:id="@+id/layout_app_section"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:background="@color/white"
            >

            <RelativeLayout
                android:layout_width="match_parent"
                android:layout_height="48dp"
                android:paddingLeft="@dimen/dp_16"
                android:paddingStart="@dimen/dp_16"
                android:paddingRight="@dimen/dp_16"
                android:paddingEnd="@dimen/dp_16"
                >

                <TextView
                    android:id="@+id/tv_header_name"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="我的應用"
                    android:background="@color/white"
                    android:textSize="@dimen/sp_18"
                    android:textStyle="bold"
                    android:layout_centerVertical="true"
                    android:textColor="@color/title_black"
                    >
                </TextView>

                <TextView
                    android:id="@+id/tv_edit_des"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="長按編輯,拖拽進行排序"
                    android:textColor="@color/textColorPrimary"
                    android:textSize="@dimen/sp_14"
                    android:layout_marginLeft="@dimen/dp_16"
                    android:layout_toRightOf="@id/tv_header_name"
                    android:layout_alignBottom="@id/tv_header_name"
                    android:visibility="visible"
                    />

                <TextView
                    android:id="@+id/tv_edit"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="編輯"
                    android:textColor="@color/colorPrimary"
                    android:textSize="@dimen/sp_18"
                    android:layout_alignParentEnd="true"
                    android:layout_alignParentRight="true"
                    android:layout_centerVertical="true"
                    />

            </RelativeLayout>

            <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/rv_app_section"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:nestedScrollingEnabled="false"
                android:overScrollMode="never"
                />

        </LinearLayout>

        <com.google.android.material.tabs.TabLayout
            android:id="@+id/tabLayout"
            android:layout_height="@dimen/dp_40"
            android:layout_width="match_parent"
            app:tabTextColor="@color/common_color"
            app:tabSelectedTextColor="@color/colorPrimary"
            app:tabIndicatorColor="@color/colorPrimary"
            app:tabBackground="@color/white"
            app:tabMode="scrollable"
            app:tabTextAppearance="@style/TabLayoutTextStyle"
            />
    </com.google.android.material.appbar.AppBarLayout>


    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
        android:scrollbars="vertical"
        />

</androidx.coordinatorlayout.widget.CoordinatorLayout>
           

這裡采用2個recyclerview,1個tablayout,根部局采用CoordinatorLayout,如果有tablayout懸停需求,可以指定 app:layout_scrollFlags="scroll|exitUntilCollapsed",xml中已做說明。

另外底部的RecyclerView帶有标題,又可以采用兩種方式:

1.RecyclerView嵌套RecyclerView

優點是相對比較容易與tablayout做關聯,但是滑動有卡頓

2. RecyclerView根據item 的不同,加載不同的item,采用一個RecyclerView

優點是滑動比較流暢,但是RecyclerView header的position需要和tablayout的标題相關聯,麻煩些

appItem布局比較簡單,就不再貼了

隻要UI控件确定了,其他就按此寫邏輯就可以了。

這裡主要示範一下第二種方式,體驗上好點:

Adapter采用的BaseRecyclerViewAdapterHelper  包括拖拽監聽等,這個比較簡單,照着官方demo就行,現在主要是tablayout與RecyclerView怎麼關聯起來

主界面代碼:

/**
 * section
 * 不采用RecyclerView嵌套RecyclerView方式
 * 單一RecyclerView
 */
class RecyclerFragment3 : Fragment() {

    /**
     * tab 的position和RecyclerView的Header的position的映射
     */
    private var posMap: MutableMap<Int, Int> = HashMap()

    private val divider by lazy {
        DividerItemDecoration(activity, DividerItemDecoration.VERTICAL)
    }

    private val mAdapter by lazy {
        SectionAdapter2(DataUtil.getSectionData2())
    }

    private val mmanager by lazy {
        GridLayoutManager(activity, 4)
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        // 參數:布局id,指定父布局協助生成布局參數,是否加載進父布局
        return inflater.inflate(R.layout.fragment_section, null)
    }

    companion object {
        fun getInstance() = RecyclerFragment3()
    }

    @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        DataUtil.getTabNames().forEachIndexed { index, it ->
            tab.addTab(tab.newTab().setText(it.header).setTag(it.subItemPos))
            posMap.put(it.subItemPos,index)
        }

        tab.addOnTabSelectedListener(tabSelectedListener)

        recycler.run {
            layoutManager = mmanager
            adapter = mAdapter
            mAdapter.onItemClickListener = listener
            addItemDecoration(divider)
            addOnScrollListener(scrollListener)

             // 添加footer,以便使tab能滑動到底
            post {
                val data = DataUtil.getSectionData()
                val lastAppSectionRowCount = Math.ceil(data[data.count() - 1].data.size / 4.0).toInt()
                val headerHeight = getChildAt(0).height
                val itemHeight = getChildAt(1).height
                val footviewHeight = height - (headerHeight + itemHeight * lastAppSectionRowCount)
                val footView = View(activity)
                footView.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, footviewHeight)
                mAdapter.addFooterView(footView)
            }
        }
    }

    private val listener = object : BaseQuickAdapter.OnItemClickListener {
        override fun onItemClick(adapter: BaseQuickAdapter<*, *>?, view: View?, position: Int) {
            Toast.makeText(activity, "pos:${position}", Toast.LENGTH_SHORT).show()
        }
    }

    private val scrollListener = object : RecyclerView.OnScrollListener() {
        override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
            val pos = posMap[mmanager.findFirstVisibleItemPosition()]
            if (pos != null) {
                tab.setScrollPosition(pos, 0f, true)
            }
        }
    }

    private val tabSelectedListener = object : TabLayout.OnTabSelectedListener {
        override fun onTabReselected(p0: TabLayout.Tab) {
        }

        override fun onTabUnselected(p0: TabLayout.Tab) {
        }

        override fun onTabSelected(tab: TabLayout.Tab) {
            mmanager.scrollToPositionWithOffset(tab.tag as Int, 0)
        }
    }
}
           

SectionAdapter代碼:

class SectionAdapter2(data: List<MySection>) :
    BaseSectionQuickAdapter<MySection, BaseViewHolder>(R.layout.item_section_item,R.layout.item_section_header, data) {
    override fun convertHead(helper: BaseViewHolder?, item: MySection?) {
        helper ?: return
        item ?: return
        helper.run {
            setText(R.id.tv_item_name, item.header)
        }
    }

    override fun convert(helper: BaseViewHolder, item: MySection) {
        val bean = item.t
        helper.run {
            setText(R.id.tv_item_name, bean.name)
            setImageResource(R.id.icon,R.mipmap.ic_launcher_round)
        }
    }
}
           

這裡直接繼承的官方的Adapter,其實就是根據不同布局類型加載對應資料的adapter

DataUtil代碼:

object DataUtil {

    private var sData: ArrayList<String>? = null
    private var sectionData: ArrayList<SectionBean>? = null
    private var tabNames: ArrayList<MySection>? = null
    private var sectionData2: ArrayList<MySection>? = null


    fun getStringData(): ArrayList<String> {
        if (sData == null) {
            sData = ArrayList()
            for (i in 0..20) {
                sData!!.add("第 $i 個條目")
            }
        }
        return sData!!
    }

    /**
     * 二維資料結構
     */
    fun getSectionData(): ArrayList<SectionBean> {
        if (sectionData == null) {
            sectionData = ArrayList()
            for (i in 0..8) {
                val items = ArrayList<ItemBean>()
                for (j in 0..7) {
                    items.add(ItemBean(R.mipmap.ic_launcher_round, "item$j"))
                }
                sectionData!!.add(SectionBean("區塊$i", items))
            }
        }
        return sectionData!!
    }

    /**
     * 一維資料結構
     */
    fun getSectionData2():ArrayList<MySection>{
        if (sectionData2 == null) {
            sectionData2 = ArrayList()
            val sectionData = getSectionData()
            var subItemPos = 0
            sectionData.forEach {
                sectionData2!!.add(MySection(header = it.title, isHeader = true, subItemPos = subItemPos))
                it.data.forEach {
                    sectionData2!!.add(MySection(it))
                }
                subItemPos += it.data.size + 1 // 轉化成一維清單時的position
            }
        }
        return sectionData2!!
    }

    fun getTabNames(): ArrayList<MySection> {
        if (tabNames == null) {
            tabNames = ArrayList()
            val sectionData = getSectionData()
            var subItemPos = 0
            sectionData.forEach {
                tabNames!!.add(MySection(header = it.title, isHeader = true, subItemPos = subItemPos))
                subItemPos += it.data.size + 1 // 轉化成一維清單時的position
            }
        }
        return tabNames!!
    }
}
           

DataBean相關代碼:

data class SectionBean(
    val title: String,
    val data: List<ItemBean>
)

data class ItemBean(
    @DrawableRes
    val icon_url: Int = R.mipmap.ic_launcher_round,
    val name: String
)

/**
 * section Header
 */
class MySection : SectionEntity<ItemBean> {
    var subItemPos = 0

    constructor(isHeader: Boolean = false, header: String, subItemPos: Int) : super(isHeader, header) {
        this.subItemPos = subItemPos
    }

    constructor(t: ItemBean) : super(t)
}
           

這裡需要注意的是使用BaseSectionQuickAdapter時對資料結構有一定要求。

MySection中的subItemPos字段就是header轉換為一維數組後的新的position。

這裡的兩個RecyclerView的添加删除我也沒有寫,都是對資料操作的事情,細心點問題應該都能解決,這裡有幾點需要注意:

1. 處于編輯模式後,item圖示的重新整理,就是顯示删除添加圖示,可以使用notifyItemChanged(),但是重新整理會有一些閃動,個人覺得直接循環更改圖示的樣式會好些。

2. 由于RecyclerView有複用,要多注意RecyclerView item根據不同狀态的顯示變化。

3. 為了使tablayout能滑動到最後,為RecyclerView添加了footview

4. 注意一維資料和二維資料的轉換

以上

繼續閱讀