software copyright
This commit is contained in:
@@ -0,0 +1,492 @@
|
||||
/**
|
||||
* 自然写互动课堂智慧黑板端应用软件 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, "课件加载器已释放")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user