天天看点

仿支付宝更多功能的实现

类似于功能中心这样的设计经常看到,公司这块的项目刚好有需求,自己仿着支付宝做了一下,实现的思路比较多,这里说下我的思路

先看下效果:

仿支付宝更多功能的实现

主要实现功能如下:

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. 注意一维数据和二维数据的转换

以上

继续阅读