类似于功能中心这样的设计经常看到,公司这块的项目刚好有需求,自己仿着支付宝做了一下,实现的思路比较多,这里说下我的思路
先看下效果:
主要实现功能如下:
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. 注意一维数据和二维数据的转换
以上