/** * 自然写互动课堂智慧黑板端应用软件 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(PAGE_CACHE_SIZE) /** 图片集页面路径列表 */ private val imagePages = mutableListOf() /** 事件监听器 */ 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, "课件加载器已释放") } }