הטמעה של כלי לצפייה ב-PDF

PdfViewerFragment הוא Fragment מיוחד שבו אפשר להשתמש כדי להציג מסמכי PDF באפליקציית Android. ‫PdfViewerFragment מאפשרת עיבוד פשוט של קובצי PDF, כך שתוכלו להתרכז בהיבטים אחרים של הפונקציונליות של האפליקציה.

תוצאות

מסמך PDF שעבר רינדור באפליקציית Android באמצעות PdfViewerFragment.
מסמך PDF שמוצג באפליקציה.

תאימות גרסאות

כדי להשתמש ב-PdfViewerFragment, האפליקציה צריכה לטרגט לפחות את Android S (רמת API ‏31) ואת רמת התוסף SDK ‏13. אם לא מתקיימות דרישות התאימות האלה, הספרייה מחזירה את השגיאה UnsupportedOperationException.

אפשר לבדוק את גרסת תוסף ה-SDK בזמן הריצה באמצעות מודול SdkExtensions. כך תוכלו לטעון את הפריט ואת מסמך ה-PDF באופן מותנה, רק אם המכשיר עומד בדרישות הנדרשות.

if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 13) {
    // Load the fragment and document.
}

תלויות

כדי לשלב את כלי ה-PDF באפליקציה, צריך להצהיר על התלות androidx.pdf בקובץ המודול build.gradle של האפליקציה. אפשר לגשת לספריית ה-PDF ממאגר Maven של Google.

dependencies {
    val pdfVersion = "1.0.0-alpha0X"
    implementation("androidx.pdf:pdf:pdf-viewer-fragment:$pdfVersion")
}

PdfViewerFragment תכונות

PdfViewerFragment מציג מסמכי PDF בפורמט מחולק לדפים, כך שקל לנווט בהם. כדי לטעון את הדף ביעילות, נעשה שימוש בטכניקת רינדור דו-שלבית שבה ממדי הדף נטענים בהדרגה.

כדי לבצע אופטימיזציה של השימוש בזיכרון, PdfViewerFragment מעבד רק את הדפים שמוצגים כרגע ומשחרר את מפות הביטים של הדפים שלא מוצגים במסך. בנוסף, PdfViewerFragment כולל כפתור פעולה צף (FAB) שתומך בהערות על ידי הפעלת intent מרומז android.intent.action.ANNOTATE שמכיל את ה-URI של המסמך.

הטמעה

הוספת תצוגת PDF לאפליקציית Android היא תהליך שכולל כמה שלבים.

יצירת פריסת הפעילות

מתחילים בהגדרת פריסת ה-XML של הפעילות שמארחת את מציג ה-PDF. פריסת הרכיבים צריכה לכלול את התג FrameLayout כדי להכיל את התג PdfViewerFragment ואת הלחצנים לאינטראקציות של המשתמש, כמו חיפוש בתוך המסמך.

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/pdf_container_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_gravity="center"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <FrameLayout
        android:id="@+id/fragment_container_view"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginBottom="8dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>

    <com.google.android.material.button.MaterialButton
        android:id="@+id/search_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/search_string"
        app:strokeWidth="1dp"
        android:layout_marginStart="8dp"
        android:layout_marginBottom="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

הגדרת הפעילות

הפעילות שמארחת את PdfViewerFragment צריכה להרחיב את AppCompatActivity. ב-method onCreate() של הפעילות, מגדירים את תצוגת התוכן לפריסה שיצרתם ומאתחלים את כל רכיבי ממשק המשתמש הנדרשים.

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val getContentButton: MaterialButton = findViewById(R.id.launch_button)
        val searchButton: MaterialButton = findViewById(R.id.search_button)
    }
}

אתחול PdfViewerFragment

יוצרים מופע של PdfViewerFragment באמצעות מנהל פרגמנטים שהתקבל מ-getSupportFragmentManager(). לפני שיוצרים מופע חדש של קטע, חשוב לבדוק אם כבר קיים מופע כזה, במיוחד כשמבצעים שינויים בהגדרות.

בדוגמה הבאה, הפונקציה initializePdfViewerFragment() מטפלת ביצירה ובאישור של העסקה המפוצלת. הפונקציה מחליפה קטע קיים בקונטיינר במופע של PdfViewerFragment.

class MainActivity : AppCompatActivity() {
    @RequiresExtension(extension = Build.VERSION_CODES.S, version = 13)
    private var pdfViewerFragment: PdfViewerFragment? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        // ...

        if (pdfViewerFragment == null) {
            pdfViewerFragment =
                supportFragmentManager
                    .findFragmentByTag(PDF_VIEWER_FRAGMENT_TAG) as PdfViewerFragment?
        }

    }

    // Used to instantiate and commit the fragment.
    @RequiresExtension(extension = Build.VERSION_CODES.S, version = 13)
    private fun initializePdfViewerFragment() {
        // This condition can be skipped if you want to create a new fragment every time.
        if (pdfViewerFragment == null) {
            val fragmentManager: FragmentManager = supportFragmentManager

          // Fragment initialization.
          pdfViewerFragment = PdfViewerFragmentExtended()
          val transaction: FragmentTransaction = fragmentManager.beginTransaction()

          // Replace an existing fragment in a container with an instance of a new fragment.
          transaction.replace(
              R.id.fragment,4_container_view,
              pdfViewerFragment!!,
              PDF_VIEWER_FRAGMENT_TAG
          )
          transaction.commitAllowingStateLoss()
          fragmentManager.executePendingTransactions()
        }
    }

    companion object {
        private const val MIME_TYPE_PDF = "application/pdf"
        private const val PDF_VIEWER_FRAGMENT_TAG = "pdf_viewer_fragment_tag"
    }
}

הרחבת הפונקציונליות של PdfViewerFragment

PdfViewerFragment חושף פונקציות ציבוריות שאפשר לבטל כדי להרחיב את היכולות שלו. יוצרים כיתה חדשה שמוגדרת כצאצא של PdfViewerFragment. במחלקת המשנה, מחליפים שיטות כמו onLoadDocumentSuccess() ו-onLoadDocumentError() כדי להוסיף לוגיקה מותאמת אישית, כמו רישום מדדים ביומן.

@RequiresExtension(extension = Build.VERSION_CODES.S, version = 13)
class PdfViewerFragmentExtended : PdfViewerFragment() {
          private val someLogger : SomeLogger = // ... used to log metrics

          override fun onLoadDocumentSuccess() {
                someLogger.log(/** log document success */)
          }

          override fun onLoadDocumentError(error: Throwable) {
                someLogger.log(/** log document error */, error)
          }
}

ל-PdfViewerFragment אין תפריט חיפוש מובנה, אבל יש בו סרגל חיפוש. אתם שולטים בנראות של סרגל החיפוש באמצעות isTextSearchActive API. כדי להפעיל חיפוש מסמכים, מגדירים את המאפיין isTextSearchActive של מופע PdfViewerFragment.

משתמשים ב-WindowCompat.setDecorFitsSystemWindows() כדי לוודא ש-WindowInsetsCompat מועבר בצורה נכונה לתצוגות התוכן, וזה הכרחי למיקום הנכון של תצוגת החיפוש.

class MainActivity : AppCompatActivity() {
    @RequiresExtension(extension = Build.VERSION_CODES.S, version = 13)
    override fun onCreate(savedInstanceState: Bundle?) {
        // ...
        searchButton.setOnClickListener {
            pdfViewerFragment?.isTextSearchActive =
                pdfViewerFragment?.isTextSearchActive == false
        }

        // Ensure WindowInsetsCompat are passed to content views without being
        // consumed by the decor view. These insets are used to calculate the
        // position of the search view.
        WindowCompat.setDecorFitsSystemWindows(window, false)
    }
}

שילוב עם הכלי לבחירת קבצים

כדי לאפשר למשתמשים לבחור קובצי PDF מהמכשיר שלהם, צריך לשלב את PdfViewerFragment עם הכלי לבחירת קבצים ב-Android. קודם צריך לעדכן את קובץ ה-XML של פריסת הפעילות כדי לכלול לחצן להפעלת הכלי לבחירת קבצים.

<...>
    <FrameLayout
        ...
        app:layout_constraintBottom_toTopOf="@+id/launch_button"/>
    // Adding a button to open file picker.
    <com.google.android.material.button.MaterialButton
        android:id="@+id/launch_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/launch_string"
        app:strokeWidth="1dp"
        android:layout_marginEnd="8dp"
        android:layout_marginBottom="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toStartOf="@id/search_button"/>

    <com.google.android.material.button.MaterialButton
        ...
        app:layout_constraintStart_toEndOf="@id/launch_button" />

</androidx.constraintlayout.widget.ConstraintLayout>

בשלב הבא, מפעילים את בוחר הקבצים בפעילות באמצעות registerForActivityResult(GetContent()). כשמשתמש בוחר קובץ, הקריאה החוזרת מספקת URI. לאחר מכן מגדירים את מאפיין documentUri של מופע PdfViewerFragment עם ה-URI הזה כדי לטעון ולהציג את קובץ ה-PDF שנבחר.

class MainActivity : AppCompatActivity() {
    // ...

    private var filePicker: ActivityResultLauncher<String> =
        registerForActivityResult(GetContent()) { uri: Uri? ->
            uri?.let {
                initializePdfViewerFragment()
                pdfViewerFragment?.documentUri = uri
            }
        }

    override fun onCreate(savedInstanceState: Bundle?) {
        // ...
        getContentButton.setOnClickListener { filePicker.launch(MIME_TYPE_PDF) }
    }

    private fun initializePdfViewerFragment() {
        // ...
    }

    companion object {
        private const val MIME_TYPE_PDF = "application/pdf"
        // ...
    }
}

התאמה אישית של ממשק המשתמש

אפשר להתאים אישית את ממשק המשתמש של PdfViewerFragment על ידי שינוי מאפייני XML שהספרייה חושפת. כך תוכלו להתאים את המראה של רכיבים כמו סרגל הגלילה ומחוון הדף לעיצוב של האפליקציה.

המאפיינים שניתנים להתאמה אישית כוללים:

  • fastScrollVerticalThumbDrawable — הגדרת הרכיב הגרפי של לחצן האחיזה של פס הגלילה.
  • fastScrollPageIndicatorBackgroundDrawable — הגדרת הרקע של אינדיקטור הדף.
  • fastScrollPageIndicatorMarginEnd — הגדרת השוליים הימניים של אינדיקטור הדף. צריך לוודא שערכי השוליים חיוביים.
  • fastScrollVerticalThumbMarginEnd — הגדרת השוליים הימניים של לחצן הגלילה האנכית. צריך לוודא שערכי השוליים חיוביים.

כדי להחיל את ההתאמות האישיות האלה, צריך להגדיר סגנון בהתאמה אישית במשאבי ה-XML.

<resources>
    <style name="pdfContainerStyle">
        <item name="fastScrollVerticalThumbDrawable">@drawable/custom_thumb_drawable</item>
        <item name="fastScrollPageIndicatorBackgroundDrawable">@drawable/custom_page_indicator_background</item>
        <item name="fastScrollVerticalThumbMarginEnd">8dp</item>
    </style>
</resources>

לאחר מכן, מספקים את משאב הסגנון המותאם אישית ל-PdfViewerFragment באמצעות PdfStylingOptions כשיוצרים מופע של הפראגמנט עם PdfViewerFragment.newInstance(stylingOptions).

private fun initializePdfViewerFragment() {
    // This condition can be skipped if you want to create a new fragment every time.
    if (pdfViewerFragment == null) {
      val fragmentManager: FragmentManager = supportFragmentManager

      // Create styling options.
      val stylingOptions = PdfStylingOptions(R.style.pdfContainerStyle)

      // Fragment initialization.
      pdfViewerFragment = PdfViewerFragment.newInstance(stylingOptions)

      // Execute fragment transaction.
    }
}

אם יצרתם מחלקת משנה של PdfViewerFragment, השתמשו בבונה המוגן כדי לספק את אפשרויות הסגנון. כך תוכלו לוודא שהסגנונות המותאמים אישית יוחלו בצורה נכונה על הקטע המורחב.

class StyledPdfViewerFragment: PdfViewerFragment {

    constructor() : super()

    private constructor(pdfStylingOptions: PdfStylingOptions) : super(pdfStylingOptions)

    companion object {
        fun newInstance(): StyledPdfViewerFragment {
            val stylingOptions = PdfStylingOptions(R.style.pdfContainerStyle)
            return StyledPdfViewerFragment(stylingOptions)
        }
    }
}

השלמת ההטמעה

הקוד הבא מספק דוגמה מלאה להטמעה של PdfViewerFragment בפעילות, כולל אתחול, שילוב של בוחר קבצים, פונקציונליות חיפוש והתאמה אישית של ממשק המשתמש.

class MainActivity : AppCompatActivity() {

    private var pdfViewerFragment: PdfViewerFragment? = null
    private var filePicker: ActivityResultLauncher<String> =
        registerForActivityResult(GetContent()) { uri: Uri? ->
            uri?.let {
                initializePdfViewerFragment()
                pdfViewerFragment?.documentUri = uri
            }
        }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        if (pdfViewerFragment == null) {
            pdfViewerFragment =
                supportFragmentManager
                   .findFragmentByTag(PDF_VIEWER_FRAGMENT_TAG) as PdfViewerFragment?
        }

        val getContentButton: MaterialButton = findViewById(R.id.launch_button)
        val searchButton: MaterialButton = findViewById(R.id.search_button)

        getContentButton.setOnClickListener { filePicker.launch(MIME_TYPE_PDF) }
        searchButton.setOnClickListener {
            pdfViewerFragment?.isTextSearchActive = pdfViewerFragment?.isTextSearchActive == false
        }
    }

    private fun initializePdfViewerFragment() {
        // This condition can be skipped if you want to create a new fragment every time.
        if (pdfViewerFragment == null) {
            val fragmentManager: FragmentManager = supportFragmentManager

          // Create styling options.
          // val stylingOptions = PdfStylingOptions(R.style.pdfContainerStyle)

          // Fragment initialization.
          // For customization:
          // pdfViewerFragment = PdfViewerFragment.newInstance(stylingOptions)
          pdfViewerFragment = PdfViewerFragmentExtended()
          val transaction: FragmentTransaction = fragmentManager.beginTransaction()

          // Replace an existing fragment in a container with an instance of a new fragment.
          transaction.replace(
              R.id.fragment_container_view,
              pdfViewerFragment!!,
              PDF_VIEWER_FRAGMENT_TAG
          )
          transaction.commitAllowingStateLoss()
          fragmentManager.executePendingTransactions()
        }
    }

    companion object {
        private const val MIME_TYPE_PDF = "application/pdf"
        private const val PDF_VIEWER_FRAGMENT_TAG = "pdf_viewer_fragment_tag"
    }
}

מידע חשוב על הקוד

  • מוודאים שהפרויקט עומד בדרישות המינימליות של רמת ה-API והרחבת ה-SDK.
  • הפעילות שמתארחת ב-PdfViewerFragment צריכה להימשך AppCompatActivity.
  • אפשר להרחיב את PdfViewerFragment כדי להוסיף התנהגויות מותאמות אישית.
  • אפשר להתאים אישית את ממשק המשתמש של PdfViewerFragment על ידי החלפת מאפייני XML.