• Skip to primary navigation
  • Skip to main content
  • Skip to primary sidebar

陈文管的博客

分享有价值的内容

  • Android
  • Affiliate
  • SEO
  • 前后端
  • 网站建设
  • 自动化
  • 开发资源
  • 关于

Android PDF文件浏览及目录显示交互实现

2025年3月3日发布 | 最近更新于 2025年3月3日

项目上要实现一个PDF手册文件的阅读器,除了加载PDF文件之外,还需要在页面左侧显示PDF文件的目录信息,用户可以点击切换PDF页面,滑动PDF页面的时候左侧目录页需要做同步选中处理,并且在页面右侧显示自定义滚动条,指示当前页面滑动进度。

以下是最终实现效果示例截图:

一、PDF加载开源项目

从Github的仓库来看有三个比较常用的PDF文件加载开源实现

  • https://github.com/DImuthuUpe/AndroidPdfViewer
  • https://github.com/JoanZapata/android-pdfview
  • https://github.com/TomRoush/PdfBox-Android

综合考虑评估使用AndroidPdfViewer,满足目前项目上的要求,使用这个开源项目主要的功能接口包括:列表滑动监听、页面跳转、获取PDF文件的目录信息、配置页面之间的间距、配置PDF页面显示内容撑满控件宽度。

二、AndroidPdfViewer对接实现

在build.gradle文件dependencies模块中添加开源项目的依赖

api 'com.github.barteksc:android-pdf-viewer:3.2.0-beta.1'

主页面布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/background">
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv_catalog"
        android:layout_width="438dp"
        android:layout_height="match_parent"
        android:layout_marginLeft="48dp"
        android:layout_marginTop="54dp" />
    <com.github.barteksc.pdfviewer.PDFView
        android:id="@+id/pdfView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginLeft="50dp"
        android:layout_marginTop="48dp"
        android:layout_marginRight="140dp"
        android:layout_toRightOf="@+id/rv_catalog"
        android:background="@color/transparent" />
    <VerticalScrollbar
        android:id="@+id/verticalScrollbar"
        android:layout_width="4dp"
        android:layout_height="912dp"
        android:layout_alignParentRight="true"
        android:layout_alignParentBottom="true"
        android:layout_marginTop="48dp"
        android:layout_marginRight="16dp"
        android:layout_marginBottom="32dp" />
</RelativeLayout>

rv_catalog用于在左侧显示PDF目录信息,verticalScrollbar用于在右侧显示PDF文件的滚动条指示当前页面滑动进度。

1. 二级目录列表实现

目录列表的BookmarkRecyclerAdapter类实现如下:

import android.content.Context
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.flight.manual.core.common.entity.ManualEntity
import com.shockwave.pdfium.PdfDocument
import com.xiaopeng.lib.utils.LogUtils
import com.xiaopeng.xui.widget.XRelativeLayout
class BookmarkRecyclerAdapter(private val context: Context) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    private val ITEM_TYPE_PARENT = 0
    private val ITEM_TYPE_CHILD = 1
    private val mHandler: Handler = Handler(Looper.getMainLooper())
    private val inflater: LayoutInflater = LayoutInflater.from(context)
    var onBookmarkClickListener: (PdfDocument.Bookmark) -> Unit? = {}
    // 将数据转换为一个包含父目录和子目录的列表
    val itemList = mutableListOf<ManualEntity.BookmarkItem>()
    private var lastClickTimeStamp: Long = System.currentTimeMillis()
    override fun getItemViewType(position: Int): Int {
        return when (itemList[position]) {
            is ManualEntity.BookmarkItem.Parent -> ITEM_TYPE_PARENT
            is ManualEntity.BookmarkItem.Child -> ITEM_TYPE_CHILD
        }
    }
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return when (viewType) {
            ITEM_TYPE_PARENT -> ParentViewHolder(
                inflater.inflate(R.layout.item_pdf_parent_bookmark, parent, false)
            )
            ITEM_TYPE_CHILD -> ChildViewHolder(
                inflater.inflate(R.layout.item_pdf_child_bookmark, parent, false)
            )
            else -> throw IllegalArgumentException("Invalid view type")
        }
    }
    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when (val item = itemList[position]) {
            is ManualEntity.BookmarkItem.Parent -> bindParentView(holder as ParentViewHolder, item, position)
            is ManualEntity.BookmarkItem.Child -> bindChildView(holder as ChildViewHolder, item)
        }
    }
    override fun getItemCount(): Int = itemList.size
    // 绑定父目录视图
    private fun bindParentView(holder: ParentViewHolder, item: ManualEntity.BookmarkItem.Parent, position: Int) {
        val titleTextView: TextView = holder.itemView.findViewById(R.id.catalog_title)
        val arrowImageView: ImageView = holder.itemView.findViewById(R.id.catalog_expand)
        val parentContainer: XRelativeLayout = holder.itemView.findViewById(R.id.parent_container)
        titleTextView.text = item.bookmark.title
        // 设置箭头图标和动画
        arrowImageView.rotation = if (item.isExpand) 90f else 0f
        // 设置箭头点击事件,只控制展开/折叠
        arrowImageView.setOnClickListener {
            if (System.currentTimeMillis() - lastClickTimeStamp < 500) {
                LogUtils.e(MainActivity.TAG, "bindParentView arrowImageView frequent click, return")
                return@setOnClickListener
            }
            lastClickTimeStamp = System.currentTimeMillis()
            if (item.isExpand) {
                collapseGroup(position)
            } else {
                expandGroup(position)
            }
        }
        // 设置父目录点击事件,更新选中状态
        holder.itemView.setOnClickListener {
            // 重置其他父项的选中状态
            clearOtherParentSelections(item)
            clearOtherChildSelections(null)
            // 设置当前父目录为选中状态
            item.isSelected = true
            notifyItemChanged(position)  // 只刷新当前父目录
            onBookmarkClick(item.bookmark)
        }
        // 设置箭头图标显示与否
        arrowImageView.visibility = if (item.bookmark.hasChildren()) View.VISIBLE else View.GONE
        // Set parent background if it's selected
        parentContainer.setBackgroundResource(
            if (item.isSelected) R.mipmap.ic_parent_catelog_selected else 0
        )
    }
    // 绑定子目录视图
    private fun bindChildView(holder: ChildViewHolder, item: ManualEntity.BookmarkItem.Child) {
        val titleTextView: TextView = holder.itemView.findViewById(R.id.catalog_title)
        titleTextView.text = item.bookmark.title
        // 根据 isSelected 来处理加粗状态
        titleTextView.setTextColor(
            if (item.isSelected) context.getColor(R.color.text_color) else context.getColor(R.color.text_color_40)
        )
        holder.itemView.setOnClickListener {
            // 设置当前子目录为选中状态
            item.isSelected = true
            notifyItemChanged(itemList.indexOf(item)) // 仅刷新当前子项
            // 重置其他父项的选中状态
            itemList.forEach {
                if(it is ManualEntity.BookmarkItem.Parent) {
                    if(it.bookmark == item.parentMark) {
                        clearOtherParentSelections(it)
                    }
                }
            }
            // 重置其他子项的选中状态
            clearOtherChildSelections(item)
            // 回调点击事件
            onBookmarkClick(item.bookmark)
        }
    }
    // 处理展开/折叠父目录
    private fun expandGroup(position: Int) {
        if(position < 0 || position >= itemList.size) {
            LogUtils.e(MainActivity.TAG, "expandGroup return:${position}, itemList.size:${itemList.size}")
            return
        }
        if(itemList[position] is ManualEntity.BookmarkItem.Parent) {
            val parent = (itemList[position] as ManualEntity.BookmarkItem.Parent).bookmark
            // 在原始位置插入子项,确保顺序不变
            val childItems = parent.children.map { ManualEntity.BookmarkItem.Child(it, parent) }
            itemList.addAll(position + 1, childItems)
            (itemList[position] as ManualEntity.BookmarkItem.Parent).isExpand = true
            notifyItemRangeInserted(position + 1, parent.children.size) // 最小化刷新,仅插入子项
            // 更新当前父目录的箭头状态
            notifyItemChanged(position)
            mHandler?.postDelayed({notifyDataSetChanged()}, 500)
        }
    }
    private fun collapseGroup(position: Int) {
        if(position < 0 || position >= itemList.size) return
        if(itemList[position] is ManualEntity.BookmarkItem.Parent) {
            val parent = (itemList[position] as ManualEntity.BookmarkItem.Parent).bookmark
            val childrenToRemove = parent.children.size
            for (i in 0 until childrenToRemove) {
                if(position + 1 < itemList.size) {
                    itemList.removeAt(position + 1)
                }
            }
            (itemList[position] as ManualEntity.BookmarkItem.Parent).isExpand = false
            notifyItemRangeRemoved(position + 1, childrenToRemove) // 最小化刷新,仅移除子项
            // 更新当前父目录的箭头状态
            notifyItemChanged(position)
            mHandler?.postDelayed({notifyDataSetChanged()}, 500)
        }
    }
    override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
        mHandler?.removeCallbacksAndMessages(null)
        super.onDetachedFromRecyclerView(recyclerView)
    }
    // 重置其他父项的选中状态
    fun clearOtherParentSelections(selectedItem: ManualEntity.BookmarkItem.Parent?) {
        // 遍历 itemList,找到所有父目录项,并设置未选中
        for (i in itemList.indices) {
            if (itemList[i] is ManualEntity.BookmarkItem.Parent && itemList[i] != selectedItem) {
                var childItem = itemList[i] as ManualEntity.BookmarkItem.Parent
                if(childItem.isSelected) {
                    childItem.isSelected = false
                    notifyItemChanged(i) // 只刷新其他子项
                }
            }
        }
    }
    // 重置其他子项的选中状态
    fun clearOtherChildSelections(selectedItem: ManualEntity.BookmarkItem.Child?) {
        for (i in itemList.indices) {
            if (itemList[i] is ManualEntity.BookmarkItem.Child && itemList[i] != selectedItem) {
                var childItem = itemList[i] as ManualEntity.BookmarkItem.Child
                if(childItem.isSelected) {
                    childItem.isSelected = false
                    notifyItemChanged(i) // 只刷新其他子项
                }
            }
        }
    }
    // 点击事件回调
    private fun onBookmarkClick(bookmark: PdfDocument.Bookmark) {
        // Call the listener callback
        onBookmarkClickListener(bookmark)
    }
    // ViewHolder for Parent
    class ParentViewHolder(view: View) : RecyclerView.ViewHolder(view)
    // ViewHolder for Child
    class ChildViewHolder(view: View) : RecyclerView.ViewHolder(view)
}

item_pdf_parent_bookmark.xml 父目录的布局如下:

<?xml version="1.0" encoding="utf-8"?>
<com.xiaopeng.xui.widget.XLinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:orientation="vertical"
    android:layout_height="wrap_content">

    <com.xiaopeng.xui.widget.XRelativeLayout
        android:id="@+id/parent_container"
        android:layout_width="match_parent"
        android:layout_height="96dp"
        android:orientation="horizontal">

        <com.xiaopeng.xui.widget.XImageView
            android:id="@+id/catalog_expand"
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:layout_centerVertical="true"
            android:layout_marginLeft="28dp"
            android:rotation="0"
            android:src="@mipmap/ic_expand_more" />

        <com.xiaopeng.xui.widget.XTextView
            android:id="@+id/catalog_title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:layout_marginLeft="16dp"
            android:layout_marginRight="16dp"
            android:layout_toRightOf="@+id/catalog_expand"
            android:ellipsize="end"
            android:singleLine="true"
            android:textColor="@color/text_color"
            android:textSize="28dp" />
    </com.xiaopeng.xui.widget.XRelativeLayout>

    <!-- Child RecyclerView to display sub-items -->
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/childRV"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="12dp"
        android:visibility="gone" />
</com.xiaopeng.xui.widget.XLinearLayout>

item_pdf_child_bookmark.xml 子目录的布局如下:

<?xml version="1.0" encoding="utf-8"?> 
<com.xiaopeng.xui.widget.XRelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    xmlns:tools="http://schemas.android.com/tools" 
    android:id="@+id/child_container" 
    android:layout_width="match_parent" 
    android:layout_height="66dp" 
    android:gravity="center_vertical"> 
 
    <com.xiaopeng.xui.widget.XTextView 
        android:id="@+id/catalog_title" 
        android:layout_width="match_parent" 
        android:layout_height="wrap_content" 
        android:layout_gravity="left" 
        android:layout_marginLeft="78dp" 
        android:layout_marginRight="16dp" 
        android:singleLine="true" 
        android:ellipsize="end" 
        android:textColor="@color/text_color_40" 
        android:textSize="28dp" 
        tools:text="3.2测试标题测试标题测试标题测试标题测试标题"/> 
</com.xiaopeng.xui.widget.XRelativeLayout>

ManualEntity 类中的BookmarkItem类的实现如下:

sealed class BookmarkItem {    
    var isSelected: Boolean = false    
    data class Parent(val bookmark: PdfDocument.Bookmark, var isExpand: Boolean = false) : BookmarkItem()    
    data class Child(val bookmark: PdfDocument.Bookmark, val parentMark: PdfDocument.Bookmark) : BookmarkItem()
}

二级目录列表页可以使用Android原生的ExpandableListView,这边使用RecyclerView来实现,这边需要注意的是在做展开/折叠父目录操作的时候,notifyItemRangeInserted、notifyItemRangeRemoved和notifyItemChanged都是为了最小范围更新列表,保证RecyclerView呈现展开和折叠列表的动画。

而mHandler?.postDelayed({notifyDataSetChanged()}, 500) 的调用是为了刷新整个列表,保证在展开/折叠操作列表之后,每一个列表项的position位置都更新到最新。

2. PDF文件的初始化加载

加载PDF文件的初始化代码实现如下,这边使用的方式是在应用内assets目录下集成一个初始的PDF文件,在应用启动的时候先检测SD卡指定目录下是否存在PDF文件,没有就先解压到对应的目录去加载:

private fun initPdf() {
        mPdfView?.fromFile(File(PdfFileManager.FLYING_MANUAL_PDF_PATH))
            ?.enableSwipe(true) // allows to block changing pages using swipe
            ?.swipeHorizontal(false)
            ?.enableDoubletap(true)
            ?.defaultPage(0)
            ?.onLoad({ nbPages ->
                LogUtils.i(TAG, "initPdf onLoad nbPages:$nbPages")
                onPdfLoaded(nbPages)
            }) // called after document is loaded and starts to be rendered
            ?.onPageChange({ page, pageCount ->
                // 保证在点击目录列表切换页面的时候,不和PDF页面滑动冲突
                if (mIsJumpToPage) {
                    mIsJumpToPage = false
                    LogUtils.i(TAG, "mPdfView onPageChange mIsJumpToPage return")
                    return@onPageChange
                }
                onPdfPageChange(page)
            })
            ?.onPageScroll({ page, positionOffset ->
                mScrollBar?.updatePosition(positionOffset)
            })
            ?.onError({ throwable ->
                LogUtils.e(TAG, " mPdfView onError:${throwable.message}")
            })
            ?.onPageError({ page, throwable ->
                LogUtils.e(TAG, " mPdfView onPageError page:$page, error:${throwable.message}")
            })
            ?.enableAnnotationRendering(false) // render annotations (such as comments, colors or forms)
            ?.password(null)
            ?.scrollHandle(null)
            ?.enableAntialiasing(true) // improve rendering a little bit on low-res screens
            ?.spacing(0)
            ?.pageFitPolicy(FitPolicy.WIDTH)
            ?.fitEachPage(true)
            ?.load();
    }

3. 目录列表的初始化

private fun initAdapter() {
    val llm = LinearLayoutManager(this)
    llm.orientation = LinearLayoutManager.VERTICAL
    mRecyclerView?.layoutManager = llm
    mRecyclerView?.addItemDecoration(CatalogItemDecoration(this));
    mBookmarkAdapter = BookmarkRecyclerAdapter(this)
    mRecyclerView?.adapter = mBookmarkAdapter
    mBookmarkAdapter?.onBookmarkClickListener = { bookmark ->
        val pageIndex = bookmark.pageIdx.toInt()
        LogUtils.i(TAG, "mBookmarkAdapter onBookmarkClickListener pageIndex:$pageIndex")
        mIsJumpToPage = true
        mPdfView?.jumpTo(pageIndex)
    }
}

CatalogItemDecoration列表间距配置实现如下:

class CatalogItemDecoration(context: Context) : ItemDecoration() {
    private val parentMargin = dpToPx(40, context) // 父目录的间距 40dp
    private val childMargin = dpToPx(12, context)  // 子目录和父目录之间的间距 12dp
    override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
        val position = parent.getChildAdapterPosition(view)
        val adapter = parent.adapter as BookmarkRecyclerAdapter
        if (position == RecyclerView.NO_POSITION) {
            return
        }
        if(position < 0 || position >= adapter.itemList.size) {
            return
        }
        when (adapter.itemList[position]) {
            is ManualEntity.BookmarkItem.Parent -> {
                // 在父目录下方添加子目录时,子目录之间的间距为12d
                if (position < parent.adapter?.itemCount?.minus(1) ?: 0) {
                    val nextItem = (parent.adapter as BookmarkRecyclerAdapter).itemList[position + 1]
                    if (nextItem is ManualEntity.BookmarkItem.Child) {
                        outRect.bottom = 0
                    } else {
                        outRect.bottom = parentMargin
                    }
                } else {
                    outRect.bottom = parentMargin
                }
            }
            is ManualEntity.BookmarkItem.Child -> {
                // 子目录和父目录之间的间距 12dp
                if (position > 0) {
                    val preItem = (parent.adapter as BookmarkRecyclerAdapter).itemList[position - 1]
                    // 如果下一个是子目录,保持 12dp 间距
                    if (preItem is ManualEntity.BookmarkItem.Parent) {
                        outRect.top = childMargin
                    } else {
                        outRect.top = 0
                    }
                }
                if (position < parent.adapter?.itemCount?.minus(1) ?: 0) {
                    val nextItem = (parent.adapter as BookmarkRecyclerAdapter).itemList[position + 1]
                    if (nextItem is ManualEntity.BookmarkItem.Parent) {
                        outRect.bottom = parentMargin
                    } else {
                        outRect.bottom = 0
                    }
                }
                // 如果是最后一个子目录,底部间距为 40dp
                if (position == parent.adapter?.itemCount?.minus(1)) {
                    outRect.bottom = parentMargin
                }
            }
        }
    }
    // 将 dp 转换为 px
    private fun dpToPx(dp: Int, context: Context): Int {
        return (dp * context.resources.displayMetrics.density).toInt()
    }
}

4. 目录信息的加载

这边只保留二级目录,不展示超过二级以上的目录信息。

private fun onPdfLoaded(totalPages: Int) {
    var catalog: MutableList<Bookmark>? = mPdfView?.tableOfContents
    if (!catalog.isNullOrEmpty()) {
        LogUtils.i(TAG, "tableOfContents size:" + catalog.size)
        catalog.forEach { childBookmark ->
            // 移除二级子目录的子目录,即三级及以上的目录
            childBookmark.children?.forEach { child ->
                child.children?.clear()
            }
        }
        for (bookmark in catalog) {
            mBookmarkAdapter?.itemList?.add(ManualEntity.BookmarkItem.Parent(bookmark))
        }
        mBookmarkAdapter?.notifyDataSetChanged()
    } else {
        LogUtils.i(TAG, "initPdf tableOfContents isNullOrEmpty")
    }
    mScrollBar?.updatePdfPage(totalPages)
}

5. PDF页面滑动目录跟随处理

在PDF页面滑动的时候,左侧目录要做跟随选中处理,类似飞书文档的目录操作。

private fun onPdfPageChange(page: Int) {
        LogUtils.i(TAG, "onPdfPageChange page:$page")
        // 遍历列表,找到匹配的 Bookmark
        val matchedItem = mBookmarkAdapter?.itemList?.firstOrNull {
            (it is ManualEntity.BookmarkItem.Parent && it.bookmark.pageIdx == page.toLong()) ||
                    (it is ManualEntity.BookmarkItem.Child && it.bookmark.pageIdx == page.toLong())
        }
        matchedItem?.let { item ->
            // 标记当前匹配的 Bookmark 为选中状态
            if (item is ManualEntity.BookmarkItem.Parent) {
                // 如果匹配的是父目录,首先检查已选中的子目录
                mBookmarkAdapter?.itemList?.forEach { childItem ->
                    if (childItem is ManualEntity.BookmarkItem.Child && childItem.isSelected) {
                        // 如果子目录已选中,但它的父目录不是当前匹配的父目录,则取消选中
                        if (childItem.parentMark != item.bookmark) {
                            childItem.isSelected = false
                            val childPosition = mBookmarkAdapter?.itemList?.indexOf(childItem) ?: return@forEach
                            mBookmarkAdapter?.notifyItemChanged(childPosition)
                        }
                    }
                }
                item.isSelected = true
                mBookmarkAdapter?.clearOtherParentSelections(item)
            } else if (item is ManualEntity.BookmarkItem.Child) {
                // 如果匹配的是子目录,首先检查已选中的父目录
                mBookmarkAdapter?.itemList?.forEach { parentItem ->
                    if (parentItem is ManualEntity.BookmarkItem.Parent && parentItem.isSelected) {
                        // 已选中子目录的父目录不是当前选中的父目录,则取消选中
                        if (parentItem.bookmark != item.parentMark) {
                            parentItem.isSelected = false
                            val parentPosition = mBookmarkAdapter?.itemList?.indexOf(parentItem) ?: return@forEach
                            mBookmarkAdapter?.notifyItemChanged(parentPosition)
                        }
                    }
                }
                item.isSelected = true
                mBookmarkAdapter?.clearOtherChildSelections(item)
            }
            // 通知刷新
            val position = mBookmarkAdapter?.itemList?.indexOf(item) ?: return
            mBookmarkAdapter?.notifyItemChanged(position)
            // 判断是否需要滚动到该项
            val layoutManager = mRecyclerView?.layoutManager as LinearLayoutManager
            val firstVisiblePosition = layoutManager.findFirstVisibleItemPosition()
            val lastVisiblePosition = layoutManager.findLastVisibleItemPosition()
            if (position < firstVisiblePosition || position > lastVisiblePosition) {
                mRecyclerView?.scrollToPosition(position)
            }
        } ?: run {
            // 如果没有找到匹配项,可以记录日志,或者执行其他逻辑
            LogUtils.i(TAG, "No matching bookmark found for page: $page")
        }
    }

6. 自定义PDF页面滚动条控件

滚动条的宽度固定,高度根据PDF页面的总高度来计算一个页面的百分比,而 PDFView 控件 onPageScroll 回调方法的 positionOffset 刚好是当前页面滑动位置相对总页面的百分比。

class VerticalScrollbar(context: Context, attrs: AttributeSet?) : View(context, attrs) {
    companion object {
        private const val TAG = "VerticalScrollbar"
    }
    private var scrollHeight = 0f
    private var scrollPosition = 0f
    private val handler = Handler(Looper.getMainLooper()) // To handle delayed actions
    private val paint = Paint()
    private var mTotalPages = 0
    // Update the scrollbar size
    fun updatePdfPage(totalPages: Int) {
        if(totalPages <= 0) {
            visibility = GONE // Hide after 2 seconds
            LogUtils.i(TAG, "updatePdfPage totalPages <= 0 gone")
            return
        }
        visibility = VISIBLE // Hide after 2 seconds
        this.mTotalPages = totalPages
        this.scrollHeight = measuredHeight * (1f / totalPages)
        paint.color = context.resources.getColor(R.color.color_scrollbar)
        invalidate()
    }
    // Update the scrollbar position
    fun updatePosition(position: Float) {
        if(mTotalPages <= 0) {
            return
        }
        this.scrollPosition = position * measuredHeight
        // Ensure the scrollbar doesn't get cut off at the bottom
        if (scrollPosition + scrollHeight > measuredHeight) {
            scrollPosition = height - scrollHeight // Position it at the bottom if it exceeds
        }
        // Show scrollbar and delay hiding for 2 seconds
        visibility = VISIBLE
        invalidate()
        handler.removeCallbacksAndMessages(null)  // Clear any pending hide actions
        handler.postDelayed({
            visibility = GONE // Hide after 2 seconds
        }, 2000)
    }
    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        canvas?.let {
            // Draw the scrollbar as a filled rectangle
            it.drawRect(0f, scrollPosition, measuredWidth.toFloat(), scrollPosition + scrollHeight, paint)
        }
    }
    override fun onDetachedFromWindow() {
        handler.removeCallbacksAndMessages(null)
        super.onDetachedFromWindow()
    }
}

这边需要注意的是,要获取目录信息,必须原始的PDF文件就具备目录信息,如果原始的PDF文件不含有目录信息则 mPdfView?.tableOfContents 获取的信息就是空的。

扩展阅读:

  • Android 航线剖面图自定义控件绘制实现
  • Android 自定义棱形样式进度条控件
  • Android 弧形 RecyclerView 实现(Kotlin)
  • 美图手机音乐Widget动画实现
  • Android 心率动画自定义控件实现
  • Android 卡片旋转切换动效实现详解
  • Android 残影数字动画实现详解
  • Android 自定义菱形横向滑动指示器控件
  • Android 航线缩略图简易绘制实现
博客公众号

微信公众号

转载请注明出处:陈文管的博客 – Android PDF文件浏览及目录显示交互实现

发表回复 取消回复

您的电子邮箱地址不会被公开。 必填项已用*标注

10 + 19 =

文章目录

  • 一、PDF加载开源项目
  • 二、AndroidPdfViewer对接实现
    • 1. 二级目录列表实现
    • 2. PDF文件的初始化加载
    • 3. 目录列表的初始化
    • CatalogItemDecoration列表间距配置实现如下:
    • 4. 目录信息的加载
    • 5. PDF页面滑动目录跟随处理
    • 6. 自定义PDF页面滚动条控件
博客公众号

闽ICP备18001825号-1 · Copyright © 2025 · Powered by chenwenguan.com