文章目录

  • 1、修改SuspensionDecoration类,完善悬浮头效果
  • 2、修改MainActivity的布局文件,查看界面效果
  • 3、设置字母导航条的回调函数,完成联动功能



  目标效果如下。


android按字母索引 android字母索引 联动_android


实现步骤:

  1. 修改SuspensionDecoration类,完善悬浮头效果
  2. 修改MainActivity的布局文件,查看界面效果
  3. 设置字母导航条的回调函数,完成联动功能

1、修改SuspensionDecoration类,完善悬浮头效果

  首先改动下SuspensionDecoration这个类,上一篇是理想状态下的效果,但是在快速滑动列表的后并不能保证图片准确的生成。并且在滑动字母导航条后也会产生有些图片不生成和错乱的效果。所以这里需要改动一下,以下把整个类代码贴了出来。

import android.graphics.*
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.widget.LinearLayout
import android.widget.TextView
import androidx.annotation.IdRes
import androidx.annotation.LayoutRes
import androidx.recyclerview.widget.RecyclerView

/**
 * 带有悬浮效果的装饰器
 */
class SuspensionDecoration(
    private val titles: List<String>,
    @IdRes private val titleResId: Int,
    private val headHeight: Int,
    @LayoutRes private val headLayout: Int
) :
    RecyclerView.ItemDecoration() {

    //添加默认悬浮物
    private val defaultKey = "default"
    //保存当前绘制的头部
    private var nowHeaderKey: String = defaultKey
    //保存当前展示的列表分组条目快照
    private val headDic = mutableMapOf<String, Bitmap>().apply {
        put(nowHeaderKey, Bitmap.createBitmap(headHeight, headHeight, Bitmap.Config.ARGB_8888))
    }
    //保存标题
    private val headLabel by lazy { mutableListOf(defaultKey).apply { addAll(titles) } }

    /**
     * 绘制悬浮层(绘制的图像将会呈现在每个条目的上层)
     */
    override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        super.onDrawOver(c, parent, state)
        //第一次绘制将会触发获取所有图片
        if (headDic.size == 1) {
            getViewBitmaps(parent)
        }
        //列表不一定有头部 所以循坏遍历查询第一个头部
        var headerItemView: View? = null
        for (position in 0 until parent.childCount) {
            if (null != parent.getChildAt(position).findHeaderView()) {
                break
            }
        }
        if (null == headerItemView) {
            headerItemView = parent.getChildAt(0)
        }
        //处理没有子控件的情况
        if (null == headerItemView) {
            return
        }
        //寻找标题头
        headerItemView.findHeaderView()?.let { nowHeaderKey = it.text.toString() }
        //通过改变偏移量到达悬浮头部的动态效果
        val offset = observeNextHeader(parent)
        //观察上一个标题(处理显示效果)
        observeLastHeader(parent)
        //绘制悬浮头
        val stickyRect = Rect(0, offset, parent.right, headHeight)//绘制图片显示的区域
        val drawRect = Rect(0, 0, parent.right, headHeight - offset)//当前悬浮头可绘制区域
        c.drawBitmap(headDic[nowHeaderKey]!!, stickyRect, drawRect, null)
    }

    /**
     * 观察上一个标题头
     */
    private fun observeLastHeader(parent: RecyclerView) {
        //找出当前显示的标题(只找第一个)
        for (index in (0 until parent.childCount)) {
            val childView = parent.getChildAt(index)
            val headText = childView.findHeaderView()
            if (null != headText) {
                val position = headLabel.indexOf(headText.text)//获取标题的索引
                val offset = childView.top//获取偏移量
                if (offset > 0 && position > 0) {
                    nowHeaderKey = headLabel[position - 1]
                }
                return
            }
        }
    }


    /**
     * 观察下一个标题头
     */
    private fun observeNextHeader(parent: RecyclerView): Int {
        var nextHeaderView: View? = null
        for (index in (1 until parent.childCount)) {
            val childView = parent.getChildAt(index)
            if (null != childView.findHeaderView()) {
                nextHeaderView = childView
                break
            }
        }
        if (null == nextHeaderView) {
            return 0
        }
        //距离顶部高度
        val top = nextHeaderView.top
        //计算偏移量
        if (top in 0 until headHeight) {
            return headHeight - top
        }
        return 0
    }


    /**
     * 获取头部
     */
    private fun View.findHeaderView() = findViewById<TextView>(titleResId)

    /**
     * 快照
     */
    private fun getViewBitmaps(parent: RecyclerView) {
        val view = LayoutInflater.from(parent.context).inflate(headLayout, null)
        val headTitle = view.findViewById<TextView>(titleResId)
        headTitle.gravity = Gravity.CENTER_VERTICAL
        headTitle.layoutParams = LinearLayout.LayoutParams(parent.width, headHeight)
        //调用layout方法布局后,可以得到view的尺寸大小
        view.measure(parent.width, headHeight)
        view.layout(0, 0, parent.width, headHeight)
        //将所有悬浮图片都生成出来
        (1 until headLabel.size).forEach { index ->
            val title = headLabel[index]
            headTitle.text = title
            val bitmap = Bitmap.createBitmap(parent.width, headHeight, Bitmap.Config.RGB_565)
            val canvas = Canvas(bitmap)
            view.draw(canvas)
            headDic[title] = bitmap
        }
        headTitle.text = nowHeaderKey
    }

}

  在SuspensionDecoration类中主要的改动就是获取图片部分,此处将不再是滑动触发获取分组条目的快照,而是在第一次触发绘制的时候将所有分组条目的快照生成出来并保存到map集合中。获取图片请参考getViewBitmaps()这个方法。经过这次改动,这个装饰器才算是大功告成了。

2、修改MainActivity的布局文件,查看界面效果

  界面布局如下,一个列表加一个字母检索导航条:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/listView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.lyan.sidelistdemo.LetterNavigationView
        android:id="@+id/sideBar"
        android:layout_width="100dp"
        android:layout_height="0dp"
        android:layout_margin="4dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

  MainActivity界面也稍微做了点改动。因为SuspensionDecoration与分组列表有关联性。所以将SuspensionDecoration的加载和移除都放到了loadingTestDataToListView()方法中,目的是为了数据刷新的时候还能正确的显示悬浮头部效果(这里懒了没有测试- -!)。

import android.graphics.Color
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.text.TextUtils
import androidx.recyclerview.widget.LinearLayoutManager
import com.blankj.utilcode.util.SizeUtils
import com.blankj.utilcode.util.ToastUtils
import com.blankj.utilcode.util.Utils
import com.fondesa.recyclerviewdivider.RecyclerViewDivider
import com.lyan.sidelistdemo.Tool.toPinyinWithCheck
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    //适配器装载的数据
    private val itemData by lazy {
        mutableListOf(
            ItemModel(Item("群组一"), ItemModel.GROUP),
            ItemModel(Item("群组二"), ItemModel.GROUP)
        )
    }
    //列表的适配器
    private val itemAdapter by lazy { ItemAdapter(itemData) }

    //创建视图初始化相关操作
    override fun onCreate(savedInstanceState: Bundle?) {
        //初始化工具类
        Utils.init(this.applicationContext)
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        //item点击事件
        itemAdapter.setOnItemClickListener { _, _, position ->
            if (!itemData[position].isHeader) {
                ToastUtils.showShort(itemData[position].t.title)
            }
        }
        //是指列表展示效果绑定适配器
        listView.layoutManager = LinearLayoutManager(this)
        listView.adapter = itemAdapter
        //设置列表的分割线
        RecyclerViewDivider.with(this).color(Color.parseColor("#C3C3C3"))
            .size(2).build().addTo(listView)
        //加载假数据
        loadingTestDataToListView()
    }

    //吸顶悬浮头的效果
    private lateinit var suspensionDecoration: SuspensionDecoration

    /**
     * 加载假数据(模拟网络请求,获取数据源)
     */
    private fun loadingTestDataToListView() {
        val lastItem = "#"
        //记录列表刷新其实位置s
        val startPosition = itemData.size
        //对名字列表进行分组
        val groupByResult = Tool.nameArray
            .groupBy { toPinyinWithCheck(it) }.toSortedMap()
        //遍历插值(放入每个分组列表中的人名)
        val forEachChildList = fun(nameArray: List<String>) {
            nameArray.forEach { name -> itemData.add(ItemModel(Item(name))) }
        }
        //添加分组表头
        groupByResult.mapKeys { item ->
            if (item.key != lastItem) {
                itemData.add(ItemModel(item.key.toString()))
                forEachChildList(item.value)
            }
        }
        //添加其它项
        groupByResult[lastItem]?.let {
            itemData.add(ItemModel(lastItem))
            forEachChildList(it)
        }
        //标题
        val titleArray = mutableListOf<String>().apply {
            addAll(groupByResult.keys.toList().filter { !TextUtils.equals(lastItem, it) })
            add(lastItem)
        }

        //悬浮头效果
        suspensionDecoration =
            SuspensionDecoration(
                titleArray,
                R.id.headTitle,
                SizeUtils.dp2px(30f),
                R.layout.item_header
            )
        listView.removeItemDecoration(suspensionDecoration)
        //刷新列表
        itemAdapter.notifyItemRangeChanged(startPosition, Tool.nameArray.size)
        //悬浮头效果
        listView.addItemDecoration(suspensionDecoration)
    }

}

  布局效果如下:

android按字母索引 android字母索引 联动_字母导航条_02

3、设置字母导航条的回调函数,完成联动功能

  接下来也就是最后一步了,在MainActivity中添加字幕导航条的滑动回调监听,随后根据回调函数返回的内容让Recylerview滑动到指定位置,这样就完成了悬浮头列表与字幕导航条的联动。

android按字母索引 android字母索引 联动_android_03

//字母导航条回调函数
    sideBar.setWordSelectedListener { word, wordIndex ->
        if (wordIndex == 0) {//↑
            listView.smoothScrollToPosition(0)
            return@setWordSelectedListener
        }
        val findIndex = itemData.indexOfFirst { it.isHeader && it.header == word }
        if (findIndex == -1) {//不存在所选内容的条目
            return@setWordSelectedListener
        }
        //移动列表到指定位置
        listView.scrollToPosition(findIndex)
        val layoutManager = listView.layoutManager as LinearLayoutManager
        layoutManager.scrollToPositionWithOffset(findIndex, 0)
    }