项目上要实现一个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.view.animation.PathInterpolator
import android.widget.ImageView
import android.widget.RelativeLayout
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.entity.ManualEntity
import com.shockwave.pdfium.PdfDocument
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: RelativeLayout = holder.itemView.findViewById(R.id.parent_container)
titleTextView.text = item.bookmark.title
titleTextView.isSelected = item.isSelected
arrowImageView.isSelected = item.isSelected
// 设置箭头图标和动画
arrowImageView.rotation = if (item.isExpand) 90f else 0f
// 设置箭头图标显示与否
arrowImageView.visibility = if (item.bookmark.hasChildren()) View.VISIBLE else View.GONE
// 设置箭头点击事件,只控制展开/折叠
titleTextView.setOnClickListener {
onExpandCollapseClick(item, position, arrowImageView)
}
arrowImageView.setOnClickListener {
onExpandCollapseClick(item, position, arrowImageView)
}
// 设置父目录点击事件,更新选中状态
holder.itemView.setOnClickListener {
onSelectItem(item, position)
}
// Set parent background if it's selected
parentContainer.setBackgroundColor(
if (item.isSelected) context.getColor(R.color.color_catelog_selected) else context.getColor(
R.color.color_catelog_unselected
)
)
}
private fun onSelectItem(item: ManualEntity.BookmarkItem.Parent, position: Int) {
// 重置其他父项的选中状态
clearOtherParentSelections(item)
clearOtherChildSelections(null)
// 设置当前父目录为选中状态
item.isSelected = true
notifyItemChanged(position)
onBookmarkClick(item.bookmark)
}
private fun onExpandCollapseClick(item: ManualEntity.BookmarkItem.Parent, position: Int, arrowImageView: ImageView) {
if (System.currentTimeMillis() - lastClickTimeStamp < 500) {
return
}
lastClickTimeStamp = System.currentTimeMillis()
// 执行旋转动画
if (item.isExpand) {
// 折叠时旋转回0度
arrowImageView.animate()
.rotation(0f)
.setDuration(150)
.setInterpolator(PathInterpolator(0.4f, 0f, 0.2f, 1f))
.start()
collapseGroup(position)
} else {
// 展开时旋转到90度
arrowImageView.animate()
.rotation(90f)
.setDuration(150)
.setInterpolator(PathInterpolator(0.4f, 0f, 0.2f, 1f))
.start()
expandGroup(position)
}
onSelectItem(item, position)
mHandler?.postDelayed({ notifyDataSetChanged() }, 500)
}
// 绑定子目录视图
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) {
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) }
val newList = itemList.toMutableList()
newList.addAll(position + 1, childItems)
(newList[position] as ManualEntity.BookmarkItem.Parent).isExpand = true
updateList(newList)
}
}
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 newList = itemList.toMutableList()
val childrenToRemove = parent.children.size
for (i in 0 until childrenToRemove) {
if (position + 1 < newList.size) {
newList.removeAt(position + 1)
}
}
(newList[position] as ManualEntity.BookmarkItem.Parent).isExpand = false
updateList(newList)
}
}
class BookmarkDiffUtil(
private val oldList: List<ManualEntity.BookmarkItem>,
private val newList: List<ManualEntity.BookmarkItem>
) : DiffUtil.Callback() {
override fun getOldListSize(): Int = oldList.size
override fun getNewListSize(): Int = newList.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldList[oldItemPosition] == newList[newItemPosition]
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldList[oldItemPosition] == newList[newItemPosition]
}
}
// 在 Adapter 中使用 DiffUtil
private fun updateList(newList: List<ManualEntity.BookmarkItem>) {
val diffResult = DiffUtil.calculateDiff(BookmarkDiffUtil(itemList, newList))
itemList.clear()
itemList.addAll(newList)
diffResult.dispatchUpdatesTo(this)
}
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"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RelativeLayout
android:id="@+id/parent_container"
android:layout_width="match_parent"
android:layout_height="96dp"
android:orientation="horizontal">
<ImageView
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="@drawable/selector_catalog_arrow" />
<TextView
android:id="@+id/catalog_title"
style="@style/XText.Body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginLeft="12dp"
android:layout_marginRight="12dp"
android:layout_toRightOf="@+id/catalog_expand"
android:ellipsize="end"
android:paddingLeft="0dp"
android:gravity="left"
android:paddingRight="0dp"
android:singleLine="true"
android:textColor="@drawable/selector_catalog_text_color"
android:textSize="28dp" />
</RelativeLayout>
<!-- 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" />
</LinearLayout>
item_pdf_child_bookmark.xml 子目录的布局如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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="96dp"
android:gravity="center_vertical">
<TextView
android:id="@+id/catalog_title"
style="@style/XText.Body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:layout_marginLeft="78dp"
android:gravity="left"
android:layout_marginRight="12dp"
android:ellipsize="end"
android:paddingLeft="0dp"
android:paddingRight="0dp"
android:singleLine="true"
android:textColor="@color/text_color_40"
android:textSize="28dp"
tools:text="3.2测试标题测试标题测试标题测试标题测试标题" />
</RelativeLayout>
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文件浏览及目录显示交互实现