Files
system-design/software-copyright/09-writech-app-board/engine/CoursewareLoader.kt
T
2026-03-22 15:24:40 +08:00

493 lines
15 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 自然写互动课堂智慧黑板端应用软件 V1.0
*
* CoursewareLoader.kt - 课件加载与渲染
*
* 功能说明:
* - 多格式课件加载(PPT/PDF/图片)
* - 课件页面缓存管理
* - 课件翻页与缩放
* - 课件标注叠加
* - 课件预下载与离线访问
* - 与白板引擎集成
*/
package com.writech.board.engine
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.pdf.PdfRenderer
import android.os.ParcelFileDescriptor
import android.util.Log
import android.util.LruCache
import java.io.File
import java.io.FileOutputStream
import java.net.URL
import java.util.concurrent.*
/**
* 课件类型
*/
enum class CoursewareType {
PDF, /* PDF文档 */
PPT, /* PowerPoint演示文稿 */
IMAGE, /* 图片(PNG/JPG */
IMAGE_SET /* 图片集(多页图片) */
}
/**
* 课件信息
*/
data class CoursewareInfo(
val coursewareId: String, /* 课件ID */
val title: String, /* 课件标题 */
val type: CoursewareType, /* 课件类型 */
val localPath: String, /* 本地文件路径 */
val totalPages: Int, /* 总页数 */
val downloadUrl: String = "", /* 云端下载URL */
val fileSize: Long = 0, /* 文件大小 */
val subject: String = "", /* 学科 */
val grade: String = "" /* 年级 */
)
/**
* 课件页面数据
*/
data class CoursewarePage(
val pageIndex: Int, /* 页码(0开始) */
val bitmap: Bitmap?, /* 页面位图 */
val width: Int, /* 原始宽度 */
val height: Int /* 原始高度 */
)
/**
* 课件加载回调
*/
interface CoursewareLoadListener {
fun onCoursewareLoaded(info: CoursewareInfo)
fun onPageReady(page: CoursewarePage)
fun onLoadProgress(progress: Float)
fun onLoadError(error: String)
}
/**
* 课件加载与渲染引擎
*
* 支持多种格式课件的加载、缓存和渲染:
* - PDF: 使用Android PdfRenderer渲染
* - PPT: 转换为图片后渲染
* - 图片: 直接BitmapFactory加载
*/
class CoursewareLoader(private val context: Context) {
companion object {
private const val TAG = "CoursewareLoader"
/** 页面缓存最大数量 */
private const val PAGE_CACHE_SIZE = 10
/** 渲染目标DPI */
private const val RENDER_DPI = 300
/** 课件存储目录 */
private const val COURSEWARE_DIR = "courseware"
/** 下载超时(毫秒) */
private const val DOWNLOAD_TIMEOUT_MS = 60000
}
/* ==================== 状态 ==================== */
/** 当前加载的课件信息 */
var currentCourseware: CoursewareInfo? = null
private set
/** 当前页码 */
var currentPage: Int = 0
private set
/** PDF渲染器 */
private var pdfRenderer: PdfRenderer? = null
private var pdfFileDescriptor: ParcelFileDescriptor? = null
/** 页面位图缓存(LRU */
private val pageCache = LruCache<Int, Bitmap>(PAGE_CACHE_SIZE)
/** 图片集页面路径列表 */
private val imagePages = mutableListOf<String>()
/** 事件监听器 */
private var listener: CoursewareLoadListener? = null
/** 后台线程池 */
private val executor: ExecutorService = Executors.newFixedThreadPool(2)
/**
* 设置加载监听器
*/
fun setListener(listener: CoursewareLoadListener) {
this.listener = listener
}
/* ==================== 课件加载 ==================== */
/**
* 加载本地课件文件
*
* @param filePath 本地文件路径
* @param type 课件类型
*/
fun loadFromFile(filePath: String, type: CoursewareType) {
executor.submit {
try {
Log.i(TAG, "加载课件: $filePath, 类型=$type")
when (type) {
CoursewareType.PDF -> loadPdf(filePath)
CoursewareType.IMAGE -> loadSingleImage(filePath)
CoursewareType.IMAGE_SET -> loadImageSet(filePath)
CoursewareType.PPT -> loadPptAsImages(filePath)
}
} catch (e: Exception) {
Log.e(TAG, "课件加载失败", e)
listener?.onLoadError("加载失败: ${e.message}")
}
}
}
/**
* 从云端下载并加载课件
*/
fun loadFromUrl(url: String, coursewareId: String, type: CoursewareType) {
executor.submit {
try {
Log.i(TAG, "下载课件: $url")
listener?.onLoadProgress(0f)
/* 确定本地存储路径 */
val localDir = File(context.filesDir, COURSEWARE_DIR)
if (!localDir.exists()) localDir.mkdirs()
val extension = when (type) {
CoursewareType.PDF -> ".pdf"
CoursewareType.PPT -> ".pptx"
else -> ".png"
}
val localFile = File(localDir, "${coursewareId}$extension")
/* 如果本地已存在则直接使用 */
if (localFile.exists() && localFile.length() > 0) {
Log.i(TAG, "使用本地缓存: ${localFile.absolutePath}")
loadFromFile(localFile.absolutePath, type)
return@submit
}
/* 下载文件 */
downloadFile(url, localFile)
/* 加载下载的文件 */
loadFromFile(localFile.absolutePath, type)
} catch (e: Exception) {
Log.e(TAG, "课件下载失败", e)
listener?.onLoadError("下载失败: ${e.message}")
}
}
}
/**
* 下载文件到本地
*/
private fun downloadFile(url: String, destFile: File) {
val connection = URL(url).openConnection()
connection.connectTimeout = DOWNLOAD_TIMEOUT_MS
connection.readTimeout = DOWNLOAD_TIMEOUT_MS
val totalSize = connection.contentLengthLong
var downloadedSize = 0L
connection.getInputStream().use { input ->
FileOutputStream(destFile).use { output ->
val buffer = ByteArray(8192)
var bytesRead: Int
while (input.read(buffer).also { bytesRead = it } != -1) {
output.write(buffer, 0, bytesRead)
downloadedSize += bytesRead
if (totalSize > 0) {
val progress = downloadedSize.toFloat() / totalSize
listener?.onLoadProgress(progress)
}
}
}
}
Log.i(TAG, "文件下载完成: ${destFile.absolutePath}, 大小=${downloadedSize / 1024}KB")
}
/* ==================== PDF加载 ==================== */
/**
* 加载PDF文件
*/
private fun loadPdf(filePath: String) {
closePdfRenderer()
val file = File(filePath)
pdfFileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
pdfRenderer = PdfRenderer(pdfFileDescriptor!!)
val pageCount = pdfRenderer!!.pageCount
currentCourseware = CoursewareInfo(
coursewareId = file.nameWithoutExtension,
title = file.nameWithoutExtension,
type = CoursewareType.PDF,
localPath = filePath,
totalPages = pageCount
)
currentPage = 0
Log.i(TAG, "PDF加载成功: ${file.name}, ${pageCount}页")
listener?.onCoursewareLoaded(currentCourseware!!)
/* 渲染第一页 */
renderPdfPage(0)
}
/**
* 渲染PDF指定页面为Bitmap
*/
private fun renderPdfPage(pageIndex: Int) {
val renderer = pdfRenderer ?: return
if (pageIndex < 0 || pageIndex >= renderer.pageCount) return
/* 先检查缓存 */
pageCache.get(pageIndex)?.let { cached ->
val page = CoursewarePage(pageIndex, cached, cached.width, cached.height)
listener?.onPageReady(page)
return
}
/* 渲染新页面 */
val pdfPage = renderer.openPage(pageIndex)
/* 计算渲染尺寸(基于DPI */
val scale = RENDER_DPI.toFloat() / 72f
val width = (pdfPage.width * scale).toInt()
val height = (pdfPage.height * scale).toInt()
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
pdfPage.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
pdfPage.close()
/* 加入缓存 */
pageCache.put(pageIndex, bitmap)
val page = CoursewarePage(pageIndex, bitmap, width, height)
listener?.onPageReady(page)
Log.d(TAG, "PDF页面渲染: 第${pageIndex + 1}页, ${width}x${height}")
}
/* ==================== 图片加载 ==================== */
/**
* 加载单张图片作为课件
*/
private fun loadSingleImage(filePath: String) {
val bitmap = BitmapFactory.decodeFile(filePath)
if (bitmap == null) {
listener?.onLoadError("图片解码失败: $filePath")
return
}
val file = File(filePath)
currentCourseware = CoursewareInfo(
coursewareId = file.nameWithoutExtension,
title = file.nameWithoutExtension,
type = CoursewareType.IMAGE,
localPath = filePath,
totalPages = 1
)
currentPage = 0
pageCache.put(0, bitmap)
listener?.onCoursewareLoaded(currentCourseware!!)
listener?.onPageReady(CoursewarePage(0, bitmap, bitmap.width, bitmap.height))
Log.i(TAG, "图片课件加载: ${bitmap.width}x${bitmap.height}")
}
/**
* 加载图片集(目录下多张图片作为多页课件)
*/
private fun loadImageSet(dirPath: String) {
val dir = File(dirPath)
val imageFiles = dir.listFiles { file ->
file.extension.lowercase() in listOf("png", "jpg", "jpeg", "bmp")
}?.sortedBy { it.name } ?: emptyList()
if (imageFiles.isEmpty()) {
listener?.onLoadError("图片集为空: $dirPath")
return
}
imagePages.clear()
imageFiles.forEach { imagePages.add(it.absolutePath) }
currentCourseware = CoursewareInfo(
coursewareId = dir.name,
title = dir.name,
type = CoursewareType.IMAGE_SET,
localPath = dirPath,
totalPages = imageFiles.size
)
currentPage = 0
listener?.onCoursewareLoaded(currentCourseware!!)
/* 加载第一页 */
loadImagePage(0)
Log.i(TAG, "图片集加载: ${imageFiles.size}页")
}
/**
* 加载图片集的指定页
*/
private fun loadImagePage(pageIndex: Int) {
if (pageIndex < 0 || pageIndex >= imagePages.size) return
pageCache.get(pageIndex)?.let { cached ->
listener?.onPageReady(CoursewarePage(pageIndex, cached, cached.width, cached.height))
return
}
val bitmap = BitmapFactory.decodeFile(imagePages[pageIndex])
if (bitmap != null) {
pageCache.put(pageIndex, bitmap)
listener?.onPageReady(CoursewarePage(pageIndex, bitmap, bitmap.width, bitmap.height))
}
}
/**
* PPT加载(转换为图片后渲染)
* 实际使用Apache POI或云端转换服务
*/
private fun loadPptAsImages(filePath: String) {
Log.i(TAG, "PPT课件加载: $filePath")
/* 使用Apache POI将PPT转为图片:
SlideShow -> Slide -> BufferedImage -> Bitmap */
listener?.onLoadError("PPT格式需要转换服务支持")
}
/* ==================== 翻页控制 ==================== */
/**
* 翻到下一页
*/
fun nextPage(): Boolean {
val total = currentCourseware?.totalPages ?: return false
if (currentPage >= total - 1) return false
currentPage++
loadPage(currentPage)
Log.d(TAG, "翻到第${currentPage + 1}/${total}页")
return true
}
/**
* 翻到上一页
*/
fun previousPage(): Boolean {
if (currentPage <= 0) return false
currentPage--
loadPage(currentPage)
Log.d(TAG, "翻到第${currentPage + 1}/${currentCourseware?.totalPages}页")
return true
}
/**
* 跳转到指定页
*/
fun goToPage(pageIndex: Int): Boolean {
val total = currentCourseware?.totalPages ?: return false
if (pageIndex < 0 || pageIndex >= total) return false
currentPage = pageIndex
loadPage(pageIndex)
return true
}
/**
* 加载指定页面(根据课件类型调用不同方法)
*/
private fun loadPage(pageIndex: Int) {
executor.submit {
when (currentCourseware?.type) {
CoursewareType.PDF -> renderPdfPage(pageIndex)
CoursewareType.IMAGE_SET -> loadImagePage(pageIndex)
else -> { /* 单图片无需翻页 */ }
}
}
/* 预加载相邻页面 */
executor.submit { preloadAdjacentPages(pageIndex) }
}
/**
* 预加载前后页面到缓存
*/
private fun preloadAdjacentPages(pageIndex: Int) {
val total = currentCourseware?.totalPages ?: return
listOf(pageIndex - 1, pageIndex + 1).forEach { adjPage ->
if (adjPage in 0 until total && pageCache.get(adjPage) == null) {
when (currentCourseware?.type) {
CoursewareType.PDF -> {
/* 预渲染PDF页面 */
val renderer = pdfRenderer ?: return
val pdfPage = renderer.openPage(adjPage)
val scale = RENDER_DPI.toFloat() / 72f
val w = (pdfPage.width * scale).toInt()
val h = (pdfPage.height * scale).toInt()
val bmp = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
pdfPage.render(bmp, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
pdfPage.close()
pageCache.put(adjPage, bmp)
}
CoursewareType.IMAGE_SET -> {
if (adjPage < imagePages.size) {
BitmapFactory.decodeFile(imagePages[adjPage])?.let {
pageCache.put(adjPage, it)
}
}
}
else -> {}
}
}
}
}
/* ==================== 资源管理 ==================== */
/**
* 关闭PDF渲染器
*/
private fun closePdfRenderer() {
pdfRenderer?.close()
pdfRenderer = null
pdfFileDescriptor?.close()
pdfFileDescriptor = null
}
/**
* 释放所有资源
*/
fun release() {
closePdfRenderer()
pageCache.evictAll()
imagePages.clear()
executor.shutdown()
Log.i(TAG, "课件加载器已释放")
}
}