ה-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
- יוצרים פרויקט Kotlin בשם
ClippingExampleבאמצעות התבנית Empty Activity. משתמשים ב-com.example.androidכתוספת לשם החבילה. - פתיחת
MainActivity.kt. - בשיטה
onCreate(), מחליפים את תצוגת התוכן שמוגדרת כברירת מחדל ומגדירים את תצוגת התוכן למופע חדש שלClippedView. זו תהיה התצוגה המותאמת אישית שלכם לדוגמאות של הקליפים שתיצרו בהמשך.
setContentView(ClippedView(this))- באותה רמה כמו
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: מוסיפים מאפיינים ומשאבי מחרוזות
- מגדירים את המאפיינים שבהם ישתמשו בתצוגות החתוכות בקובץ משאבים חדש ב-
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 עם ערכים גדולים יותר שחלים רק על מסכים גדולים יותר.
- ב-Android Studio, לוחצים לחיצה ימנית על התיקייה values ובוחרים באפשרות New > Values resource file (חדש > קובץ משאבים של ערכים).
- בתיבת הדו-שיח New Resource File (קובץ משאבים חדש), קוראים לקובץ
dimens. בקטע Available qualifiers (מאפייני סיווג זמינים), בוחרים באפשרות Smallest Screen Width (רוחב המסך הקטן ביותר) ולוחצים על הלחצן >> כדי להוסיף אותה אל Chosen qualifiers (מאפייני סיווג שנבחרו). מזינים 480 בתיבה Smallest screen width (רוחב המסך הקטן ביותר) ולוחצים על OK (אישור).

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

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

- מחליפים את תוכן ברירת המחדל של הקובץ
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>- ב-
strings.xml, מוסיפים את המחרוזות הבאות. הם ישמשו להצגת טקסט באזור העריכה.
<string name="clipping">Clipping</string>
<string name="translated">translated text</string>
<string name="skewed">"Skewed and "</string>שלב 3: יצירה ואתחול של אובייקט Paint ואובייקט Path
- חוזרים לתצוגה Android של הפרויקט.
- ב-
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)
}- ב-
ClippedView, יוצרים ומפעילים אתPathכדי לאחסן באופן מקומי את הנתיב של מה ששורטט. ייבוא שלandroid.graphics.Path
private val path = Path()שלב 4: הגדרת הצורות
באפליקציה הזו מוצגות כמה שורות ושתי עמודות של צורות שנחתכו בדרכים שונות.
לכולם יש מכנה משותף:
- מלבן גדול (או ריבוע) שמשמש כקונטיינר
- קו אלכסוני לרוחב המלבן הגדול
- מעגל
- מחרוזת טקסט קצרה

בשלב הזה מגדירים מאפיינים לצורות האלה מתוך משאבים, כך שצריך לקבל את המאפיינים רק פעם אחת כשמשתמשים בהם בהמשך.
- ב-
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)- הוספת משתנים למיקום של מלבן ולמיקום של מלבן קטן יותר.
private val rectInset = resources.getDimension(R.dimen.rectInset)
private val smallRectOffset = resources.getDimension(R.dimen.smallRectOffset)- מוסיפים משתנה לרדיוס של עיגול. זהו רדיוס המעגל שמצויר בתוך המלבן.
private val circleRadius = resources.getDimension(R.dimen.circleRadius)- מוסיפים היסט וגודל טקסט לטקסט שמוצג בתוך המלבן.
private val textOffset = resources.getDimension(R.dimen.textOffset)
private val textSize = resources.getDimension(R.dimen.textSize)שלב 4: מגדירים את המיקומים של השורות והעמודות
הצורות של האפליקציה הזו מוצגות בשתי עמודות וארבע שורות, בהתאם לערכים של המאפיינים שהוגדרו למעלה. החישובים המתמטיים לא מוסברים ב-Codelab הזה, אבל כדאי לעיין בהם כשמעתיקים את הקוד שמופיע בשלב הזה.
- מגדירים את הקואורדינטות של שתי עמודות.
private val columnOne = rectInset
private val columnTwo = columnOne + rectInset + clipRectRight- מוסיפים את הקואורדינטות לכל שורה, כולל השורה האחרונה של הטקסט שעבר טרנספורמציה.
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)- מריצים את האפליקציה. האפליקציה אמורה להיפתח עם מסך לבן ריק מתחת לשם האפליקציה.

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

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

(1) קודם, מתרגמים את Canvas למיקום שבו רוצים לצייר את המלבן. כלומר, במקום לחשב איפה צריך לצייר את המלבן הבא ואת כל הצורות האחרות, אתם מזיזים את Canvas נקודת המוצא, כלומר את מערכת הקואורדינטות שלה.
(2) לאחר מכן, מציירים את המלבן בנקודת האפס החדשה של אזור הציור. כלומר, מציירים את הצורות באותו מיקום במערכת הצירים המתורגמת. השיטה הזו פשוטה יותר ויעילה יותר.
(3) לבסוף, משחזרים את Canvas ל-Origin המקורי.
זה האלגוריתם שתצטרכו להטמיע:
- ב-
onDraw(), קוראים לפונקציה כדי למלא אתCanvasבצבע הרקע האפור ולצייר את הצורות המקוריות. - קוראים לפונקציה עבור כל מלבן חתוך והטקסט שרוצים לצייר.
לכל מלבן או טקסט:
- שמירת המצב הנוכחי של
Canvasכדי שתוכלו לאפס אותו למצב ההתחלתי. - מתרגמים את
Originשל אזור הציור למיקום שבו רוצים לצייר. - החלת צורות ונתיבים לחיתוך.
- משרטטים את המלבן או הטקסט.
- שחזור המצב של
Canvas.
שלב: החלפת השיטה onDraw()
- מחליפים את
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)
}- יוצרים 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()
- יוצרים שיטה
drawClippedRectangle()שמקבלת ארגומנטcanvasמסוגCanvas.
private fun drawClippedRectangle(canvas: Canvas) {
}- בתוך השיטה
drawClippedRectangle(), מגדירים את הגבולות של מלבן החיתוך לכל הצורה. החלת מלבן חיתוך שמגביל את הציור רק לריבוע.
canvas.clipRect(
clipRectLeft,clipRectTop,
clipRectRight,clipRectBottom
)השיטה Canvas.clipRect(...) מצמצמת את האזור במסך שאליו אפשר לכתוב פעולות ציור עתידיות. היא מגדירה את גבולות החיתוך כהצטלבות המרחבית של מלבן החיתוך הנוכחי והמלבן שמועבר אל clipRect(). יש הרבה וריאציות של השיטה clipRect() שמקבלות צורות שונות של אזורים ומאפשרות פעולות שונות על מלבן החיתוך.
- ממלאים את הצורה
canvasבצבע לבן. כן! כל אזור הציור, כי אתם לא מציירים מלבנים, אתם גוזרים! בגלל מלבן החיתוך, רק האזור שמוגדר על ידי מלבן החיתוך ימולא, וייווצר מלבן לבן. שאר המשטח יישאר אפור.
canvas.drawColor(Color.WHITE)- משנים את הצבע לאדום ומציירים קו אלכסוני בתוך מלבן החיתוך.
paint.color = Color.RED
canvas.drawLine(
clipRectLeft,clipRectTop,
clipRectRight,clipRectBottom,paint
)- מגדירים את הצבע לירוק ומציירים עיגול בתוך מלבן הגזירה.
paint.color = Color.GREEN
canvas.drawCircle(
circleRadius,clipRectBottom - circleRadius,
circleRadius,paint
)- מגדירים את הצבע לכחול ומציירים טקסט שמיושר לקצה הימני של מלבן החיתוך. משתמשים ב-
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()
- כדי לראות את הפעולה של השיטה
drawClippedRectangle(), מציירים את המלבן הראשון שלא נחתך על ידי הטמעה של השיטהdrawBackAndUnclippedRectangle()כמו שמוצג בהמשך. שומרים אתcanvas, מתרגמים למיקום השורה והעמודה הראשונים, מציירים על ידי קריאה ל-drawClippedRectangle(), ואז משחזרים אתcanvasלמצב הקודם.
private fun drawBackAndUnclippedRectangle(canvas: Canvas){
canvas.drawColor(Color.GRAY)
canvas.save()
canvas.translate(columnOne,rowOne)
drawClippedRectangle(canvas)
canvas.restore()
}- מריצים את האפליקציה. אמור להופיע המלבן הלבן הראשון עם העיגול, הקו האדום והטקסט שלו על רקע אפור.

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

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

אפשר להשתמש בקוד הבא, שמבצע את הפעולות הבאות:
- שומרים את הקנבס.
- תתורגם נקודת המוצא של אזור הציור למרחב פתוח בשורה הראשונה, בעמודה השנייה, משמאל למלבן הראשון.
- החלת שני מלבני חיתוך. האופרטור
DIFFERENCEמחסר את המלבן השני מהמלבן הראשון.
- מפעילים את method
drawClippedRectangle()כדי לצייר את הקנבס ששונה. - לשחזר את מצב הלוח.
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()
}- מריצים את האפליקציה והיא אמורה להיראות כך.

שלב 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)
לאחר מכן, מוסיפים מלבן עם פינות מעוגלות, שהוא צורת חיתוך נפוצה.

- ברמה העליונה, יוצרים משתנה של מלבן ומאתחלים אותו.
RectFהיא מחלקה שמכילה קואורדינטות של מלבן בנקודה צפה.
private var rectF = RectF(
rectInset,
rectInset,
clipRectRight - rectInset,
clipRectBottom - rectInset
)- מטמיעים את הפונקציה
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)
ציור טקסט לא שונה מציור צורות אחרות, ואפשר להחיל על הטקסט טרנספורמציות. לדוגמה, אפשר לתרגם טקסט על ידי תרגום של אזור העריכה וציור הטקסט.

- מטמיעים את הפונקציה שבהמשך.
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()
}- מריצים את האפליקציה כדי לראות את הטקסט המתורגם.

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

- יוצרים את הפונקציה שבהמשך ב-
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()
}- מריצים את האפליקציה כדי לראות את הטקסט המוטה שמוצג לפני הטקסט המתורגם.

השיטה 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(), ואפשר למצוא אותן גם במסמכי התיעוד.
| quickReject |
| quickReject |
| quickReject |
בתרגיל הזה, תציירו בשורה חדשה, מתחת לטקסט ובתוך התג clipRect, כמו קודם.
- קודם מתקשרים אל
quickReject()עם מלבןinClipRectangle, שחופף ל-clipRect. לכן הפונקציהquickReject()מחזירה את הערך false, הצורהclipRectמלאה בצבעBLACK, והמלבןinClipRectangleמצויר.

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

אם יש לכם ציורים מורכבים, תוכלו לדעת במהירות אילו צורות נמצאות לגמרי מחוץ לאזור החיתוך, ואילו צורות נמצאות באופן חלקי או מלא בתוך אזור החיתוך, ולכן תצטרכו לבצע חישובים נוספים ולצייר אותן.
Step: Experiment with quickReject()
- ברמה העליונה, יוצרים משתנה לקואורדינטות y של שורה נוספת.
private val rejectRow = rowFour + rectInset + 2*clipRectBottom- מוסיפים את הפונקציה
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()
}- ב-
onDraw(), מבטלים את ההערה של הקריאה ל-drawQuickRejectExample(). - מריצים את האפליקציה ורואים מלבן שחור, שהוא אזור החיתוך המלא, וחלקים מ-
inClipRectangle, כי שני המלבנים חופפים, ולכןquickReject()מחזירfalseו-inClipRectangleמצויר.

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

מורידים את הקוד של ה-Codelab המוגמר.
$ git clone https://github.com/googlecodelabs/android-kotlin-drawing-clipping
אפשר גם להוריד את המאגר כקובץ ZIP, לבטל את הדחיסה שלו ולפתוח אותו ב-Android Studio.
- ה-
Contextשל פעילות שומר על מצב שמשמר טרנספורמציות ואזורי חיתוך שלCanvas. - משתמשים במקשים
canvas.save()ו-canvas.restore()כדי לצייר ולחזור למצב המקורי של אזור העריכה. - כדי לשרטט כמה צורות באזור הציור, אפשר לחשב את המיקום שלהן או להזיז (לתרגם) את נקודת המוצא של אזור הציור. האפשרות השנייה יכולה להקל על יצירת שיטות עזר לרצפים חוזרים של ציורים.
- אזורי החיתוך יכולים להיות בכל צורה, שילוב של צורות או נתיב.
- אתם יכולים להוסיף אזורי חיתוך, להחסיר מהם או ליצור חיתוך ביניהם כדי לקבל בדיוק את האזור שאתם צריכים.
- אפשר להחיל טרנספורמציות על טקסט על ידי שינוי הקנבס.
- השיטה
quickReject()Canvasמאפשרת לבדוק אם מלבן או נתיב מסוימים נמצאים לגמרי מחוץ לאזורים שגלויים כרגע.
קורס ב-Udacity:
מסמכי תיעוד למפתחי Android:
- כיתה
Canvas - כיתה
Bitmap - כיתה
View - כיתה
Paint - הגדרות
Bitmap.config - אופרטורים של
Region.Op - כיתה
Path - כיתה
Canvas - כיתה
Bitmap - כיתה
View - כיתה
Paint - הגדרות
Bitmap.config - אופרטורים של
Region.Op - כיתה
Path android.graphicsכלים גרפיים- הגדרות
Bitmap.ConfigCanvas - Canvas ו-Drawables
- מה עושה canvas.translate()
- הסבר על הפונקציות save() ו-restore() בהקשר של Canvas
- clipping
- overdraw.
@JvmOverloads
אפשר גם לעיין בסדרת המאמרים בנושא ארכיטקטורת גרפיקה כדי לקבל הסבר מפורט על האופן שבו מסגרת Android מציירת על המסך.
בקטע הזה מפורטות אפשרויות למשימות ביתיות לתלמידים שעובדים על ה-Codelab הזה כחלק מקורס בהנחיית מדריך. המורה צריך:
- אם צריך, מקצים שיעורי בית.
- להסביר לתלמידים איך להגיש מטלות.
- בודקים את שיעורי הבית.
אנשי ההוראה יכולים להשתמש בהצעות האלה כמה שרוצים, ומומלץ להם להקצות כל שיעורי בית אחרים שהם חושבים שמתאימים.
אם אתם עובדים על ה-codelab הזה לבד, אתם יכולים להשתמש במשימות האלה כדי לבדוק את הידע שלכם.
עונים על השאלות הבאות
שאלה 1
איזו שיטה מפעילים כדי להחריג צורות מציור בצורה יעילה?
▢ excludeFromDrawing()
▢ quickReject()
▢ onDraw()
▢ clipRect()
שאלה 2
Canvas.save() ו-Canvas.restore() שומרים ומשחזרים איזה מידע?
▢ צבע, רוחב קו וכו'.
▢ טרנספורמציות נוכחיות בלבד
▢ טרנספורמציות נוכחיות ואזור חיתוך
▢ רק אזור החיתוך הנוכחי
שאלה 3
Paint.Align מצוין:
▢ איך מיישרים את הצורות הבאות בציור
▢ באיזה צד של המקור הטקסט נלקח
▢ איפה באזור הגזירה הוא מיושר
▢ באיזה צד של הטקסט ליישר את המקור
קישורים למדריכי Codelab נוספים בקורס הזה זמינים בדף הנחיתה של מדריכי Codelab בנושא Android מתקדם ב-Kotlin.