שלום! אנחנו שמחים שחזרתם לסדרה שלנו בנושא CameraX ו-Jetpack Compose. בפוסטים הקודמים הסברנו את היסודות של הגדרת תצוגה מקדימה של המצלמה והוספנו את הפונקציונליות של הקשה למיקוד.
🧱 חלק 1: יצירת תצוגה מקדימה בסיסית של המצלמה באמצעות ארטיפקט חדש של מצלמה. הסברנו איך לטפל בהרשאות ואיך לבצע שילוב בסיסי.
👆 חלק 2: שימוש במערכת מחוות ההלחנה, בגרפיקה ובקורוטינות להטמעה של הקשה ויזואלית למיקוד.
🔦 חלק 3 (הפוסט הזה): הסבר על שכבת-על של רכיבי ממשק משתמש של Compose על גבי התצוגה המקדימה של המצלמה, כדי לשפר את חוויית המשתמש.
📂 חלק 4: שימוש בממשקי API דינמיים ובמסגרת האנימציה של Compose כדי ליצור מעבר חלק בין מצב שולחן לבין מצב רגיל בטלפונים מתקפלים.
בפוסט הזה נתעמק בנושא מעניין יותר מבחינה ויזואלית – הטמעה של אפקט של אור זרקורים על גבי התצוגה המקדימה של המצלמה, באמצעות זיהוי פנים כבסיס לאפקט. למה, אתם שואלים? קשה לי לומר בוודאות. אבל זה נראה מגניב 🙂. וחשוב מכך, זה מדגים איך אפשר לתרגם בקלות קואורדינטות של חיישן לקואורדינטות של ממשק משתמש, וכך להשתמש בהן ב-Compose.
הפעלת זיהוי פנים
קודם נשנה את CameraPreviewViewModel כדי להפעיל זיהוי פנים. נשתמש ב-API Camera2Interop, שמאפשר לנו ליצור אינטראקציה עם Camera2 API הבסיסי מ-CameraX. כך אנחנו יכולים להשתמש בתכונות של המצלמה שלא נחשפות ישירות על ידי CameraX. עלינו לבצע את השינויים הבאים:
-
יוצרים StateFlow שמכיל את גבולות הפנים כרשימה של
Rect. -
מגדירים את אפשרות בקשת הצילום
STATISTICS_FACE_DETECT_MODEלערך FULL, שמפעיל את זיהוי הפנים. -
מגדירים
CaptureCallbackכדי לקבל את פרטי הפנים מתוצאת הצילום.
class CameraPreviewViewModel : ViewModel() { ... private val _sensorFaceRects = MutableStateFlow(listOf<Rect>()) val sensorFaceRects: StateFlow<List<Rect>> = _sensorFaceRects.asStateFlow() private val cameraPreviewUseCase = Preview.Builder() .apply { Camera2Interop.Extender(this) .setCaptureRequestOption( CaptureRequest.STATISTICS_FACE_DETECT_MODE, CaptureRequest.STATISTICS_FACE_DETECT_MODE_FULL ) .setSessionCaptureCallback(object : CameraCaptureSession.CaptureCallback() { override fun onCaptureCompleted( session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult ) { super.onCaptureCompleted(session, request, result) result.get(CaptureResult.STATISTICS_FACES) ?.map { face -> face.bounds.toComposeRect() } ?.toList() ?.let { faces -> _sensorFaceRects.update { faces } } } }) } .build().apply { ... }
בעקבות השינויים האלה, מודל התצוגה שלנו פולט עכשיו רשימה של אובייקטים מסוג Rect שמייצגים את התיבות התוחמות של הפנים שזוהו בקואורדינטות של החיישן.
תרגום קואורדינטות של חיישנים לקואורדינטות של ממשק משתמש
תיבות התוחמות של הפנים שזוהו ואוחסנו בקטע הקודם משתמשות בקואורדינטות במערכת הקואורדינטות של החיישן. כדי לצייר את תיבות התוחמות בממשק המשתמש שלנו, אנחנו צריכים להמיר את הקואורדינטות האלה כך שהן יהיו נכונות במערכת הקואורדינטות של Compose. אנחנו צריכים:
- המרת קואורדינטות החיישן לקואורדינטות של מאגר התצוגה המקדימה
- המרת קואורדינטות של מאגר התצוגה המקדימה לקואורדינטות של ממשק משתמש מבוסס Compose
השינויים האלה מתבצעים באמצעות מטריצות טרנספורמציה. לכל אחת מהטרנספורמציות יש מטריצה משלה:
-
ה-
SurfaceRequestשלנו מחזיק במופעTransformationInfo, שמכיל מטריצה שלsensorToBufferTranform. -
ל-
CameraXViewfinderשלנו משויךCoordinateTransformer. יכול להיות שאתם זוכרים שכבר השתמשנו בטרנספורמציה הזו בפוסט הקודם בבלוג כדי להמיר קואורדינטות של הקשה למיקוד.
אנחנו יכולים ליצור שיטת עזר שתבצע את ההמרה בשבילנו:
private fun List<Rect>.transformToUiCoords( transformationInfo: SurfaceRequest.TransformationInfo?, uiToBufferCoordinateTransformer: MutableCoordinateTransformer ): List<Rect> = this.map { sensorRect -> val bufferToUiTransformMatrix = Matrix().apply { setFrom(uiToBufferCoordinateTransformer.transformMatrix) invert() } val sensorToBufferTransformMatrix = Matrix().apply { transformationInfo?.let { setFrom(it.sensorToBufferTransform) } } val bufferRect = sensorToBufferTransformMatrix.map(sensorRect) val uiRect = bufferToUiTransformMatrix.map(bufferRect) uiRect }
- אנחנו חוזרים על הפעולה הזו לגבי כל הפנים שזוהו ברשימה.
-
ה-
CoordinateTransformer.transformMatrixשאנחנו מקבלים מה-CameraXViewfinderשלנו משנה כברירת מחדל את הקואורדינטות מממשק המשתמש לקואורדינטות של מאגר הנתונים הזמני. במקרה שלנו, אנחנו רוצים שהמטריצה תפעל בכיוון ההפוך, ותמיר קואורדינטות של מאגר לקואורדינטות של ממשק משתמש. לכן אנחנו משתמשים בשיטהinvert()כדי להפוך את המטריצה. -
קודם אנחנו משנים את הקואורדינטות של הפנים מהחיישן לקואורדינטות של המאגר באמצעות
sensorToBufferTransformMatrix, ואז משנים את הקואורדינטות של המאגר לקואורדינטות של ממשק המשתמש באמצעותbufferToUiTransformMatrix.
הטמעה של אפקט ההתמקדות במישהו אחד
עכשיו נעדכן את רכיב ה-CameraPreviewContent composable כדי לצייר את אפקט ההדגשה. נשתמש ב-Canvas composable כדי לצייר מסכת מעבר צבעים על התצוגה המקדימה, וכך להפוך את הפנים המזוהות לגלויות:
@Composable fun CameraPreviewContent( viewModel: CameraPreviewViewModel, modifier: Modifier = Modifier, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current ) { val surfaceRequest by viewModel.surfaceRequest.collectAsStateWithLifecycle() val sensorFaceRects by viewModel.sensorFaceRects.collectAsStateWithLifecycle() val transformationInfo by produceState<SurfaceRequest.TransformationInfo?>(null, surfaceRequest) { try { surfaceRequest?.setTransformationInfoListener(Runnable::run) { transformationInfo -> value = transformationInfo } awaitCancellation() } finally { surfaceRequest?.clearTransformationInfoListener() } } val shouldSpotlightFaces by remember { derivedStateOf { sensorFaceRects.isNotEmpty() && transformationInfo != null} } val spotlightColor = Color(0xDDE60991) .. surfaceRequest?.let { request -> val coordinateTransformer = remember { MutableCoordinateTransformer() } CameraXViewfinder( surfaceRequest = request, coordinateTransformer = coordinateTransformer, modifier = .. ) AnimatedVisibility(shouldSpotlightFaces, enter = fadeIn(), exit = fadeOut()) { Canvas(Modifier.fillMaxSize()) { val uiFaceRects = sensorFaceRects.transformToUiCoords( transformationInfo = transformationInfo, uiToBufferCoordinateTransformer = coordinateTransformer ) // Fill the whole space with the color drawRect(spotlightColor) // Then extract each face and make it transparent uiFaceRects.forEach { faceRect -> drawRect( Brush.radialGradient( 0.4f to Color.Black, 1f to Color.Transparent, center = faceRect.center, radius = faceRect.minDimension * 2f, ), blendMode = BlendMode.DstOut ) } } } } }
ככה זה עובד:
- אנחנו אוספים את רשימת הפנים ממודל התצוגה.
-
כדי שלא נצטרך להרכיב מחדש את כל המסך בכל פעם שרשימת הפנים שזוהו משתנה, אנחנו משתמשים ב-
derivedStateOfכדי לעקוב אחרי זיהוי של פנים כלשהן. אחר כך אפשר להשתמש בו עםAnimatedVisibilityכדי להוסיף אנימציה לשכבת העל הצבעונית בכניסה וביציאה. -
ה-
surfaceRequestמכיל את המידע שדרוש לנו כדי להמיר את קואורדינטות החיישן לקואורדינטות של מאגר הנתונים הזמני ב-SurfaceRequest.TransformationInfo. אנחנו משתמשים בפונקציהproduceStateכדי להגדיר מאזין בבקשת המשטח, ומנקים את המאזין הזה כשהקומפוזיציה יוצאת מעץ הקומפוזיציה. -
אנחנו משתמשים ב-
Canvasכדי לצייר מלבן ורוד שקוף שמכסה את כל המסך. -
אנחנו דוחים את הקריאה של המשתנה
sensorFaceRectsעד שאנחנו בתוך בלוק הציורCanvas. לאחר מכן אנחנו משנים את הקואורדינטות לקואורדינטות של ממשק המשתמש. - אנחנו מבצעים איטרציה על הפנים שזוהו, ולכל פנים אנחנו מציירים מעבר צבע רדיאלי שיהפוך את החלק הפנימי של מלבן הפנים לשקוף.
-
אנחנו משתמשים ב-
BlendMode.DstOutכדי לוודא שאנחנו חותכים את הגרדיאנט מהמלבן הוורוד, וכך יוצרים את אפקט הזרקור.
הערה: כשמשנים את המצלמה ל DEFAULT_FRONT_CAMERA, תראו שההדגשה הפוכה! זו בעיה מוכרת, ואנחנו עוקבים אחריה ב Issue Tracker של Google.
תוצאה
בעזרת הקוד הזה, יש לנו אפקט של זרקור שפועל במלואו ומדגיש את הפנים שמזוהות. קטע הקוד המלא זמין כאן.
האפקט הזה הוא רק ההתחלה – באמצעות היכולות של Compose, אפשר ליצור מגוון רחב של חוויות מצלמה מדהימות מבחינה ויזואלית. היכולת להמיר קואורדינטות של חיישנים ושל מאגרים לקואורדינטות של ממשק המשתמש של Compose ובחזרה מאפשרת לנו להשתמש בכל התכונות של ממשק המשתמש של Compose ולשלב אותן בצורה חלקה עם מערכת המצלמה הבסיסית. אנימציות, גרפיקה מתקדמת בממשק המשתמש, ניהול פשוט של מצב ממשק המשתמש ושליטה מלאה במחוות – אין גבול לדמיון!
בפוסט האחרון בסדרה נסביר איך להשתמש בממשקי API אדפטיביים ובמסגרת האנימציה של פיתוח נייטיב כדי ליצור מעברים חלקים בין ממשקי משתמש שונים של מצלמות במכשירים מתקפלים. עדכונים נוספים בקרוב!
קטעי הקוד בבלוג הזה כפופים לרישיון הבא:
// Copyright 2024 Google LLC. SPDX-License-Identifier: Apache-2.0
תודה רבה לניק באצ'ר, אלכס ואניו, טרוור מקגווייר, דון טרנר ולורן וורד על הבדיקה ועל המשוב. האפליקציה הזו נוצרה הודות לעבודה הקשה של Yasith Vidanaarachch.
להמשך הקריאה
-
מדריכים
במאמר הזה נסביר איך להשתמש ב-API של הבדיקה waitUntil ב-Compose כדי להמתין עד שתנאים מסוימים יתקיימו.
Jose Alcérreca • משך הקריאה: 3 דקות
-
מדריכים
בין אם אתם משתמשים ב-Gemini ב-Android Studio, ב-Gemini CLI, ב-Antigravity או בסוכנים של צד שלישי כמו Claude Code או Codex, המטרה שלנו היא להבטיח שאפשר יהיה לפתח אפליקציות ל-Android באיכות גבוהה בכל מקום.
Adarsh Fernando, Esteban de la Canal • משך הקריאה: 4 דקות
-
מדריכים
מכיוון שהתרוקנות הסוללה המוגזמת היא נושא חשוב למשתמשי Android, Google נוקטת צעדים משמעותיים כדי לעזור למפתחים ליצור אפליקציות חסכוניות יותר בצריכת הסוללה.
Alice Yuan • משך הקריאה: 8 דקות
כדאי תמיד להיות בעניינים
רוצים לקבל טיפים עדכניים לפיתוח Android ישירות לאימייל כל שבוע?