חיתוך אובייקטים ב-Canvas

ה-codelab הזה הוא חלק מהקורס Advanced Android in Kotlin (פיתוח מתקדם ל-Android ב-Kotlin). כדי להפיק את המרב מהקורס הזה, מומלץ לעבוד על ה-codelabs לפי הסדר, אבל זה לא חובה. כל ה-codelab של הקורס מפורטים בדף הנחיתה של ה-codelab בנושא Android מתקדם ב-Kotlin.

מבוא

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

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

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

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

ב-codelab הזה תתנסו בדרכים שונות ליצירת קליפים.

מה שכדאי לדעת

חשוב שתכירו את:

  • איך יוצרים אפליקציה עם Activity ומריצים אותה באמצעות Android Studio.
  • איך יוצרים ציור ב-Canvas.
  • איך יוצרים View בהתאמה אישית ומבטלים את onDraw() וonSizeChanged().

מה תלמדו

  • איך חותכים אובייקטים כדי לצייר על Canvas.
  • איך שומרים ומשחזרים מצבי שרטוט של קנבס.
  • איך מחילים טרנספורמציות על אזור העריכה ועל טקסט.

הפעולות שתבצעו:

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

באפליקציה ClippingExample מוצג איך אפשר להשתמש בצורות ולשלב אותן כדי לציין אילו חלקים של אזור הציור יוצגו בתצוגה. האפליקציה הסופית תיראה כמו בצילום המסך שלמטה.

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

שלב 1: יוצרים את הפרויקט ClippingExample

  1. יוצרים פרויקט Kotlin בשם ClippingExample באמצעות התבנית Empty Activity. משתמשים ב-com.example.android כתוספת לשם החבילה.
  2. פתיחת MainActivity.kt.
  3. בשיטה onCreate(), מחליפים את תצוגת התוכן שמוגדרת כברירת מחדל ומגדירים את תצוגת התוכן למופע חדש של ClippedView. זו תהיה התצוגה המותאמת אישית שלכם לדוגמאות של הקליפים שתיצרו בהמשך.
setContentView(ClippedView(this))
  1. באותה רמה כמו MainActivity.kt, יוצרים קובץ וסיווג חדשים של Kotlin לתצוגה בהתאמה אישית בשם ClippedView, שמרחיב את View. נותנים לו את החתימה שמוצגת למטה. כל שאר העבודה תתבצע בתוך ClippedView. ההערה @JvmOverloads מורה לקומפיילר של Kotlin ליצור עומסים יתרים לפונקציה הזו, שמחליפים את ערכי ברירת המחדל של הפרמטרים.
class ClippedView @JvmOverloads constructor(
   context: Context,
   attrs: AttributeSet? = null,
   defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
}

שלב 2: מוסיפים מאפיינים ומשאבי מחרוזות

  1. מגדירים את המאפיינים שבהם ישתמשו בתצוגות החתוכות בקובץ משאבים חדש ב-res/values/dimens.xml. מידות ברירת המחדל האלה מוצפנות בקוד ומוגדרות כך שיתאימו למסך קטן יחסית.
<?xml version="1.0" encoding="utf-8"?>
<resources>
   <dimen name="clipRectRight">90dp</dimen>
   <dimen name="clipRectBottom">90dp</dimen>
   <dimen name="clipRectTop">0dp</dimen>
   <dimen name="clipRectLeft">0dp</dimen>

   <dimen name="rectInset">8dp</dimen>
   <dimen name="smallRectOffset">40dp</dimen>

   <dimen name="circleRadius">30dp</dimen>
   <dimen name="textOffset">20dp</dimen>
   <dimen name="strokeWidth">4dp</dimen>

   <dimen name="textSize">18sp</dimen>
</resources>

כדי שהאפליקציה תיראה טוב במסך גדול יותר (וכדי שיהיה קל יותר לראות את הפרטים), אפשר ליצור קובץ dimens עם ערכים גדולים יותר שחלים רק על מסכים גדולים יותר.

  1. ב-Android Studio, לוחצים לחיצה ימנית על התיקייה values ובוחרים באפשרות New > Values resource file (חדש > קובץ משאבים של ערכים).
  2. בתיבת הדו-שיח New Resource File (קובץ משאבים חדש), קוראים לקובץ dimens. בקטע Available qualifiers (מאפייני סיווג זמינים), בוחרים באפשרות Smallest Screen Width (רוחב המסך הקטן ביותר) ולוחצים על הלחצן >> כדי להוסיף אותה אל Chosen qualifiers (מאפייני סיווג שנבחרו). מזינים 480 בתיבה Smallest screen width (רוחב המסך הקטן ביותר) ולוחצים על OK (אישור).

  1. הקובץ אמור להופיע בתיקיית הערכים כמו שמוצג בהמשך.

  1. אם הקובץ לא מופיע, עוברים לתצוגה Project Files באפליקציה. הנתיב המלא של הקובץ החדש מוצג כך: ClippingExample/app/src/main/res/values-sw480dp/dimens.xml.

  1. מחליפים את תוכן ברירת המחדל של הקובץ values-sw480dp/dimens.xml במאפיינים שבהמשך.
<?xml version="1.0" encoding="utf-8"?>
<resources>
   <dimen name="clipRectRight">120dp</dimen>
   <dimen name="clipRectBottom">120dp</dimen>

   <dimen name="rectInset">10dp</dimen>
   <dimen name="smallRectOffset">50dp</dimen>

   <dimen name="circleRadius">40dp</dimen>
   <dimen name="textOffset">25dp</dimen>
   <dimen name="strokeWidth">6dp</dimen>
</resources>
  1. ב-strings.xml, מוסיפים את המחרוזות הבאות. הם ישמשו להצגת טקסט באזור העריכה.
<string name="clipping">Clipping</string>
<string name="translated">translated text</string>
<string name="skewed">"Skewed and "</string>

שלב 3: יצירה ואתחול של אובייקט Paint ואובייקט Path

  1. חוזרים לתצוגה Android של הפרויקט.
  2. ב-ClippedView מגדירים משתנה Paint לציור. מפעילים את ההחלקה של קצוות האלכסונים ומשתמשים ברוחב הקו ובגודל הטקסט שמוגדרים במאפיינים, כמו שמוצג בהמשך.
private val paint = Paint().apply {
   // Smooth out edges of what is drawn without affecting shape.
   isAntiAlias = true
   strokeWidth = resources.getDimension(R.dimen.strokeWidth)
   textSize = resources.getDimension(R.dimen.textSize)
}
  1. ב-ClippedView, יוצרים ומפעילים את Path כדי לאחסן באופן מקומי את הנתיב של מה ששורטט. ייבוא של android.graphics.Path
private val path = Path()

שלב 4: הגדרת הצורות

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

לכולם יש מכנה משותף:

  • מלבן גדול (או ריבוע) שמשמש כקונטיינר
  • קו אלכסוני לרוחב המלבן הגדול
  • מעגל
  • מחרוזת טקסט קצרה

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

  1. ב-ClippedView, מתחת ל-path, מוסיפים משתנים למאפיינים של מלבן חיתוך סביב כל קבוצת הצורות.
private val clipRectRight = resources.getDimension(R.dimen.clipRectRight)
private val clipRectBottom = resources.getDimension(R.dimen.clipRectBottom)
private val clipRectTop = resources.getDimension(R.dimen.clipRectTop)
private val clipRectLeft = resources.getDimension(R.dimen.clipRectLeft)
  1. הוספת משתנים למיקום של מלבן ולמיקום של מלבן קטן יותר.
private val rectInset = resources.getDimension(R.dimen.rectInset)
private val smallRectOffset = resources.getDimension(R.dimen.smallRectOffset)
  1. מוסיפים משתנה לרדיוס של עיגול. זהו רדיוס המעגל שמצויר בתוך המלבן.
private val circleRadius = resources.getDimension(R.dimen.circleRadius)
  1. מוסיפים היסט וגודל טקסט לטקסט שמוצג בתוך המלבן.
private val textOffset = resources.getDimension(R.dimen.textOffset)
private val textSize = resources.getDimension(R.dimen.textSize)

שלב 4: מגדירים את המיקומים של השורות והעמודות

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

  1. מגדירים את הקואורדינטות של שתי עמודות.
private val columnOne = rectInset
private val columnTwo = columnOne + rectInset + clipRectRight
  1. מוסיפים את הקואורדינטות לכל שורה, כולל השורה האחרונה של הטקסט שעבר טרנספורמציה.
private val rowOne = rectInset
private val rowTwo = rowOne + rectInset + clipRectBottom
private val rowThree = rowTwo + rectInset + clipRectBottom
private val rowFour = rowThree + rectInset + clipRectBottom
private val textRow = rowFour + (1.5f * clipRectBottom)
  1. מריצים את האפליקציה. האפליקציה אמורה להיפתח עם מסך לבן ריק מתחת לשם האפליקציה.

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

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

‫(1) קודם, מתרגמים את Canvas למיקום שבו רוצים לצייר את המלבן. כלומר, במקום לחשב איפה צריך לצייר את המלבן הבא ואת כל הצורות האחרות, אתם מזיזים את Canvas נקודת המוצא, כלומר את מערכת הקואורדינטות שלה.

‫(2) לאחר מכן, מציירים את המלבן בנקודת האפס החדשה של אזור הציור. כלומר, מציירים את הצורות באותו מיקום במערכת הצירים המתורגמת. השיטה הזו פשוטה יותר ויעילה יותר.

‫(3) לבסוף, משחזרים את Canvas ל-Origin המקורי.

זה האלגוריתם שתצטרכו להטמיע:

  1. ב-onDraw(), קוראים לפונקציה כדי למלא את Canvas בצבע הרקע האפור ולצייר את הצורות המקוריות.
  2. קוראים לפונקציה עבור כל מלבן חתוך והטקסט שרוצים לצייר.

לכל מלבן או טקסט:

  1. שמירת המצב הנוכחי של Canvas כדי שתוכלו לאפס אותו למצב ההתחלתי.
  2. מתרגמים את Origin של אזור הציור למיקום שבו רוצים לצייר.
  3. החלת צורות ונתיבים לחיתוך.
  4. משרטטים את המלבן או הטקסט.
  5. שחזור המצב של Canvas.

שלב: החלפת השיטה onDraw()

  1. מחליפים את onDraw() כמו שמוצג בקוד שלמטה. מפעילים פונקציה לכל צורה שמציירים, ואתם תטמיעו אותה בהמשך.
 override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        drawBackAndUnclippedRectangle(canvas)
        drawDifferenceClippingExample(canvas)
        drawCircularClippingExample(canvas)
        drawIntersectionClippingExample(canvas)
        drawCombinedClippingExample(canvas)
        drawRoundedRectangleClippingExample(canvas)
        drawOutsideClippingExample(canvas)
        drawSkewedTextExample(canvas)
        drawTranslatedTextExample(canvas)
        // drawQuickRejectExample(canvas)
    }
  1. יוצרים stub לכל אחת מפונקציות הציור כדי שהקוד ימשיך להתקמפל. אפשר להעתיק את הקוד שלמטה.
private fun drawBackAndUnclippedRectangle(canvas: Canvas){
}
private fun drawDifferenceClippingExample(canvas: Canvas){
}
private fun drawCircularClippingExample(canvas: Canvas){
}
private fun drawIntersectionClippingExample(canvas: Canvas){
}
private fun drawCombinedClippingExample(canvas: Canvas){
}
private fun drawRoundedRectangleClippingExample(canvas: Canvas){
}
private fun drawOutsideClippingExample(canvas: Canvas){
}
private fun drawTranslatedTextExample(canvas: Canvas){
}
private fun drawSkewedTextExample(canvas: Canvas){
}
private fun drawQuickRejectExample(canvas: Canvas){
}

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

שלב 1: יוצרים את השיטה drawClippedRectangle()‎

  1. יוצרים שיטה drawClippedRectangle() שמקבלת ארגומנט canvas מסוג Canvas.
private fun drawClippedRectangle(canvas: Canvas) {
}
  1. בתוך השיטה drawClippedRectangle(), מגדירים את הגבולות של מלבן החיתוך לכל הצורה. החלת מלבן חיתוך שמגביל את הציור רק לריבוע.
canvas.clipRect(
       clipRectLeft,clipRectTop,
       clipRectRight,clipRectBottom
)

השיטה Canvas.clipRect(...) מצמצמת את האזור במסך שאליו אפשר לכתוב פעולות ציור עתידיות. היא מגדירה את גבולות החיתוך כהצטלבות המרחבית של מלבן החיתוך הנוכחי והמלבן שמועבר אל clipRect(). יש הרבה וריאציות של השיטה clipRect() שמקבלות צורות שונות של אזורים ומאפשרות פעולות שונות על מלבן החיתוך.

  1. ממלאים את הצורה canvas בצבע לבן. כן! כל אזור הציור, כי אתם לא מציירים מלבנים, אתם גוזרים! בגלל מלבן החיתוך, רק האזור שמוגדר על ידי מלבן החיתוך ימולא, וייווצר מלבן לבן. שאר המשטח יישאר אפור.
canvas.drawColor(Color.WHITE)
  1. משנים את הצבע לאדום ומציירים קו אלכסוני בתוך מלבן החיתוך.
paint.color = Color.RED
canvas.drawLine(
   clipRectLeft,clipRectTop,
   clipRectRight,clipRectBottom,paint
)
  1. מגדירים את הצבע לירוק ומציירים עיגול בתוך מלבן הגזירה.
paint.color = Color.GREEN
canvas.drawCircle(
   circleRadius,clipRectBottom - circleRadius,
   circleRadius,paint
)
  1. מגדירים את הצבע לכחול ומציירים טקסט שמיושר לקצה הימני של מלבן החיתוך. משתמשים ב-canvas.drawText() כדי לצייר טקסט.
paint.color = Color.BLUE
// Align the RIGHT side of the text with the origin.
paint.textSize = textSize
paint.textAlign = Paint.Align.RIGHT
canvas.drawText(
   context.getString(R.string.clipping),
   clipRectRight,textOffset,paint
)

שלב 2: הטמעה של השיטה drawBackAndUnclippedRectangle()

  1. כדי לראות את הפעולה של השיטה drawClippedRectangle(), מציירים את המלבן הראשון שלא נחתך על ידי הטמעה של השיטה drawBackAndUnclippedRectangle() כמו שמוצג בהמשך. שומרים את canvas, מתרגמים למיקום השורה והעמודה הראשונים, מציירים על ידי קריאה ל-drawClippedRectangle(), ואז משחזרים את canvas למצב הקודם.
private fun drawBackAndUnclippedRectangle(canvas: Canvas){
   canvas.drawColor(Color.GRAY)
   canvas.save()
   canvas.translate(columnOne,rowOne)
   drawClippedRectangle(canvas)
   canvas.restore()
}
  1. מריצים את האפליקציה. אמור להופיע המלבן הלבן הראשון עם העיגול, הקו האדום והטקסט שלו על רקע אפור.

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

כל אחת מהשיטות האלה פועלת לפי אותו דפוס.

  1. שמירת המצב הנוכחי של אזור העריכה: canvas.save()

בהקשר של הפעילות נשמרת ערימה של מצבי ציור. מצבי שרטוט מורכבים ממטריצת הטרנספורמציה הנוכחית ומאזור החיתוך הנוכחי. אתם יכולים לשמור את המצב הנוכחי, לבצע פעולות שמשנות את מצב השרטוט (כמו תרגום או סיבוב של אזור הציור), ואז לשחזר את מצב השרטוט השמור. (הערה: זה דומה לפקודה stash ב-git).

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

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

  1. תרגום המקור של אזור הציור לקואורדינטות של השורה והעמודה: canvas.translate()

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

  1. מחילים טרנספורמציות על path, אם יש כאלה.
  2. החלת חיתוך: canvas.clipPath(path)
  3. מציירים את הצורות: drawClippedRectangle() or drawText()
  4. שחזור המצב הקודם של הקנבס: canvas.restore()

שלב 1: מטמיעים את הפונקציה drawDifferenceClippingExample(canvas)

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

אפשר להשתמש בקוד הבא, שמבצע את הפעולות הבאות:

  1. שומרים את הקנבס.
  2. תתורגם נקודת המוצא של אזור הציור למרחב פתוח בשורה הראשונה, בעמודה השנייה, משמאל למלבן הראשון.
  3. החלת שני מלבני חיתוך. האופרטור DIFFERENCE מחסר את המלבן השני מהמלבן הראשון.
  1. מפעילים את method‏ drawClippedRectangle() כדי לצייר את הקנבס ששונה.
  2. לשחזר את מצב הלוח.
private fun drawDifferenceClippingExample(canvas: Canvas) {
   canvas.save()
   // Move the origin to the right for the next rectangle.
   canvas.translate(columnTwo,rowOne)
   // Use the subtraction of two clipping rectangles to create a frame.
   canvas.clipRect(
       2 * rectInset,2 * rectInset,
       clipRectRight - 2 * rectInset,
       clipRectBottom - 2 * rectInset
   )
   // The method clipRect(float, float, float, float, Region.Op
   // .DIFFERENCE) was deprecated in API level 26. The recommended
   // alternative method is clipOutRect(float, float, float, float),
   // which is currently available in API level 26 and higher.
   if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O){
       canvas.clipRect(
           4 * rectInset,4 * rectInset,
           clipRectRight - 4 * rectInset,
           clipRectBottom - 4 * rectInset,
            Region.Op.DIFFERENCE
       )
   } else {
       canvas.clipOutRect(
           4 * rectInset,4 * rectInset,
           clipRectRight - 4 * rectInset,
           clipRectBottom - 4 * rectInset
       )
   }
   drawClippedRectangle(canvas)
   canvas.restore()
}
  1. מריצים את האפליקציה והיא אמורה להיראות כך.

שלב 2: מטמיעים את הפונקציה drawCircularClippingExample(canvas)

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

private fun drawCircularClippingExample(canvas: Canvas) {

   canvas.save()
   canvas.translate(columnOne, rowTwo)
   // Clears any lines and curves from the path but unlike reset(),
   // keeps the internal data structure for faster reuse.
   path.rewind()
   path.addCircle(
       circleRadius,clipRectBottom - circleRadius,
       circleRadius,Path.Direction.CCW
   )
   // The method clipPath(path, Region.Op.DIFFERENCE) was deprecated in
   // API level 26. The recommended alternative method is
   // clipOutPath(Path), which is currently available in
   // API level 26 and higher.
   if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
       canvas.clipPath(path, Region.Op.DIFFERENCE)
   } else {
       canvas.clipOutPath(path)
   }
   drawClippedRectangle(canvas)
   canvas.restore()
}

שלב 3: מטמיעים את הפונקציה drawIntersectionClippingExample(canvas)

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

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

private fun drawIntersectionClippingExample(canvas: Canvas) {
   canvas.save()
   canvas.translate(columnTwo,rowTwo)
   canvas.clipRect(
       clipRectLeft,clipRectTop,
       clipRectRight - smallRectOffset,
       clipRectBottom - smallRectOffset
   )
   // The method clipRect(float, float, float, float, Region.Op
   // .INTERSECT) was deprecated in API level 26. The recommended
   // alternative method is clipRect(float, float, float, float), which
   // is currently available in API level 26 and higher.
   if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
       canvas.clipRect(
           clipRectLeft + smallRectOffset,
           clipRectTop + smallRectOffset,
           clipRectRight,clipRectBottom,
           Region.Op.INTERSECT
       )
   } else {
       canvas.clipRect(
           clipRectLeft + smallRectOffset,
           clipRectTop + smallRectOffset,
           clipRectRight,clipRectBottom
       )
   }
   drawClippedRectangle(canvas)
   canvas.restore()
}

שלב 4: מטמיעים את הפונקציה drawCombinedClippingExample(canvas)

לאחר מכן, משלבים צורות, עיגול ומלבן, ומציירים נתיב כלשהו כדי להגדיר אזור חיתוך.

private fun drawCombinedClippingExample(canvas: Canvas) {
   canvas.save()
   canvas.translate(columnOne, rowThree)
   path.rewind()
   path.addCircle(
       clipRectLeft + rectInset + circleRadius,
       clipRectTop + circleRadius + rectInset,
       circleRadius,Path.Direction.CCW
   )
   path.addRect(
       clipRectRight / 2 - circleRadius,
       clipRectTop + circleRadius + rectInset,
       clipRectRight / 2 + circleRadius,
       clipRectBottom - rectInset,Path.Direction.CCW
   )
   canvas.clipPath(path)
   drawClippedRectangle(canvas)
   canvas.restore()
}

שלב 5: מטמיעים את הפונקציה drawRoundedRectangleClippingExample(canvas)

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

  1. ברמה העליונה, יוצרים משתנה של מלבן ומאתחלים אותו. ‫RectF היא מחלקה שמכילה קואורדינטות של מלבן בנקודה צפה.
private var rectF = RectF(
   rectInset,
   rectInset,
   clipRectRight - rectInset,
   clipRectBottom - rectInset
)
  1. מטמיעים את הפונקציה drawRoundedRectangleClippingExample(). הפונקציה addRoundRect() מקבלת מלבן, ערכים של רדיוס הפינה של ערכי x ו-y, ואת הכיוון של קו המתאר של המלבן המעוגל. המאפיין Path.Direction מציין את הכיוון של צורות סגורות (למשל מלבנים, אליפסות) כשמוסיפים אותן לנתיב. CCW מייצג סיבוב נגד כיוון השעון.
private fun drawRoundedRectangleClippingExample(canvas: Canvas) {
   canvas.save()
   canvas.translate(columnTwo,rowThree)
   path.rewind()
   path.addRoundRect(
       rectF,clipRectRight / 4,
       clipRectRight / 4, Path.Direction.CCW
   )
   canvas.clipPath(path)
   drawClippedRectangle(canvas)
   canvas.restore()
}

שלב 6: מטמיעים את הפונקציה drawOutsideClippingExample(canvas)

כדי לחתוך את החלק שמחוץ למלבן, מכפילים את השוליים הפנימיים של מלבן החיתוך.

private fun drawOutsideClippingExample(canvas: Canvas) {
   canvas.save()
   canvas.translate(columnOne,rowFour)
   canvas.clipRect(2 * rectInset,2 * rectInset,
       clipRectRight - 2 * rectInset,
       clipRectBottom - 2 * rectInset)
   drawClippedRectangle(canvas)
   canvas.restore()
}

שלב 7: מטמיעים את הפונקציה drawTranslatedTextExample(canvas)

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

  1. מטמיעים את הפונקציה שבהמשך.
private fun drawTranslatedTextExample(canvas: Canvas) {
   canvas.save()
   paint.color = Color.GREEN
   // Align the RIGHT side of the text with the origin.
   paint.textAlign = Paint.Align.LEFT
   // Apply transformation to canvas.
   canvas.translate(columnTwo,textRow)
   // Draw text.
   canvas.drawText(context.getString(R.string.translated),
       clipRectLeft,clipRectTop,paint)
   canvas.restore()
}
  1. מריצים את האפליקציה כדי לראות את הטקסט המתורגם.

שלב 8: מטמיעים את הפונקציה drawSkewedTextExample(canvas)

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

  1. יוצרים את הפונקציה שבהמשך ב-ClippedView.
private fun drawSkewedTextExample(canvas: Canvas) {
   canvas.save()
   paint.color = Color.YELLOW
   paint.textAlign = Paint.Align.RIGHT
   // Position text.
   canvas.translate(columnTwo, textRow)
   // Apply skew transformation.
   canvas.skew(0.2f, 0.3f)
   canvas.drawText(context.getString(R.string.skewed),
       clipRectLeft, clipRectTop, paint)
   canvas.restore()
}
  1. מריצים את האפליקציה כדי לראות את הטקסט המוטה שמוצג לפני הטקסט המתורגם.

השיטה quickReject() Canvas מאפשרת לבדוק אם מלבן או נתיב מסוימים נמצאים לגמרי מחוץ לאזורים שגלויים כרגע, אחרי שכל הטרנספורמציות הוחלו.

השיטה quickReject() שימושית מאוד כשיוצרים ציורים מורכבים יותר וצריך לעשות את זה כמה שיותר מהר. עם quickReject(), אתם יכולים להחליט ביעילות אילו אובייקטים לא צריך לצייר בכלל, ואין צורך לכתוב לוגיקה משלכם לגבי חיתוך.

  • השיטה quickReject() מחזירה true אם המלבן או הנתיב לא יהיו גלויים בכלל במסך. במקרה של חפיפה חלקית, עדיין צריך לבצע בדיקה עצמאית.
  • הערך של EdgeType הוא AA (Antialiased: Treat edges by rounding-out, because they may be antialiased) או BW (Black-White: Treat edges by just rounding to the nearest pixel boundary) לעיגול בלבד לפי הפיקסל הקרוב ביותר.

יש כמה גרסאות של quickReject(), ואפשר למצוא אותן גם במסמכי התיעוד.

boolean

quickReject(float left, float top, float right, float bottom, Canvas.EdgeType type)

boolean

quickReject(RectF rect, Canvas.EdgeType type)

boolean

quickReject(Path path, Canvas.EdgeType type)

בתרגיל הזה, תציירו בשורה חדשה, מתחת לטקסט ובתוך התג clipRect, כמו קודם.

  • קודם מתקשרים אל quickReject() עם מלבן inClipRectangle, שחופף ל-clipRect. לכן הפונקציה quickReject() מחזירה את הערך false, הצורה clipRect מלאה בצבע BLACK, והמלבן inClipRectangle מצויר.

  • לאחר מכן משנים את הקוד ומתקשרים אל quickReject(), עם notInClipRectangle. הפונקציה quickReject() מחזירה עכשיו את הערך true, המשתנה clipRect מתמלא בערך WHITE והמשתנה notInClipRectangle לא מצויר.

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

Step: Experiment with quickReject()

  1. ברמה העליונה, יוצרים משתנה לקואורדינטות y של שורה נוספת.
   private val rejectRow = rowFour + rectInset + 2*clipRectBottom
  1. מוסיפים את הפונקציה drawQuickRejectExample() הבאה אל ClippedView. קוראים את הקוד, כי הוא מכיל את כל מה שצריך לדעת כדי להשתמש ב-quickReject().
private fun drawQuickRejectExample(canvas: Canvas) {
   val inClipRectangle = RectF(clipRectRight / 2,
       clipRectBottom / 2,
       clipRectRight * 2,
       clipRectBottom * 2)

   val notInClipRectangle = RectF(RectF(clipRectRight+1,
       clipRectBottom+1,
       clipRectRight * 2,
       clipRectBottom * 2))

   canvas.save()
   canvas.translate(columnOne, rejectRow)
   canvas.clipRect(
       clipRectLeft,clipRectTop,
       clipRectRight,clipRectBottom
   )
   if (canvas.quickReject(
           inClipRectangle, Canvas.EdgeType.AA)) {
       canvas.drawColor(Color.WHITE)
   }
   else {
       canvas.drawColor(Color.BLACK)
       canvas.drawRect(inClipRectangle, paint
       )
   }
       canvas.restore()
}
  1. ב-onDraw(), מבטלים את ההערה של הקריאה ל-drawQuickRejectExample().
  2. מריצים את האפליקציה ורואים מלבן שחור, שהוא אזור החיתוך המלא, וחלקים מ-inClipRectangle, כי שני המלבנים חופפים, ולכן quickReject() מחזיר false ו-inClipRectangle מצויר.

  1. ב-drawQuickRejectExample(), משנים את הקוד כדי להפעיל את quickReject() מול notInClipRectangle.עכשיו quickReject() מחזיר true והאזור לחיתוך מתמלא בלבן.

מורידים את הקוד של ה-Codelab המוגמר.

$  git clone https://github.com/googlecodelabs/android-kotlin-drawing-clipping


אפשר גם להוריד את המאגר כקובץ ZIP, לבטל את הדחיסה שלו ולפתוח אותו ב-Android Studio.

הורדת קובץ Zip

  • ה-Context של פעילות שומר על מצב שמשמר טרנספורמציות ואזורי חיתוך של Canvas.
  • משתמשים במקשים canvas.save() ו-canvas.restore() כדי לצייר ולחזור למצב המקורי של אזור העריכה.
  • כדי לשרטט כמה צורות באזור הציור, אפשר לחשב את המיקום שלהן או להזיז (לתרגם) את נקודת המוצא של אזור הציור. האפשרות השנייה יכולה להקל על יצירת שיטות עזר לרצפים חוזרים של ציורים.
  • אזורי החיתוך יכולים להיות בכל צורה, שילוב של צורות או נתיב.
  • אתם יכולים להוסיף אזורי חיתוך, להחסיר מהם או ליצור חיתוך ביניהם כדי לקבל בדיוק את האזור שאתם צריכים.
  • אפשר להחיל טרנספורמציות על טקסט על ידי שינוי הקנבס.
  • השיטה quickReject() Canvas מאפשרת לבדוק אם מלבן או נתיב מסוימים נמצאים לגמרי מחוץ לאזורים שגלויים כרגע.

קורס ב-Udacity:

מסמכי תיעוד למפתחי Android:

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

בקטע הזה מפורטות אפשרויות למשימות ביתיות לתלמידים שעובדים על ה-Codelab הזה כחלק מקורס בהנחיית מדריך. המורה צריך:

  • אם צריך, מקצים שיעורי בית.
  • להסביר לתלמידים איך להגיש מטלות.
  • בודקים את שיעורי הבית.

אנשי ההוראה יכולים להשתמש בהצעות האלה כמה שרוצים, ומומלץ להם להקצות כל שיעורי בית אחרים שהם חושבים שמתאימים.

אם אתם עובדים על ה-codelab הזה לבד, אתם יכולים להשתמש במשימות האלה כדי לבדוק את הידע שלכם.

עונים על השאלות הבאות

שאלה 1

איזו שיטה מפעילים כדי להחריג צורות מציור בצורה יעילה?

excludeFromDrawing()

quickReject()

onDraw()

clipRect()

שאלה 2

Canvas.save() ו-Canvas.restore() שומרים ומשחזרים איזה מידע?

‫▢ צבע, רוחב קו וכו'.

‫▢ טרנספורמציות נוכחיות בלבד

‫▢ טרנספורמציות נוכחיות ואזור חיתוך

‫▢ רק אזור החיתוך הנוכחי

שאלה 3

Paint.Align מצוין:

▢ איך מיישרים את הצורות הבאות בציור

‫▢ באיזה צד של המקור הטקסט נלקח

‫▢ איפה באזור הגזירה הוא מיושר

‫▢ באיזה צד של הטקסט ליישר את המקור

קישורים למדריכי Codelab נוספים בקורס הזה זמינים בדף הנחיתה של מדריכי Codelab בנושא Android מתקדם ב-Kotlin.