添加pdf拆分二级功能界面。

This commit is contained in:
ocean 2025-09-23 18:33:25 +08:00
parent 06a2720547
commit df84891d91
25 changed files with 969 additions and 85 deletions

View File

@ -1,10 +1,14 @@
package com.all.pdfreader.pro.app.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
/**
* 拆分pdf展示用的每页信息
*/
@Parcelize
data class PdfPageItem(
val pageIndex: Int,
var previewFilePath: String?, // 缓存文件路径
var isSelected: Boolean
)
) : Parcelable

View File

@ -0,0 +1,31 @@
package com.all.pdfreader.pro.app.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
/**
* 拆分pdf被选中使用的结构
*/
@Parcelize
data class PdfSelectedPagesItem(
val filePath: String,
var fileName: String, // 文件名
val pages: List<PdfPageItem> // 所有页的数据
): Parcelable {
/**
* 获取已选中的页面列表
*/
fun selectedPages(): List<PdfPageItem> =
pages.filter { it.isSelected }.sortedBy { it.pageIndex }
/**
* 获取已选中的页码字符串例如 "1,2,3,4,6"
*/
fun selectedPageNumbersString(): String =
selectedPages().joinToString(",") { (it.pageIndex + 1).toString() }
/**
* 获取第一个图片没有返回空字符串
*/
fun getFirstThumbPath(): String = pages.first().previewFilePath ?: ""
}

View File

@ -0,0 +1,7 @@
package com.all.pdfreader.pro.app.model
data class PdfSplitResultItem(
val filePath: String,
val thumbnailPath: String? = null,
var isSelected: Boolean
)

View File

@ -2,5 +2,6 @@ package com.all.pdfreader.pro.app.model
enum class RenameType {
FILE, // 修改文件名
BOOKMARK // 修改书签名
BOOKMARK, // 修改书签名
NAME // 修改集合的item名称
}

View File

@ -4,14 +4,22 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import com.all.pdfreader.pro.app.R
import com.all.pdfreader.pro.app.databinding.ActivityPdfSplitBinding
import com.all.pdfreader.pro.app.model.PdfPageItem
import com.all.pdfreader.pro.app.model.PdfSelectedPagesItem
import com.all.pdfreader.pro.app.model.RenameType
import com.all.pdfreader.pro.app.ui.adapter.SplitPdfAdapter
import com.all.pdfreader.pro.app.ui.adapter.SplitSelectedPdfAdapter
import com.all.pdfreader.pro.app.ui.dialog.PromptDialogFragment
import com.all.pdfreader.pro.app.ui.dialog.RenameDialogFragment
import com.all.pdfreader.pro.app.util.FileUtils.toUnderscoreDateTime
import com.all.pdfreader.pro.app.util.PdfUtils
import com.gyf.immersionbar.ImmersionBar
import kotlinx.coroutines.launch
@ -33,14 +41,19 @@ class SplitPdfActivity : BaseActivity() {
private lateinit var binding: ActivityPdfSplitBinding
private lateinit var adapter: SplitPdfAdapter
private var splitList: MutableList<PdfPageItem> = mutableListOf()
private var selectedList: MutableList<PdfSelectedPagesItem> = mutableListOf()
private lateinit var selectedPdfAdapter: SplitSelectedPdfAdapter
private var isSelectedViewShow = false//用于返回的时候是否提示(点击了继续按钮就会产生提示)
private lateinit var filePath: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityPdfSplitBinding.inflate(layoutInflater)
setContentView(binding.root)
setupBackPressedCallback()
ImmersionBar.with(this).statusBarView(binding.view).statusBarDarkFont(true)
.navigationBarColor(R.color.white).init()
val filePath = intent.getStringExtra(EXTRA_PDF_PATH)
.navigationBarColor(R.color.bg_color).init()
filePath = intent.getStringExtra(EXTRA_PDF_PATH)
?: throw IllegalArgumentException("PDF file hash is required")
if (filePath.isEmpty()) {
showToast(getString(R.string.file_not))
@ -52,40 +65,77 @@ class SplitPdfActivity : BaseActivity() {
}
private fun initView() {
binding.continueNowBtn.isEnabled = false
binding.title.text = getString(R.string.selected_page, 0)
binding.selectAllBtn.visibility = View.GONE
binding.loadingRoot.root.visibility = View.VISIBLE
binding.selectAllBtn.visibility = View.GONE//初始隐藏全选按钮
binding.addBtn.visibility = View.GONE//初始隐藏add按钮
binding.loadingRoot.root.visibility = View.VISIBLE//初始显示loading
binding.splitRv.layoutManager = GridLayoutManager(this, 2)
adapter = SplitPdfAdapter(splitList) { item, pos ->
item.isSelected = !item.isSelected
adapter.setItemSelected(pos, item.isSelected)
binding.title.text = getString(R.string.selected_page, adapter.getSelPages())
// 只有存在选中的item则为true
val anySelected = splitList.any { it.isSelected }
binding.continueNowBtn.isEnabled = anySelected
updateContinueNowBtnState(anySelected)
}
binding.splitRv.adapter = adapter
selectedPdfAdapter = SplitSelectedPdfAdapter(selectedList, onEditClick = { item, pos ->
RenameDialogFragment(
RenameType.NAME, null, item.fileName, onOkClickString = { string ->
selectedList[pos].fileName = string
selectedPdfAdapter.updateItem(pos)
}).show(supportFragmentManager, "BookmarksDialogFragment")
}, onDeleteClick = { item, pos ->
selectedList.remove(item)
selectedPdfAdapter.removeItem(pos)
updateSplitBtnState(selectedList.isNotEmpty())
})
binding.splitSelectRv.layoutManager = LinearLayoutManager(this)
binding.splitSelectRv.adapter = selectedPdfAdapter
}
private fun setupClick() {
binding.backBtn.setOnClickListener { finish() }
binding.backBtn.setOnClickListener { onBackPressedDispatcher.onBackPressed() }
binding.selectAllBtn.setOnClickListener {
val selectAll = splitList.any { !it.isSelected }
val selectAll = splitList.any { !it.isSelected }//如果列表里有一页没选中 → 返回 true
adapter.setAllSelected(selectAll)
binding.title.text = getString(R.string.selected_page, adapter.getSelPages())
binding.continueNowBtn.isEnabled = selectAll
updateSelectAllState(selectAll)
updateContinueNowBtnState(selectAll)
}
binding.continueNowBtn.setOnClickListener {
val selectedPages = splitList.filter { it.isSelected }.map { it.copy() }
val name =
getString(R.string.split) + "_" + System.currentTimeMillis().toUnderscoreDateTime()
val item = PdfSelectedPagesItem(filePath, name, selectedPages)
selectedList.add(item)
selectedPdfAdapter.updateAdapter()
isSelectedViewShow = true
updateViewState(true)
}
binding.addBtn.setOnClickListener {
//点击重新添加后选中状态全部置为false全选按钮置为false
adapter.setAllSelected(false)
updateSelectAllState(false)
updateViewState(false)
}
binding.splitBtn.setOnClickListener {
if (selectAll) {
binding.selectAll.setBackgroundResource(R.drawable.dr_circular_sel_on_bg)
} else {
binding.selectAll.setBackgroundResource(R.drawable.dr_circular_sel_off_bg)
}
}
}
private fun initSplitData(file: File) {
lifecycleScope.launch {
splitList.clear()
PdfUtils.clearPdfThumbsCache(this@SplitPdfActivity) // 先清理旧缓存
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
var firstPageLoaded = false
PdfUtils.splitPdfToPageItemsFlow(this@SplitPdfActivity, file).collect { pageItem ->
logDebug("pageItem flow ->$pageItem")
if (splitList.size <= pageItem.pageIndex) {
splitList.add(pageItem)
adapter.notifyItemInserted(splitList.size - 1)
@ -93,20 +143,106 @@ class SplitPdfActivity : BaseActivity() {
splitList[pageItem.pageIndex] = pageItem
adapter.updateItem(pageItem.pageIndex)
}
if (!firstPageLoaded) {
if (!firstPageLoaded) {//有数据回来则隐藏loading显示全选按钮
binding.loadingRoot.root.visibility = View.GONE
binding.selectAllBtn.visibility = View.VISIBLE
firstPageLoaded = true
}
}
binding.loadingRoot.root.visibility = View.GONE
}
}
}
private fun updateSelectAllState(b: Boolean) {
binding.selectAll.setBackgroundResource(
if (b) R.drawable.dr_circular_sel_on_bg
else R.drawable.dr_circular_sel_off_bg
)
}
private fun updateContinueNowBtnState(b: Boolean) {
binding.continueNowBtn.setBackgroundResource(
if (b) R.drawable.dr_click_btn_bg
else R.drawable.dr_btn_not_clickable_bg
)
}
private fun updateSplitBtnState(b: Boolean) {
binding.splitBtn.setBackgroundResource(
if (b) R.drawable.dr_click_btn_bg
else R.drawable.dr_btn_not_clickable_bg
)
}
private fun updateViewState(b: Boolean) {
if (b) {
binding.selectAllBtn.visibility = View.GONE//隐藏全选按钮
binding.addBtn.visibility = View.VISIBLE//显示add按钮
binding.title.text = getString(R.string.split_pdf)//设置标题
} else {
binding.selectAllBtn.visibility = View.VISIBLE
binding.addBtn.visibility = View.GONE
binding.title.text =
getString(R.string.selected_page, adapter.getSelPages())//重新设置标题为选中了多少个
}
updateViewStateWithAnimation(b)
updateSplitBtnState(selectedList.isNotEmpty())
}
private fun updateViewStateWithAnimation(showSelectedView: Boolean) {
if (showSelectedView) {
// 切换到已选页面列表
binding.splitListLayout.animate().translationY(-50f).alpha(0f).setDuration(200)
.withEndAction {
binding.splitListLayout.visibility = View.GONE
binding.splitListLayout.translationY = 0f
}.start()
binding.splitSelectLayout.alpha = 0f
binding.splitSelectLayout.translationY = 50f
binding.splitSelectLayout.visibility = View.VISIBLE
binding.splitSelectLayout.animate().translationY(0f).alpha(1f).setDuration(200).start()
} else {
// 切回拆分页列表
binding.splitSelectLayout.animate().translationY(50f).alpha(0f).setDuration(200)
.withEndAction {
binding.splitSelectLayout.visibility = View.GONE
binding.splitSelectLayout.translationY = 0f
}.start()
binding.splitListLayout.alpha = 0f
binding.splitListLayout.translationY = -50f
binding.splitListLayout.visibility = View.VISIBLE
binding.splitListLayout.animate().translationY(0f).alpha(1f).setDuration(200).start()
}
}
override fun onDestroy() {
super.onDestroy()
splitList.clear()
PdfUtils.clearPdfThumbsCache(this)
}
private fun setupBackPressedCallback() {
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (isSelectedViewShow) {
//使用提示对话框
PromptDialogFragment(
getString(R.string.exit_split),
getString(R.string.confirm_discard_changes),
getString(R.string.discard),
onOkClick = {
isEnabled = false
onBackPressedDispatcher.onBackPressed()
}).show(supportFragmentManager, getString(R.string.exit_split))
} else {
isEnabled = false
onBackPressedDispatcher.onBackPressed()
}
}
})
}
}

View File

@ -0,0 +1,85 @@
package com.all.pdfreader.pro.app.ui.act
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager
import com.all.pdfreader.pro.app.R
import com.all.pdfreader.pro.app.databinding.ActivityPdfSplitResultBinding
import com.all.pdfreader.pro.app.model.PdfPageItem
import com.all.pdfreader.pro.app.model.PdfSelectedPagesItem
import com.all.pdfreader.pro.app.model.PdfSplitResultItem
import com.all.pdfreader.pro.app.ui.act.SplitPdfActivity
import com.all.pdfreader.pro.app.ui.adapter.SplitPdfResultAdapter
import com.all.pdfreader.pro.app.util.PdfUtils
import com.gyf.immersionbar.ImmersionBar
import kotlinx.coroutines.launch
import java.io.File
class SplitPdfResultActivity : BaseActivity() {
override val TAG: String = "SplitPdfResultActivity"
companion object {
private const val EXTRA_SELECTED_LIST = "extra_selected_list"
fun createIntent(
context: Context,
selectedPageIndices: List<Int>
): Intent {
return Intent(context, SplitPdfResultActivity::class.java).apply {
}
}
}
private lateinit var binding: ActivityPdfSplitResultBinding
private lateinit var adapter: SplitPdfResultAdapter
private var splitResultList: MutableList<PdfSplitResultItem> = mutableListOf()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityPdfSplitResultBinding.inflate(layoutInflater)
setContentView(binding.root)
ImmersionBar.with(this).statusBarView(binding.view).statusBarDarkFont(true)
.navigationBarColor(R.color.bg_color).init()
initView()
setupClick()
}
private fun initView() {
adapter = SplitPdfResultAdapter(splitResultList)
binding.recyclerView.layoutManager = LinearLayoutManager(this)
binding.recyclerView.adapter = adapter
}
private fun startSplittingPDF(
inputFile: File, selectedPages: List<PdfPageItem>, outputDir: File, outputFileName: String
) {
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
val resultFile = PdfUtils.exportSelectedPages(
inputFile = inputFile,
selectedPages = selectedPages,
outputDir = outputDir,
outputFileName = outputFileName,
onProgress = { current, total ->
})
}
}
}
private fun setupClick() {
binding.backBtn.setOnClickListener {
finish()
}
}
override fun onDestroy() {
super.onDestroy()
}
}

View File

@ -51,10 +51,12 @@ class SplitPdfAdapter(
holder.binding.apply {
//更新文字图片与选中状态
pageNumberTv.text = "${item.pageIndex + 1}"
Glide.with(holder.binding.root)
.load(File(item.previewFilePath ?: ""))
.transform(CenterCrop(), RoundedCorners(8.dpToPx(holder.binding.root.context)))
.into(image)
if (item.previewFilePath != null) {
Glide.with(holder.binding.root)
.load(File(item.previewFilePath ?: ""))
.transform(CenterCrop(), RoundedCorners(8.dpToPx(holder.binding.root.context)))
.into(image)
}
bindSelection(item, holder)
}
holder.binding.root.setOnClickListener {
@ -74,7 +76,7 @@ class SplitPdfAdapter(
private fun bindSelection(item: PdfPageItem, holder: PdfViewHolder) {
val b = holder.binding
val selRes =
if (item.isSelected) R.drawable.dr_circular_sel_on_bg else R.drawable.dr_circular_sel_off_bg
if (item.isSelected) R.drawable.dr_item_page_img_sel_on_bg else R.drawable.dr_item_page_img_sel_off_bg
b.selIv.setBackgroundResource(selRes)
val bgRes = if (item.isSelected) R.drawable.dr_sel_on_frame else R.drawable.dr_sel_off_frame

View File

@ -0,0 +1,58 @@
package com.all.pdfreader.pro.app.ui.adapter
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.all.pdfreader.pro.app.R
import com.all.pdfreader.pro.app.databinding.AdapterSplitSelectedResultItemBinding
import com.all.pdfreader.pro.app.model.PdfSplitResultItem
import com.all.pdfreader.pro.app.util.AppUtils.dpToPx
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.CenterCrop
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import java.io.File
class SplitPdfResultAdapter(
private val list: MutableList<PdfSplitResultItem>
) : RecyclerView.Adapter<SplitPdfResultAdapter.PdfViewHolder>() {
inner class PdfViewHolder(val binding: AdapterSplitSelectedResultItemBinding) :
RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = PdfViewHolder(
AdapterSplitSelectedResultItemBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
)
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: PdfViewHolder, position: Int) {
val item = list[position]
if (item.thumbnailPath != null) {
Glide.with(holder.binding.root).load(File(item.thumbnailPath))
.transform(CenterCrop(), RoundedCorners(8.dpToPx(holder.binding.root.context)))
.into(holder.binding.image)
}
holder.binding.nameTv.text = File(item.filePath).name
holder.binding.pathTv.text =
holder.binding.root.context.getString(R.string.path_details, item.filePath)
if (item.isSelected) {
holder.binding.selectIv.visibility = View.VISIBLE
} else {
holder.binding.selectIv.visibility = View.INVISIBLE
}
}
override fun getItemCount(): Int = list.size
@SuppressLint("NotifyDataSetChanged")
fun updateAdapter() {
notifyDataSetChanged()
}
fun updateItem(position: Int) = notifyItemChanged(position)
fun removeItem(position: Int) = notifyItemRemoved(position)
}

View File

@ -0,0 +1,61 @@
package com.all.pdfreader.pro.app.ui.adapter
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.all.pdfreader.pro.app.databinding.AdapterSplitSelectedPageItemBinding
import com.all.pdfreader.pro.app.model.PdfSelectedPagesItem
import com.all.pdfreader.pro.app.util.AppUtils.dpToPx
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.CenterCrop
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import java.io.File
class SplitSelectedPdfAdapter(
private val list: MutableList<PdfSelectedPagesItem>,
private val onEditClick: (PdfSelectedPagesItem, Int) -> Unit,
private val onDeleteClick: (PdfSelectedPagesItem, Int) -> Unit
) : RecyclerView.Adapter<SplitSelectedPdfAdapter.PdfViewHolder>() {
inner class PdfViewHolder(val binding: AdapterSplitSelectedPageItemBinding) :
RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = PdfViewHolder(
AdapterSplitSelectedPageItemBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
)
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: PdfViewHolder, position: Int) {
val item = list[position]
holder.binding.tvPages.text = item.selectedPageNumbersString()
val thumbPath = item.getFirstThumbPath()
if (thumbPath.isNotEmpty()) {
Glide.with(holder.binding.root).load(File(thumbPath))
.transform(CenterCrop(), RoundedCorners(8.dpToPx(holder.binding.root.context)))
.into(holder.binding.tvFileImg)
}
holder.binding.tvFileName.text = item.fileName
holder.binding.editTitleBtn.setOnClickListener {
val pos = holder.bindingAdapterPosition
onEditClick(item, pos)
}
holder.binding.deleteBtn.setOnClickListener {
val pos = holder.bindingAdapterPosition
onDeleteClick(item, pos)
}
}
override fun getItemCount(): Int = list.size
@SuppressLint("NotifyDataSetChanged")
fun updateAdapter() {
notifyDataSetChanged()
}
fun updateItem(position: Int) = notifyItemChanged(position)
fun removeItem(position: Int) = notifyItemRemoved(position)
}

View File

@ -1,7 +1,6 @@
package com.all.pdfreader.pro.app.ui.dialog
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -125,10 +124,10 @@ class BookmarksDialogFragment(
dialogAddBookMarks()
}
binding.deleteALLPageBtn.setOnClickListener {
DeleteDialogFragment(
PromptDialogFragment(
getString(R.string.delete_bookmarks_title),
getString(R.string.delete_bookmarks_desc),
onDeleteClick = {
onOkClick = {
viewModel.deleteAllBookmark(pdfDocument.filePath)
adapter.removeAllItems()
bookmarks = emptyList()

View File

@ -1,7 +1,6 @@
package com.all.pdfreader.pro.app.ui.dialog
import android.net.Uri
import android.nfc.Tag
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@ -115,10 +114,10 @@ class ListMoreDialogFragment(val filePath: String) : BottomSheetDialogFragment()
dismiss()
}
binding.deleteFileBtn.setOnClickListener {
DeleteDialogFragment(
PromptDialogFragment(
getString(R.string.delete_file_title),
getString(R.string.delete_file_desc),
onDeleteClick = {
onOkClick = {
viewModel.deleteFile(pdfDocument.filePath)
}).show(parentFragmentManager, "deleteFile")
dismiss()
@ -179,11 +178,11 @@ class ListMoreDialogFragment(val filePath: String) : BottomSheetDialogFragment()
dismiss()
}
binding.removeRecentBtn.setOnClickListener {
DeleteDialogFragment(
PromptDialogFragment(
getString(R.string.remove_dialog_title),
getString(R.string.remove_dialog_desc),
getString(R.string.remove),
onDeleteClick = {
onOkClick = {
viewModel.removeRecent(pdfDocument.filePath)
}).show(parentFragmentManager, "removeRecent")
dismiss()

View File

@ -11,11 +11,12 @@ import androidx.fragment.app.DialogFragment
import com.all.pdfreader.pro.app.R
import com.all.pdfreader.pro.app.databinding.DialogDeleteBinding
class DeleteDialogFragment(
class PromptDialogFragment(
private val title: String,
private val desc: String,
private val okBtnText: String? = null,
private val onDeleteClick: () -> Unit
private val onOkClick: () -> Unit,
private val onCancelClick: () -> Unit = {}
) : DialogFragment() {
private lateinit var binding: DialogDeleteBinding
@ -54,10 +55,11 @@ class DeleteDialogFragment(
private fun setupOnClick() {
binding.okBtn.setOnClickListener {
onDeleteClick()
onOkClick()
dismiss()
}
binding.cancelBtn.setOnClickListener {
onCancelClick()
dismiss()
}
}

View File

@ -23,7 +23,9 @@ import java.io.File
class RenameDialogFragment(
private val type: RenameType,
private val bookmark: BookmarkEntity? = null,
private val onOkClick: (BookmarkEntity) -> Unit= {}
private val name: String? = null,
private val onOkClick: (BookmarkEntity) -> Unit = {},
private val onOkClickString: (String) -> Unit = {}
) : DialogFragment() {
private lateinit var binding: DialogRenameFileBinding
@ -71,6 +73,15 @@ class RenameDialogFragment(
dismiss()
}
}
RenameType.NAME -> {
name?.let {
initView(it)
setupOnClick(it, it, null)
}?:run {
dismiss()
}
}
}
}
@ -104,6 +115,14 @@ class RenameDialogFragment(
dismiss()
}
}
RenameType.NAME -> {
val text = binding.etName.text.toString()
if (validateEnter(text, oldName, filePath)) {
onOkClickString(text)
dismiss()
}
}
}
}
@ -161,6 +180,17 @@ class RenameDialogFragment(
return false
}
}
RenameType.NAME -> {
if (name.length > 50) {
binding.tilName.error = getString(R.string.name_too_long)
return false
}
if (name == oldName) {
binding.tilName.error = getString(R.string.name_not_changed)
return false
}
}
}
// 禁止开头/结尾空格

View File

@ -5,38 +5,28 @@ import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.pdf.PdfRenderer
import android.net.Uri
import android.os.Bundle
import android.os.CancellationSignal
import android.os.ParcelFileDescriptor
import android.print.PageRange
import android.print.PrintAttributes
import android.print.PrintDocumentAdapter
import android.print.PrintDocumentInfo
import android.print.PrintManager
import android.print.pdf.PrintedPdfDocument
import android.view.View
import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
import android.webkit.MimeTypeMap
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.EditText
import android.widget.Toast
import androidx.core.content.FileProvider
import com.all.pdfreader.pro.app.R
import java.io.File
import java.io.FileOutputStream
import androidx.core.graphics.createBitmap
import androidx.print.PrintHelper
import com.all.pdfreader.pro.app.R
import com.all.pdfreader.pro.app.model.PrintResult
import com.all.pdfreader.pro.app.ui.adapter.PrintPdfAdapter
import com.shockwave.pdfium.PdfDocument
import com.shockwave.pdfium.PdfPasswordException
import com.shockwave.pdfium.PdfiumCore
import com.tom_roush.pdfbox.pdmodel.PDDocument
import com.tom_roush.pdfbox.pdmodel.common.PDPageLabelRange
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
object AppUtils {

View File

@ -326,7 +326,10 @@ object FileUtils {
return sdf.format(Date(this))
}
fun Long.toUnderscoreDateTime(): String {
val sdf = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.ENGLISH)
return sdf.format(Date(this))
}
data class FileInfo(
val name: String, val size: Long, val uri: Uri
)

View File

@ -2,8 +2,8 @@ package com.all.pdfreader.pro.app.util
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.os.ParcelFileDescriptor
import android.util.Log
import androidx.core.graphics.createBitmap
import com.all.pdfreader.pro.app.model.PdfPageItem
import com.shockwave.pdfium.PdfiumCore
@ -26,7 +26,8 @@ object PdfUtils {
fun clearPdfThumbsCache(context: Context) {
val cacheDir = File(context.cacheDir, child)
if (cacheDir.exists()) {
cacheDir.listFiles()?.forEach { it.delete() }
val ss = cacheDir.deleteRecursively() // Kotlin 提供的递归删除方法
Log.d("ocean", "clearPdfThumbsCache->$ss")
}
}
@ -66,13 +67,10 @@ object PdfUtils {
val targetHeight = (height * scale).toInt()
val bitmap = createBitmap(thumbWidth, targetHeight, Bitmap.Config.RGB_565)
val canvas = Canvas(bitmap)
canvas.scale(scale, scale)
pdfiumCore.renderPageBitmap(pdfDocument, bitmap, i, 0, 0, width, height)
pdfiumCore.renderPageBitmap(pdfDocument, bitmap, i, 0, 0, thumbWidth, targetHeight)
// 保存为压缩 JPEG
val outFile = File(cacheDir, "page_$i.jpg")
val outFile = File(cacheDir, inputFile.name + "_page_$i.jpg")
FileOutputStream(outFile).use { fos ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 70, fos)
}
@ -89,36 +87,45 @@ object PdfUtils {
}.flowOn(Dispatchers.IO)
/**
* 获取 PDF 总页数
*/
fun getPdfPageCount(inputFile: File): Int {
PDDocument.load(inputFile).use { return it.numberOfPages }
}
/**
* 导出用户勾选的页生成新的 PDF
* @param inputFile PDF 文件
* @param selectedPages 用户选择的页列表
* @param outputDir 输出目录外部存储可访问目录
* @param outputFileName 输出文件名
* 导出选中的 PDF 页到一个新的 PDF 文件
*
* 使用 PDFBox PDDocument 导出原 PDF 中选中的页支持进度回调
* 避免直接使用缩略图生成 PDF以保持原 PDF 的矢量质量
*
* @param inputFile PDF 文件
* @param selectedPages 选中的页对象列表每个对象包含 pageIndex
* @param outputDir 输出目录如果不存在会自动创建
* @param outputFileName 输出文件名例如 "selected.pdf"
* @param onProgress 可选回调当前处理进度 (current , total )
* @return 新生成的 PDF 文件失败返回 null
*/
suspend fun exportSelectedPages(
inputFile: File, selectedPages: List<PdfPageItem>, outputDir: File, outputFileName: String
inputFile: File,
selectedPages: List<PdfPageItem>,
outputDir: File,
outputFileName: String,
onProgress: ((current: Int, total: Int) -> Unit)? = null
): File? = withContext(Dispatchers.IO) {
// 如果没有选中的页,直接返回 null
if (selectedPages.isEmpty()) return@withContext null
if (!outputDir.exists()) outputDir.mkdirs() // 确保目录存在
if (!outputDir.exists()) outputDir.mkdirs()
val outputFile = File(outputDir, outputFileName)
try {
PDDocument.load(inputFile).use { document ->
val newDocument = PDDocument()
selectedPages.sortedBy { it.pageIndex }.forEach { pageItem ->
val page = document.getPage(pageItem.pageIndex)
newDocument.addPage(page)
PDDocument().use { newDocument ->
// 按页索引排序,保证顺序正确
val sortedPages = selectedPages.sortedBy { it.pageIndex }
val total = sortedPages.size//总数
sortedPages.forEachIndexed { index, pageItem ->
// 导入原文档的页面到新文档
newDocument.importPage(document.getPage(pageItem.pageIndex))
// 回调进度
onProgress?.invoke(index + 1, total)
}
// 保存新 PDF 文件
newDocument.save(outputFile)
}
newDocument.save(outputFile)
newDocument.close()
}
outputFile
} catch (e: Exception) {
@ -126,4 +133,5 @@ object PdfUtils {
null
}
}
}

View File

@ -2,6 +2,6 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="8dp"/>
<solid android:color="@color/grey"/>
<solid android:color="@color/placeholder_bg_color"/>
</shape>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 底部的虚线 -->
<item
android:gravity="bottom"
android:height="2dp">
<shape android:shape="line">
<stroke
android:width="1dp"
android:color="@color/line_color"
android:dashWidth="6dp"
android:dashGap="3dp" />
</shape>
</item>
</layer-list>

View File

@ -62,6 +62,25 @@
android:src="@drawable/gou_white" />
</LinearLayout>
<LinearLayout
android:id="@+id/addBtn"
android:layout_width="44dp"
android:layout_height="44dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:gravity="center">
<ImageView
android:id="@+id/addAll"
android:layout_width="24dp"
android:layout_height="24dp"
android:background="@drawable/dr_circular_sel_on_bg"
android:padding="2dp"
android:src="@drawable/add_icon_white" />
</LinearLayout>
</LinearLayout>
<include
@ -70,13 +89,16 @@
android:visibility="gone" />
<RelativeLayout
android:id="@+id/splitListLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:visibility="visible">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/splitRv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/continue_now_btn"
android:layout_margin="8dp"
android:overScrollMode="never"
android:scrollbars="none"
@ -86,11 +108,14 @@
tools:listitem="@layout/adapter_split_page_item" />
<LinearLayout
android:id="@+id/allow_access_btn"
android:id="@+id/continue_now_btn"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_alignParentBottom="true"
android:layout_margin="16dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:background="@drawable/dr_btn_not_clickable_bg"
android:gravity="center">
@ -103,4 +128,43 @@
android:textSize="16sp" />
</LinearLayout>
</RelativeLayout>
<RelativeLayout
android:id="@+id/splitSelectLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/splitSelectRv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/splitBtn"
android:layout_margin="8dp"
android:overScrollMode="never"
android:scrollbars="none"
tools:itemCount="3"
tools:listitem="@layout/adapter_split_selected_page_item" />
<LinearLayout
android:id="@+id/splitBtn"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_alignParentBottom="true"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:background="@drawable/dr_btn_not_clickable_bg"
android:gravity="center">
<TextView
style="@style/TextViewFont_PopSemiBold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/split_pdf"
android:textColor="@color/white"
android:textSize="16sp" />
</LinearLayout>
</RelativeLayout>
</LinearLayout>

View File

@ -0,0 +1,133 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/bg_color"
android:orientation="vertical">
<View
android:id="@+id/view"
android:layout_width="match_parent"
android:layout_height="0dp" />
<LinearLayout
android:id="@+id/statusLayout"
android:layout_width="match_parent"
android:layout_height="48dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/backBtn"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:gravity="center">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/back_black" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical">
<ImageView
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_marginTop="16dp"
android:background="@drawable/dr_circular_sel_on_bg"
android:src="@drawable/gou_white" />
<TextView
style="@style/TextViewFont_PopMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/congratulations"
android:textColor="@color/black"
android:textSize="20sp" />
<TextView
style="@style/TextViewFont_PopMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:gravity="center"
android:text="@string/file_created_success"
android:textColor="@color/black_60"
android:textSize="16sp" />
<View
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:layout_margin="16dp"
android:background="@color/line_color" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="150dp"
android:layout_margin="16dp"
tools:itemCount="3"
tools:listitem="@layout/adapter_split_selected_result_item" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp">
<LinearLayout
android:id="@+id/shareBtn"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_marginEnd="8dp"
android:layout_weight="1"
android:background="@drawable/dr_cancel_btn_bg"
android:gravity="center">
<TextView
style="@style/TextViewFont_PopSemiBold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/share_file"
android:textColor="@color/black_60"
android:textSize="16sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/okBtn"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:background="@drawable/dr_click_btn_bg"
android:gravity="center">
<TextView
android:id="@+id/okBtnTv"
style="@style/TextViewFont_PopSemiBold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/ok"
android:textColor="@color/white"
android:textSize="16sp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@ -19,11 +19,23 @@
android:layout_height="56dp"
android:background="@drawable/dr_item_img_frame">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/dr_placeholder_bg"
android:gravity="center">
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@drawable/file_document" />
</LinearLayout>
<ImageView
android:id="@+id/tvFileImg"
android:layout_width="56dp"
android:layout_height="56dp"
android:src="@mipmap/ic_launcher_round" />
android:layout_height="56dp" />
<LinearLayout
android:visibility="gone"

View File

@ -6,7 +6,7 @@
<RelativeLayout
android:id="@+id/itemBgLayout"
android:layout_width="match_parent"
android:layout_height="230dp"
android:layout_height="220dp"
android:layout_margin="8dp"
android:background="@drawable/dr_sel_off_frame"
android:gravity="center"
@ -28,8 +28,7 @@
<ImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop" />
android:layout_height="match_parent" />
<TextView
android:id="@+id/pageNumberTv"

View File

@ -0,0 +1,140 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="8dp"
android:paddingTop="8dp"
tools:ignore="RtlSymmetry">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<RelativeLayout
android:layout_width="56dp"
android:layout_height="56dp"
android:background="@drawable/dr_item_img_frame">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/dr_placeholder_bg"
android:gravity="center">
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@drawable/file_document" />
</LinearLayout>
<ImageView
android:id="@+id/tvFileImg"
android:layout_width="56dp"
android:layout_height="56dp" />
</RelativeLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:gravity="center_vertical"
android:orientation="vertical">
<LinearLayout
android:id="@+id/editTitleBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center">
<TextView
android:id="@+id/tvFileName"
style="@style/TextViewFont_PopMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="middle"
android:maxLines="1"
android:text="@string/app_name"
android:textColor="@color/icon_color"
android:textSize="16sp" />
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginStart="4dp"
android:layout_marginBottom="4dp"
android:src="@drawable/edit_icon" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="2dp"
android:layout_marginTop="2dp"
android:background="@drawable/underline_dashed" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
style="@style/TextViewFont_PopRegular"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/pages"
android:textColor="@color/black_60"
android:textSize="14sp" />
<TextView
android:id="@+id/tvPages"
style="@style/TextViewFont_PopRegular"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:ellipsize="end"
android:maxLines="1"
android:text="0"
android:textColor="@color/black_60"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/deleteBtn"
android:layout_width="48dp"
android:layout_height="48dp"
android:gravity="center">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/delete_cha_icon" />
</LinearLayout>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:background="@color/line_color" />
</LinearLayout>

View File

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="8dp"
android:paddingTop="8dp"
tools:ignore="RtlSymmetry">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<RelativeLayout
android:layout_width="56dp"
android:layout_height="56dp"
android:background="@drawable/dr_item_img_frame">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/dr_placeholder_bg"
android:gravity="center">
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@drawable/file_document" />
</LinearLayout>
<ImageView
android:id="@+id/image"
android:layout_width="56dp"
android:layout_height="56dp" />
</RelativeLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:gravity="center_vertical"
android:orientation="vertical">
<TextView
android:id="@+id/nameTv"
style="@style/TextViewFont_PopMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="middle"
android:maxLines="1"
android:text="@string/app_name"
android:textColor="@color/icon_color"
android:textSize="16sp" />
<TextView
android:id="@+id/pathTv"
style="@style/TextViewFont_PopRegular"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="2"
android:text="@string/path_details"
android:textColor="@color/black_60"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/selectBtn"
android:layout_width="48dp"
android:layout_height="48dp"
android:gravity="center">
<ImageView
android:id="@+id/selectIv"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/gou" />
</LinearLayout>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:background="@color/line_color" />
</LinearLayout>

View File

@ -18,6 +18,7 @@
<string name="sort_by">Sort by</string>
<string name="created_date">Created Date</string>
<string name="path">Path</string>
<string name="path_details">Path %1$s</string>
<string name="file_name">File Name</string>
<string name="file_size">File Size</string>
<string name="ascending">Ascending</string>
@ -120,8 +121,16 @@
<string name="bookmark_loading">Loading bookmarks, please try again later</string>
<string name="no_files_yet">no files yet</string>
<string name="split_pdf">Split PDF</string>
<string name="split">Split</string>
<string name="merge_pdf">Merge PDF</string>
<string name="selected_page">%1$d Selected</string>
<string name="loading">Loading</string>
<string name="continue_now">Continue Now</string>
<string name="exit_split">Exit Split</string>
<string name="confirm_discard_changes">Are you sure you want to exit and discard changes?</string>
<string name="discard">Discard</string>
<string name="pages">Pages:</string>
<string name="congratulations">Congratulations</string>
<string name="file_created_success">Your file has been successfully created</string>
<string name="please_select_page">Please select at least one page</string>
</resources>