18 Commits 6ac8ecdb84 ... d06e23080b

Author SHA1 Message Date
  Wayne Huang d06e23080b Merge branch '124-implementAdBetweenPages' into 'master' 4 years ago
  cooperku_kdanmobile 57f1ad8505 AdditionalPageManager koin inject 5 years ago
  cooperku_kdanmobile 5d623a141e Add range check 5 years ago
  cooperku_kdanmobile 26b1231001 Refactor: remove interfaces 5 years ago
  cooperku_kdanmobile 7822be65a0 Refactor: rename 5 years ago
  cooperku_kdanmobile 415bed9766 Fix wrong visible page bug 5 years ago
  cooperku_kdanmobile f09c463123 Refactor: rename 5 years ago
  cooperku_kdanmobile 6453574cdc Merge branch 'implementClosePageAd' into 124-implementAdBetweenPages 5 years ago
  cooperku_kdanmobile d4d820099c Add AdditionalPageManager::tryToHideAdditionalPageAt(position) 5 years ago
  cooperku_kdanmobile 109422b836 Fix wrong state bug 5 years ago
  cooperku_kdanmobile 0f9595d770 Merge branch 'refactorPageAd' into 124-implementAdBetweenPages 5 years ago
  cooperku_kdanmobile 190294a480 Refactor 5 years ago
  Cooper Ku 1a0597f84f Merge branch '127-removeAdPage' into '124-implementAdBetweenPages' 5 years ago
  cooperku_kdanmobile 6dc69c34c5 Add function ReaderActivity::removeAdPages() 5 years ago
  cooperku_kdanmobile bbcb6fcdb5 Fix wrong comments 5 years ago
  cooperku_kdanmobile 5dd8abd02c Apply custom KMPDFPageAdapter, KMPDFPageView & KMPDFReaderView 5 years ago
  cooperku_kdanmobile 987807781e Add custom KMPDFPageAdapter, KMPDFPageView & KMPDFReaderView 5 years ago
  cooperku_kdanmobile b1c584f202 Add AdPageHelper 5 years ago

+ 40 - 4
src/main/java/com/kdanmobile/reader/ReaderActivity.kt

@@ -3,6 +3,7 @@ package com.kdanmobile.reader
 import android.annotation.SuppressLint
 import android.app.Activity
 import android.app.Dialog
+import android.content.Context
 import androidx.lifecycle.Observer
 import android.content.Intent
 import android.net.Uri
@@ -18,9 +19,11 @@ import android.view.*
 import android.view.animation.AnimationUtils
 import android.widget.*
 import com.kdanmobile.base.KdanBaseActivity
-import com.kdanmobile.kmpdfkit.pdfcommon.KMPDFReaderView
 import com.kdanmobile.kmpdfkit.pdfcommon.PDFInfo
+import com.kdanmobile.kmpdfkit.manager.KMPDFFactory
+import com.kdanmobile.kmpdfkit.pdfcommon.*
 import com.kdanmobile.reader.Utils.applyConstraintSet
+import com.kdanmobile.reader.additionalpage.*
 import com.kdanmobile.reader.annotationattribute.AnnotationAttribute
 import com.kdanmobile.reader.annotationattribute.AnnotationColor
 import com.kdanmobile.reader.annotationattribute.InkAttribute
@@ -77,6 +80,9 @@ abstract class ReaderActivity :
     open fun loadCurrentPageIndex(filename: String, defaultValue: Int): Int = 0
     open fun onOpenedFile() { /* do nothing */ }
     open fun onFilePathOrUriError(filePath: String?) { println("onFilePathOrUriError: $filePath") }
+    //  子類別可複寫此方法以客製化KMPDFPageAdapter
+    abstract fun providePdfPageAdapter(context: Context, filePickerSupport: FilePicker.FilePickerSupport, kmpdfFactory: KMPDFFactory): KMPDFPageAdapter
+    open fun provideAdditionalPageManager(): AdditionalPageManager = AdditionalPageManager()
     protected open fun onEvent(hookEvent: HookEvent) { /* do nothing */ }
 
     companion object {
@@ -115,8 +121,13 @@ abstract class ReaderActivity :
         get() {
             return viewModel.readerModel
         }
+    protected val additionalPageManager: AdditionalPageManager
+        get() {
+            return viewModel.additionalPageManager
+        }
 
     private val viewModel: ReaderViewModel by viewModel {
+        val additionalPageManager = provideAdditionalPageManager()
         val filePath = intent.getStringExtra(KEY_FILE_ABSOLUTE)
         if (!filePath.isNullOrEmpty()) {
             insertToRecentDocumentList(filePath as String)
@@ -124,7 +135,7 @@ abstract class ReaderActivity :
         val uri = if (!filePath.isNullOrEmpty()) Uri.parse(Uri.encode(filePath)) else null
         // The URI may be null in some special case.
         // Just use Uri.EMPTY and show an alert dialog in onCreate() when the uri is null.
-        parametersOf(uri ?: Uri.EMPTY)
+        parametersOf(additionalPageManager, uri ?: Uri.EMPTY)
     }
     private var isShowPasswordActivity = false
 
@@ -449,13 +460,13 @@ abstract class ReaderActivity :
         container.removeAllViews()
         if (!isOpened) return
         val context = this
-        val readerView = object : KMPDFReaderView(context) {
+        //  採用客製化的KMPDFReaderView以覆寫畫面點擊判斷和當前頁面計算方法
+        val readerView = object : AdditionalPageReaderView(context, additionalPageManager.pageConverter) {
             @SuppressLint("ClickableViewAccessibility")
             override fun onTouchEvent(motionEvent: MotionEvent): Boolean {
                 if (motionEvent.action == MotionEvent.ACTION_UP) {
                     if (viewModel.isCopyModeLiveData.value == true) {
                         if (viewModel.copySelection()) {
-                            val context = this@ReaderActivity
                             Toast.makeText(context, R.string.reader_copy_text_success, Toast.LENGTH_SHORT).show()
                         }
                     }
@@ -465,6 +476,8 @@ abstract class ReaderActivity :
 
             override fun onMoveToChild(pageIndex: Int) {
                 viewModel.setPageIndex(pageIndex)
+                //  畫面更新時應通知additionalPageManager
+                additionalPageManager.onPageChanged(pageIndex)
             }
 
             override fun onScrolling() {
@@ -493,6 +506,20 @@ abstract class ReaderActivity :
             }
         }
         viewModel.setReaderView(readerView)
+
+        /**
+         * 使用客製化KMPDFPageAdapter取代內建的Adapter
+         * 注意,需要在KMPDFFactory::setReaderView之後調用才不會被複寫
+         */
+        viewModel.getReaderView()?.apply {
+            adapter = providePdfPageAdapter(
+                    context,
+                    FilePicker.FilePickerSupport {},
+                    viewModel.readerModel.kmpdfFactory!!
+            )
+            refresh(true)
+        }
+
         if (!filePath.isNullOrEmpty()) {
             val defaultValue = viewModel.pageIndexLiveData.value ?: 0
             val pageIndex = loadCurrentPageIndex(filePath as String, defaultValue)
@@ -511,6 +538,15 @@ abstract class ReaderActivity :
         onOpenedFile()
     }
 
+    fun removeAdditionalPages() {
+        if (additionalPageManager.displayStrategyType == AdditionalPageDisplayStrategyType.HIDE) return
+        val handler = viewModel.pdfInfoHandler
+        val pageIndex = currentPageIndex
+        additionalPageManager.displayStrategyType = AdditionalPageDisplayStrategyType.HIDE
+        viewModel.getReaderView()?.refresh(true)
+        handler.goToCurrentPage(pageIndex)
+    }
+
     private fun initTextBoxContextMenuActions() {
         viewModel.setTextBoxContextMenuActions(object : TextBoxContextMenuActionListener {
             override fun onDelete(): Boolean {

+ 16 - 3
src/main/java/com/kdanmobile/reader/ReaderModel.kt

@@ -11,6 +11,7 @@ import com.kdanmobile.kmpdfkit.pdfcommon.OutlineItem
 import com.kdanmobile.kmpdfkit.pdfcommon.TextChar
 import com.kdanmobile.kmpdfkit.pdfcommon.TextWord
 import com.kdanmobile.reader.screen.handler.*
+import com.kdanmobile.reader.additionalpage.AdditionalPageManager
 
 class ReaderModel {
     private var filename: String? = null
@@ -18,6 +19,9 @@ class ReaderModel {
     var password: String = ""
         private set
 
+    var additionalPageManager: AdditionalPageManager? = null
+        private set
+
     var kmpdfFactory: KMPDFFactory? = null
         private set
 
@@ -43,9 +47,16 @@ class ReaderModel {
         this.isInitialized = true
     }
 
+    fun initAdditionalPageManager(additionalPageManager: AdditionalPageManager?) {
+        this.additionalPageManager = additionalPageManager
+    }
+
     fun initKMPDFDocumentController() {
         kmpdfDocumentController = kmpdfFactory?.getController(KMPDFFactory.ControllerType.DOCUMENT) as KMPDFDocumentController
         kmpdfSignatureController = kmpdfFactory?.getController(KMPDFFactory.ControllerType.SIGNATURE) as KMPDFSignatureController
+        kmpdfDocumentController?.also {
+            additionalPageManager?.pageConverter?.kmpdfDocumentController = it
+        }
     }
 
     @Synchronized
@@ -58,6 +69,7 @@ class ReaderModel {
         kmpdfDocumentController = null
         kmpdfSignatureController = null
         onPdfChangedListener = null
+        additionalPageManager = null
     }
 
     val pdfInfoHandler = object : PdfInfoHandler {
@@ -66,7 +78,7 @@ class ReaderModel {
         }
 
         override fun getPdfPageCount(isNativeRefresh: Boolean): Int {
-            return kmpdfDocumentController?.getDocumentPageCount(isNativeRefresh) ?: 0
+            return additionalPageManager?.pageConverter?.getRawPageCount(isNativeRefresh) ?: 0
         }
 
         override fun getCurrentPage(): Int {
@@ -114,7 +126,7 @@ class ReaderModel {
         }
 
         override fun setSearchResult(page: Int, keyword: String, rectArray: Array<RectF>): Boolean {
-            return kmpdfDocumentController?.setSearchResult(keyword, page, rectArray) ?: false
+            return kmpdfDocumentController?.setSearchResult(keyword, additionalPageManager?.pageConverter?.convertToPageIndex(page) ?: 0, rectArray) ?: false
         }
 
         override fun stopSearchKeyWord(): Boolean {
@@ -130,12 +142,13 @@ class ReaderModel {
         override fun deletePages(pages: IntArray): Boolean {
             return kmpdfDocumentController?.deletePages(pages) ?: false
         }
+
         override fun save(): Boolean {
             return kmpdfDocumentController?.save() ?: false
         }
 
         override fun reorderPage(fromPage: Int, toPage: Int): Boolean {
-            return kmpdfDocumentController?.reorderPages(fromPage,toPage) ?: false
+            return kmpdfDocumentController?.reorderPages(fromPage, toPage) ?: false
         }
 
         override fun splitPDFWithPages(path: String, selectPage: IntArray): Boolean {

+ 34 - 7
src/main/java/com/kdanmobile/reader/ReaderViewModel.kt

@@ -33,12 +33,15 @@ import com.kdanmobile.reader.screen.data.SignatureAttribute
 import com.kdanmobile.reader.screen.data.StampAttribute
 import com.kdanmobile.reader.screen.data.TextBoxAttribute
 import com.kdanmobile.reader.screen.handler.*
+import com.kdanmobile.reader.additionalpage.AdditionalPageManager
 import java.io.File
 import java.util.*
 import kotlin.collections.ArrayList
 
 class ReaderViewModel(
-        private val readerModelManager: ReaderModelManager, val uri: Uri,
+        private val readerModelManager: ReaderModelManager,
+        additionalPageManager: AdditionalPageManager,
+        val uri: Uri,
         private val pdfSdkLicense: String,
         private val pdfSdkRsaMsg: String,
         private val eventManager: EventManager<Event> = EventManager()
@@ -206,6 +209,17 @@ class ReaderViewModel(
 
     private var onClickLinkListener: OnClickLinkListener? = null
 
+    var additionalPageManager: AdditionalPageManager = additionalPageManager.also {
+        readerModel.initAdditionalPageManager(it)
+    }
+        set(value) {
+            field = value
+            readerModel.initAdditionalPageManager(field)
+            kmpdfDocumentController?.also {
+                field.pageConverter.kmpdfDocumentController = it
+            }
+        }
+
     @JvmOverloads
     fun openPdfFile(context: Context, password: String, onRequestPassword: Runnable, type: String? = null): OpenFileResult {
         if (!isVerified) {
@@ -456,7 +470,6 @@ class ReaderViewModel(
             }
 
             override fun onAttachAnnotWidgetFinished(type: Annotation.Type) {
-                println("KMPDFAddAnnotCallback::onAttachAnnotWidgetFinished")
                 val mode = when (type) {
                     Annotation.Type.FREETEXT -> KMPDFAnnotEditMode.Mode.FREETEXT_MODIFY
                     Annotation.Type.STAMP -> KMPDFAnnotEditMode.Mode.STAMP_MODIFY
@@ -537,10 +550,14 @@ class ReaderViewModel(
         restoreStateBeforeDestroy()
     }
 
+    /**
+     * 之前採用以下方式會遮蔽額外頁面拖曳,原因尚不清楚...
+     * kmpdfFactory?.setAnnotationEditMode(KMPDFAnnotationBean.AnnotationType.NULL)
+     * kmpdfFactory?.kmpdfAnnotEditMode?.pdfAnnotEditMode = KMPDFAnnotEditMode.Mode.NULL
+     * annotationEitModeLiveData.postValue(AnnotationEitMode.NULL)
+     */
     fun clearSelection() {
-        kmpdfFactory?.setAnnotationEditMode(KMPDFAnnotationBean.AnnotationType.NULL)
-        kmpdfFactory?.kmpdfAnnotEditMode?.pdfAnnotEditMode = KMPDFAnnotEditMode.Mode.NULL
-        annotationEitModeLiveData.postValue(AnnotationEitMode.NULL)
+        kmpdfDocumentController?.deselectCurrentPageAnnotation()
     }
 
     fun deleteSelectedTextBox() {
@@ -559,8 +576,9 @@ class ReaderViewModel(
         (kmpdfFactory?.getController(KMPDFFactory.ControllerType.FREETEXT) as KMPDFFreeTextController).copyFreeTextContent()
     }
 
-    fun setPageIndex(pageIndex: Int){
-        mPageIndexLiveData.value = pageIndex
+    fun setPageIndex(pageIndex: Int) {
+        //  設定為原始文件頁數
+        mPageIndexLiveData.value = additionalPageManager.pageConverter.convertToRawPageIndex(pageIndex)
     }
 
     fun addBookmark(title: String) {
@@ -608,9 +626,18 @@ class ReaderViewModel(
         return errorCode == 0
     }
 
+    /**
+     * 目前在水平閱覽模式時會移除額外頁面,導致兩者總頁數不一致
+     * 更動總頁數需要重新設定當前頁面
+     */
     private fun updateViewDirection() {
+        val pageCount = additionalPageManager.pageConverter.getPageCount(false)
+        val pageIndex = pdfInfoHandler.getCurrentPage()
         kmpdfDocumentController?.pdfViewMode = viewDirection.mode
         kmpdfDocumentController?.refresh(false)
+        if (pageCount != additionalPageManager.pageConverter.getPageCount(false)) {
+            pdfInfoHandler.goToCurrentPage(pageIndex)
+        }
     }
 
     private fun updateReadMode() {

+ 63 - 0
src/main/java/com/kdanmobile/reader/additionalpage/AbstractAdditionalPageAdapter.kt

@@ -0,0 +1,63 @@
+package com.kdanmobile.reader.additionalpage
+
+import android.content.Context
+import android.graphics.Point
+import android.view.View
+import android.view.ViewGroup
+import com.kdanmobile.kmpdfkit.manager.KMPDFFactory
+import com.kdanmobile.kmpdfkit.pdfcommon.FilePicker
+import com.kdanmobile.kmpdfkit.pdfcommon.KMPDFPageAdapter
+
+/**
+ * 客製化KMPDFPageAdapter
+ *
+ * 負責創建額外頁面
+ */
+abstract class AbstractAdditionalPageAdapter(
+        protected val context: Context,
+        private val filePickerSupport: FilePicker.FilePickerSupport,
+        private val kmpdfFactory: KMPDFFactory,
+        protected val additionalPageManager: AdditionalPageManager
+) : KMPDFPageAdapter(context, filePickerSupport, kmpdfFactory) {
+    private val pageConverter = additionalPageManager.pageConverter
+
+    /**
+     * 設定額外頁面內容
+     * 子類別可覆寫此方法以客製化不同視覺設計
+     */
+    abstract fun setupViewContent(position: Int, pageView: AdditionalPageView)
+
+    /**
+     * 總頁數應包含額外頁面
+     */
+    override fun getCount(): Int {
+        return pageConverter.getPageCount()
+    }
+
+    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
+        //  判斷頁面是否為額外頁面
+        val isAdPage = pageConverter.isAdditionalPage(position)
+        //  建立或讀取客製化KMPDFPageView
+        val pageView = if (convertView == null) {
+            AdditionalPageView(context, filePickerSupport, kmpdfFactory, Point(parent?.width ?: 0, parent?.height ?: 0), isAdPage)
+        } else {
+            convertView as AdditionalPageView
+        }
+        when (isAdPage) {
+            true -> {
+                //  設定KMPDFPageView
+                pageView.apply {
+                    additionalPageHeight = AdditionalPageView.DEFAULT_ADDITIONAL_PAGE_HEIGHT
+                    setupViewContent(position, this)
+                    isAdditionalPageShowing = additionalPageManager.isAdditionalPageVisible(position)
+                }
+            }
+            false -> {
+                //  取得文件原始頁數
+                val page = pageConverter.convertToRawPageIndex(position)
+                super.getView(page, pageView, parent)
+            }
+        }
+        return pageView
+    }
+}

+ 112 - 0
src/main/java/com/kdanmobile/reader/additionalpage/AdditionalPageConverter.kt

@@ -0,0 +1,112 @@
+package com.kdanmobile.reader.additionalpage
+
+import androidx.annotation.IntRange
+import com.kdanmobile.kmpdfkit.globaldata.Config
+import com.kdanmobile.kmpdfkit.manager.controller.KMPDFDocumentController
+import kotlin.math.max
+
+/**
+ * 額外頁面頁碼轉換器
+ * 以及判斷哪些頁面是額外頁面
+ *
+ * <名詞解釋>
+ *     額外頁面:在原始文件之間插入的空頁面,之後可在其上插入其他內容(例如廣告)
+ *
+ * 預設在原始文件的每頁之間都插入額外頁面,因此每隔一頁都可以插入一個廣告
+ */
+class AdditionalPageConverter(
+        //  原始文件每隔幾頁是「額外頁面」
+        @IntRange(from = 1) additionalPageInterval: Int = DEFAULT_ADDITIONAL_PAGE_INTERVAL,
+        //  第一個額外頁面至少是從原始文件第幾頁開始
+        @IntRange(from = 1) firstAdditionalPageIndex: Int = DEFAULT_FIRST_ADDITIONAL_PAGE_INDEX
+) {
+
+    companion object {
+        //  原始文件每隔幾頁是「額外頁面」
+        const val DEFAULT_ADDITIONAL_PAGE_INTERVAL = 1
+        //  第一個額外頁面至少是從原始文件第幾頁開始
+        const val DEFAULT_FIRST_ADDITIONAL_PAGE_INDEX = 1
+    }
+
+    private val additionalPageInterval: Int = max(DEFAULT_ADDITIONAL_PAGE_INTERVAL, additionalPageInterval)
+
+    private val firstAdditionalPageIndex: Int = max(DEFAULT_FIRST_ADDITIONAL_PAGE_INDEX, firstAdditionalPageIndex)
+
+    var kmpdfDocumentController: KMPDFDocumentController? = null
+
+    /**
+     * 是否啟用額外頁面(例:已訂閱則不顯示)
+     */
+    var isAdditionalPageEnabled: () -> Boolean = { true }
+
+    /**
+     * 判斷文件第pageIndex頁是否是額外頁面
+     */
+    fun isAdditionalPage(pageIndex: Int): Boolean {
+        if (!shouldApplyAdditionalPage()) return false
+        if (pageIndex < firstAdditionalPageIndex) return false
+        return (pageIndex - firstAdditionalPageIndex) % (additionalPageInterval + 1) == 0
+    }
+
+    /**
+     * 頁碼轉換,計算原始文件的第rawPageIndex頁在插入額外頁面後是第幾頁
+     */
+    fun convertToPageIndex(rawPageIndex: Int): Int {
+        if (!shouldApplyAdditionalPage()) return rawPageIndex
+        if (rawPageIndex < firstAdditionalPageIndex) return rawPageIndex
+        return rawPageIndex + 1 + (rawPageIndex - firstAdditionalPageIndex) / additionalPageInterval
+    }
+
+    /**
+     * 頁碼轉換,計算第pageIndex頁刪除額外頁面後是原始文件的第幾頁
+     */
+    fun convertToRawPageIndex(pageIndex: Int): Int {
+        if (!shouldApplyAdditionalPage()) return pageIndex
+        if (pageIndex < firstAdditionalPageIndex) return pageIndex
+        return (pageIndex * additionalPageInterval + firstAdditionalPageIndex) / (additionalPageInterval + 1)
+    }
+
+    /**
+     * 取得包含額外頁面的總頁數
+     */
+    fun getPageCount(isNativeRefresh: Boolean = false): Int {
+        val rawPageCount = getRawPageCount(isNativeRefresh)
+        if (!shouldApplyAdditionalPage()) return rawPageCount
+        val pageCount = convertToPageIndex(rawPageCount)
+        val lastPage = pageCount - 1
+        //  最後一頁不能是額外頁面
+        return when (isAdditionalPage(lastPage)) {
+            true -> pageCount - 1
+            false -> pageCount
+        }
+    }
+
+    /**
+     * 取得原始文件的總頁數
+     */
+    fun getRawPageCount(isNativeRefresh: Boolean = false): Int {
+        return kmpdfDocumentController?.getDocumentPageCount(isNativeRefresh) ?: 0
+    }
+
+    /**
+     * 取得當前顯示頁面的頁碼
+     */
+    fun getCurrentPage(): Int {
+        return kmpdfDocumentController?.currentPageNum ?: 0
+    }
+
+    /**
+     * 是否套用額外頁面(非垂直閱覽模式則不套用)
+     */
+
+    private fun shouldApplyAdditionalPage(): Boolean {
+        return isAdditionalPageEnabled.invoke() && isVerticalContinuesViewMode()
+    }
+
+    /**
+     * 是否是垂直閱覽模式
+     */
+    private fun isVerticalContinuesViewMode(): Boolean {
+        return kmpdfDocumentController?.pdfViewMode == Config.PDFViewMode.VERTICAL_SINGLE_PAGE_CONTINUES
+    }
+}

+ 74 - 0
src/main/java/com/kdanmobile/reader/additionalpage/AdditionalPageDisplayStrategy.kt

@@ -0,0 +1,74 @@
+package com.kdanmobile.reader.additionalpage
+
+import android.util.SparseBooleanArray
+import androidx.annotation.IntRange
+import kotlin.math.abs
+import kotlin.math.max
+
+/**
+ * 額外頁面顯示策略
+ * 額外頁面創建成功後,需符合當前顯示策略類型時才會顯示該額外頁面
+ */
+class AdditionalPageDisplayStrategy(
+        var displayStrategyType: AdditionalPageDisplayStrategyType = AdditionalPageDisplayStrategyType.PAGE_INTERVAL,
+        //  至少每隔幾頁(包含額外頁面)才可以顯示廣告
+        @IntRange(from = 4) pageInterval: Int = DEFAULT_PAGE_INTERVAL,
+        //  至少每隔幾秒才可以顯示廣告
+        @IntRange(from = 5) timeInterval: Int = DEFAULT_TIME_INTERVAL
+) {
+
+    companion object {
+        //  至少每隔幾頁(包含額外頁面)才可以顯示廣告,預設值4即表示每隔至少原始文件2頁才可以顯示廣告
+        const val DEFAULT_PAGE_INTERVAL = 4
+        //  至少每隔幾秒才可以顯示廣告
+        const val DEFAULT_TIME_INTERVAL = 5
+    }
+
+    private val pageInterval: Int = max(DEFAULT_PAGE_INTERVAL, pageInterval)
+
+    private val timeInterval: Int = max(DEFAULT_TIME_INTERVAL, timeInterval)
+
+    /**
+     * 額外頁面最後顯示時間,與「TIME_INTERVAL」相關的顯示策略會使用到此變數
+     */
+    private var lastDisplayTime = System.currentTimeMillis()
+
+    /**
+     * 判斷第position頁是否能顯示額外頁面
+     */
+    fun canDisplayAdditionalPageAt(additionalPageVisibleSet: SparseBooleanArray, position: Int): Boolean {
+        return when (displayStrategyType) {
+            AdditionalPageDisplayStrategyType.HIDE -> false
+            AdditionalPageDisplayStrategyType.PAGE_INTERVAL -> isPageStrategyValid(additionalPageVisibleSet, position)
+            AdditionalPageDisplayStrategyType.TIME_INTERVAL -> isTimeStrategyValid()
+            AdditionalPageDisplayStrategyType.PAGE_OR_TIME_INTERVAL -> isPageStrategyValid(additionalPageVisibleSet, position) || isTimeStrategyValid()
+            AdditionalPageDisplayStrategyType.PAGE_AND_TIME_INTERVAL -> isPageStrategyValid(additionalPageVisibleSet, position) && isTimeStrategyValid()
+        }
+    }
+
+    /**
+     * 判斷是否符合「PAGE_INTERVAL」顯示策略
+     */
+    private fun isPageStrategyValid(additionalPageVisibleSet: SparseBooleanArray, pageIndex: Int): Boolean {
+        for (index in 0 until additionalPageVisibleSet.size()) {
+            if (abs(pageIndex - additionalPageVisibleSet.keyAt(index)) < pageInterval) {
+                return false
+            }
+        }
+        return true
+    }
+
+    /**
+     * 判斷是否符合「TIME_INTERVAL」顯示策略
+     */
+    private fun isTimeStrategyValid(): Boolean {
+        return System.currentTimeMillis() - lastDisplayTime >= timeInterval * 1000L
+    }
+
+    /**
+     * 更新最後顯示時間
+     */
+    fun updateDisplayTime() {
+        lastDisplayTime = System.currentTimeMillis()
+    }
+}

+ 18 - 0
src/main/java/com/kdanmobile/reader/additionalpage/AdditionalPageDisplayStrategyType.kt

@@ -0,0 +1,18 @@
+package com.kdanmobile.reader.additionalpage
+
+/**
+ * 額外頁面顯示策略類型
+ * 額外頁面創建成功後,需符合當前顯示策略類型時才會顯示該額外頁面
+ */
+enum class AdditionalPageDisplayStrategyType {
+    //  不顯示額外頁面,行為表現與未使用客製化的ReaderView前完全相同
+    HIDE,
+    //  每隔至少幾頁顯示一個額外頁面
+    PAGE_INTERVAL,
+    //  每隔至少幾秒顯示一個額外頁面
+    TIME_INTERVAL,
+    //  每隔幾頁或每隔幾秒顯示一個額外頁面
+    PAGE_OR_TIME_INTERVAL,
+    //  每隔幾頁且每隔幾秒顯示一個額外頁面
+    PAGE_AND_TIME_INTERVAL
+}

+ 130 - 0
src/main/java/com/kdanmobile/reader/additionalpage/AdditionalPageManager.kt

@@ -0,0 +1,130 @@
+package com.kdanmobile.reader.additionalpage
+
+import android.util.SparseBooleanArray
+import androidx.annotation.IntRange
+import kotlin.math.abs
+import kotlin.math.max
+
+/**
+ * 額外頁面管理員
+ * 判斷哪些頁面是額外頁面、是否該顯示額外頁面
+ *
+ * <名詞解釋>
+ *     額外頁面:在原始文件之間插入的空頁面,之後可在其上插入其他內容(例如廣告)
+ */
+class AdditionalPageManager(
+        val pageConverter: AdditionalPageConverter = AdditionalPageConverter(),
+        val displayStrategy: AdditionalPageDisplayStrategy = AdditionalPageDisplayStrategy(),
+        @IntRange(from = 3) nextInterval: Int = DEFAULT_NEXT_INTERVAL
+) {
+
+    companion object {
+        //  當前頁面改變後,前後第N頁該顯示額外頁面
+        const val DEFAULT_NEXT_INTERVAL = 3
+    }
+
+    private val nextInterval: Int = max(DEFAULT_NEXT_INTERVAL, nextInterval)
+
+    //  額外頁面顯示策略類型
+    var displayStrategyType: AdditionalPageDisplayStrategyType
+        get() {
+            return displayStrategy.displayStrategyType
+        }
+        set(value) {
+            displayStrategy.displayStrategyType = value
+        }
+
+    /**
+     * 記錄額外頁面頁碼與其是否可見
+     */
+    private val additionalPageVisibleSet = SparseBooleanArray()
+
+    /**
+     * 記錄哪些額外頁面是隱藏不可見
+     */
+    private val hiddenAdditionalPageSet = HashSet<Int>()
+
+    /**
+     * 請求額外頁面
+     */
+    var requestAdditionalPage: (position: Int) -> Unit = {}
+
+    /**
+     * 判斷額外頁面是否讀取完畢
+     */
+    var isAdditionalPageLoaded: (position: Int) -> Boolean = {
+        false
+    }
+
+    init {
+        pageConverter.isAdditionalPageEnabled = {
+            displayStrategyType != AdditionalPageDisplayStrategyType.HIDE
+        }
+    }
+
+    /**
+     * 判斷第position頁是否是額外頁面,並且該額外頁面是否可見
+     */
+    fun isAdditionalPageVisible(position: Int): Boolean {
+        return pageConverter.isAdditionalPage(position) && additionalPageVisibleSet[position]
+    }
+
+    /**
+     * 判斷第position頁是否可顯示額外頁面
+     * 若是滿足以下任意條件則不可顯示額外頁面:
+     * .該頁不是額外頁面
+     * .該頁在可視範圍內(與當前頁差距小於nextInterval頁)
+     * .頁碼超過文件範圍
+     */
+    private fun canDisplayAdditionalPageAt(position: Int): Boolean {
+        return when {
+            !pageConverter.isAdditionalPage(position) -> false
+            abs(position - pageConverter.getCurrentPage()) < nextInterval -> false
+            position < 0 || position >= pageConverter.getPageCount() -> false
+            else -> true
+        }
+    }
+
+    /**
+     * 當前頁面更新時呼叫此方法
+     * 判斷哪幾頁應請求/顯示額外頁面
+     */
+    fun onPageChanged(position: Int) {
+        //  若不該顯示額外頁面則停止執行
+        if (!pageConverter.isAdditionalPageEnabled.invoke()) return
+
+        for (dir in -1 .. 1 step 2) {
+            //  判斷position的前後第nextInterval頁
+            val targetPosition = position + nextInterval * dir
+            //  是否可顯示額外頁面
+            val isValid = displayStrategy.canDisplayAdditionalPageAt(additionalPageVisibleSet, targetPosition)
+            val isHidden = hiddenAdditionalPageSet.contains(targetPosition)
+            if (isValid && !isHidden) {
+                //  該額外頁面是否請求成功
+                if (isAdditionalPageLoaded.invoke(targetPosition)) {
+                    //  若該位置可顯示額外頁面
+                    if (canDisplayAdditionalPageAt(targetPosition)) {
+                        //  顯示額外頁面
+                        additionalPageVisibleSet.put(targetPosition, true)
+                        //  更新最後顯示時間
+                        displayStrategy.updateDisplayTime()
+                    }
+                } else {
+                    requestAdditionalPage.invoke(targetPosition)
+                }
+            }
+        }
+    }
+
+    /**
+     * 隱藏額外頁面
+     */
+    fun tryToHideAdditionalPageAt(position: Int): Boolean {
+        if (!pageConverter.isAdditionalPage(position)) return false
+        //  移除該額外頁面
+        additionalPageVisibleSet.put(position, false)
+        //  紀錄為隱藏狀態
+        hiddenAdditionalPageSet.add(position)
+        return true
+    }
+}

+ 84 - 0
src/main/java/com/kdanmobile/reader/additionalpage/AdditionalPageReaderView.kt

@@ -0,0 +1,84 @@
+package com.kdanmobile.reader.additionalpage
+
+import android.content.Context
+import android.graphics.PointF
+import android.view.MotionEvent
+import com.kdanmobile.kmpdfkit.pdfcommon.KMPDFReaderView
+
+/**
+ * 客製化KMPDFReaderView
+ *
+ * 用以處理額外頁面點擊事件與修改當前頁面判斷
+ */
+open class AdditionalPageReaderView(
+        context: Context,
+        private val pageConverter: AdditionalPageConverter
+) : KMPDFReaderView(context) {
+
+    /**
+     * 常數,拖曳容忍值
+     * 該值表示使用者累計拖曳未滿多少像素可視為未拖曳,避免裝置過於靈敏導致點擊困難
+     */
+    companion object {
+        private const val DRAG_DISTANCE_TOLERATION = 20f
+    }
+
+    //  點擊位置
+    private val touchPoint = PointF()
+    //  累計拖曳距離
+    private var dragDistance = 0f
+    //  是否剛點擊螢幕?
+    private var isJustTouch = false
+
+    override fun onScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
+        //  若使用者剛點擊螢幕,忽略該事件,避免畫面跳動
+        if (mMode == Mode.Viewing && isJustTouch) {
+            isJustTouch = false
+            return false
+        }
+        return super.onScroll(e1, e2, distanceX, distanceY)
+    }
+
+    override fun onInterceptTouchEvent(motionEvent: MotionEvent): Boolean {
+        //  僅閱覽模式需要判斷
+        val intercept = if (mMode == Mode.Viewing) {
+            //  按下螢幕
+            if (motionEvent.action == MotionEvent.ACTION_DOWN) {
+                //  使用者剛點擊螢幕
+                isJustTouch = true
+                //  設定點擊位置
+                touchPoint.set(motionEvent.x, motionEvent.y)
+                //  重設累計拖曳距離
+                dragDistance = 0f
+            }
+            //  計算累計拖曳距離
+            dragDistance += Math.abs(motionEvent.x - touchPoint.x) + Math.abs(motionEvent.y - touchPoint.y)
+            //  重設點擊位置
+            touchPoint.set(motionEvent.x, motionEvent.y)
+
+            //  遮蔽未超過容忍值之前的拖曳事件
+            dragDistance >= DRAG_DISTANCE_TOLERATION && motionEvent.action == MotionEvent.ACTION_MOVE
+        } else {
+            true
+        }
+        return intercept && super.onInterceptTouchEvent(motionEvent)
+    }
+
+    override fun getDisplayedViewIndex(): Int {
+        return pageConverter.convertToRawPageIndex(mCurrent)
+    }
+
+    override fun setDisplayedViewIndex(i: Int, isRecordLastJumpPageNum: Boolean) {
+        super.setDisplayedViewIndex(pageConverter.convertToPageIndex(i), isRecordLastJumpPageNum)
+    }
+
+    /**
+     * 覆寫原始的refresh方法
+     * 避免重整後當前頁面錯誤
+     */
+    override fun refresh(reflow: Boolean) {
+        val page = mCurrent
+        super.refresh(reflow)
+        super.setDisplayedViewIndex(page, false)
+    }
+}

+ 159 - 0
src/main/java/com/kdanmobile/reader/additionalpage/AdditionalPageView.kt

@@ -0,0 +1,159 @@
+package com.kdanmobile.reader.additionalpage
+
+import android.content.Context
+import android.graphics.Color
+import android.graphics.Point
+import android.graphics.PointF
+import android.graphics.RectF
+import android.view.View
+import android.widget.RelativeLayout
+import com.kdanmobile.kmpdfkit.manager.KMPDFFactory
+import com.kdanmobile.kmpdfkit.pdfcommon.FilePicker
+import com.kdanmobile.kmpdfkit.pdfcommon.KMPDFPageView
+import com.kdanmobile.reader.utils.DensityUtil
+
+/**
+ * 客製化KMPDFPageView
+ *
+ * 用以處理額外頁面顯示
+ */
+class AdditionalPageView(
+        context: Context,
+        filePickerSupport: FilePicker.FilePickerSupport,
+        kmpdfFactory: KMPDFFactory,
+        parentSize: Point,
+        //  此頁是否是額外頁面
+        private val isAdditionalPage: Boolean
+) : KMPDFPageView(context, filePickerSupport, kmpdfFactory, parentSize) {
+
+    companion object {
+        //  預設額外頁面高度
+        const val DEFAULT_ADDITIONAL_PAGE_HEIGHT = 450
+        //  預設額外頁面頁碼(建議為負值,避免書籤錯誤顯示)
+        const val ADDITIONAL_PAGE_NUMBER_ID = -99
+    }
+
+    //  額外頁面寬度(螢幕寬度)
+    val additionalPageWidth = DensityUtil.getScreenWidthPx(context)
+    //  額外頁面高度(執行期變化)
+    var additionalPageHeight = 0
+    //  不可見額外頁面的大小
+    private val invisibleAdditionalPageSize = PointF(additionalPageWidth.toFloat(), 1f)
+    //  不可見額外頁面的範圍
+    private val invisibleAdditionalPageRect = RectF(0f, 0f, invisibleAdditionalPageSize.x, invisibleAdditionalPageSize.y)
+    //  頁面的縮放值
+    private var viewScale = -1f
+
+    //  用來放置額外頁面的容器
+    val layout = RelativeLayout(context).also {
+        it.visibility = View.GONE
+        addView(it)
+    }
+
+    //  額外頁面是否可見
+    var isAdditionalPageShowing = false
+        set(value) {
+            if (!isAdditionalPage) return
+            if (field != value) {
+                field = value
+                val visibility = when (field) {
+                    true -> View.VISIBLE
+                    false -> View.INVISIBLE
+                }
+                layout.visibility = visibility
+                this.visibility = visibility
+                initPageSize = false
+            }
+        }
+    //  是否曾初始化
+    private var initPageSize = false
+
+    init {
+        //  如果是額外頁面
+        if (isAdditionalPage) {
+            //  設定頁碼
+            mPageNumber = ADDITIONAL_PAGE_NUMBER_ID
+            //  頁面大小為不可見
+            super.setPage(page, invisibleAdditionalPageSize, invisibleAdditionalPageRect)
+            //  隱藏所有內容
+            for (index in 0 until childCount) {
+                getChildAt(index).visibility = View.INVISIBLE
+            }
+            //  設定透明背景
+            setBackgroundColor(Color.TRANSPARENT)
+        }
+    }
+
+    /**
+     * 處理長按事件(顯示Context Menu)
+     */
+    override fun openLongClickBlankContextMenu(view: View?) {
+        //  如果是額外頁面,則遮蔽長按事件
+        if (isAdditionalPage) return
+        super.openLongClickBlankContextMenu(view)
+    }
+
+    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+        super.onLayout(changed, left, top, right, bottom)
+        //  如果不是額外頁面,則不執行以下指令
+        if (!isAdditionalPage) return
+        //  在某些情況下,頁碼會被重置為0,因此需要重新設定(SDK bug?)
+        mPageNumber = ADDITIONAL_PAGE_NUMBER_ID
+
+        //  如果額外頁面可見
+        if (isAdditionalPageShowing) {
+            //  頁面置中,大小固定為additionalPageWidth x additionalPageHeight
+            layout.layout(-x.toInt(), 0, (additionalPageWidth - x).toInt(), additionalPageHeight)
+            //  更新頁面高度
+            updateAdHeight()
+            //  更新內容大小
+            updateAdContentSize()
+        } else if (!initPageSize) {
+            super.setPage(page, invisibleAdditionalPageSize, invisibleAdditionalPageRect)
+            initPageSize = true
+        }
+    }
+
+    /**
+     * 取得頁面縮放值
+     */
+    private fun getPageViewScale(): Float {
+        return width / mSize.x.toFloat()
+    }
+
+    /**
+     * 更新頁面高度
+     */
+    private fun updateAdHeight() {
+        //  如果縮放未改變則不更新
+        val scale = getPageViewScale()
+        if (scale == viewScale) return
+        viewScale = scale
+        //  計算頁面大小
+        val modifyPageSize = PointF(additionalPageWidth.toFloat(), additionalPageHeight / viewScale)
+        val cropPageSize = RectF(0f, 0f, modifyPageSize.x, modifyPageSize.y)
+        //  設定頁面大小
+        setPage(page, modifyPageSize, cropPageSize)
+        //  隱藏額外頁面以外的所有內容
+        for (index in 0 until childCount) {
+            getChildAt(index).also {
+                if (it != layout) {
+                    it.visibility = View.INVISIBLE
+                }
+            }
+        }
+    }
+
+    /**
+     * 更新內容大小
+     */
+    private fun updateAdContentSize() {
+        for (i in 0 until layout.childCount) {
+            layout.getChildAt(i).apply {
+                if (left != 0 || top != 0 || right != additionalPageWidth || bottom != additionalPageHeight) {
+                    layout(0, 0, additionalPageWidth, additionalPageHeight)
+                }
+            }
+        }
+    }
+}

+ 3 - 1
src/main/java/com/kdanmobile/reader/koin/KoinModule.kt

@@ -9,6 +9,7 @@ import com.kdanmobile.reader.Config
 import com.kdanmobile.reader.ReaderModel
 import com.kdanmobile.reader.ReaderModelManager
 import com.kdanmobile.reader.ReaderViewModel
+import com.kdanmobile.reader.additionalpage.AdditionalPageManager
 import com.kdanmobile.reader.thumb.PdfThumbViewModel
 import org.koin.android.viewmodel.dsl.viewModel
 import org.koin.dsl.module
@@ -20,9 +21,10 @@ internal object KoinModule {
         viewModel { (applicationContext: Context, intent: Intent, copiedFileFolder: File) ->
             CopyFileViewModel(applicationContext, intent, copiedFileFolder)
         }
-        viewModel { (uri: Uri) ->
+        viewModel { (additionalPageManager: AdditionalPageManager, uri: Uri) ->
             ReaderViewModel(
                     get(),
+                    additionalPageManager,
                     uri,
                     Config.PDF_SDK_LICENSE,
                     Config.PDF_SDK_RSA_MSG