שלום! אנחנו שמחים שחזרתם לסדרה שלנו בנושא 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 אדפטיביים ובמסגרת האנימציה של Compose כדי ליצור מעברים חלקים בין ממשקי משתמש שונים של מצלמות במכשירים מתקפלים. עדכונים נוספים בקרוב!
קטעי הקוד בבלוג הזה כפופים לרישיון הבא:
// Copyright 2024 Google LLC. SPDX-License-Identifier: Apache-2.0
תודה רבה לניק באצ'ר, אלכס ואניו, טרוור מקגווייר, דון טרנר ולורן וורד על הבדיקה ועל המשוב. האפשרות הזו נוצרה בזכות העבודה הקשה של Yasith Vidanaarachch.
להמשך הקריאה
-
מדריכים
במאמר הזה נסביר איך להשתמש ב-API של הבדיקה waitUntil ב-Compose כדי להמתין עד להשגת תנאים מסוימים.
Jose Alcérreca • משך הקריאה: 3 דקות
-
מדריכים
למרות שביצועי האפליקציה מושווים לעיתים קרובות לממשק משתמש חלק ולזמני הפעלה מהירים, הזיכרון משמש כבסיס שקט שעליו נבנים המדדים הגלויים האלה. זה לא סוד שזיכרון המכשיר חשוב יותר מאי פעם.
Alice Yuan, Ajesh Pai, Fung Lam • משך הקריאה: 10 דקות
-
מדריכים
אנחנו שמחים להודיע היום על אישור חדש של כתובת אימייל שמונפק על ידי Google, שמפתחים יכולים עכשיו לאחזר ישירות מ-Credential Manager Digital Credential API של Android.
Niharika Arora, Jean-Pierre Pralle • משך הקריאה: 3 דקות
כדאי תמיד להיות בעניינים
רוצים לקבל טיפים עדכניים לפיתוח Android ישירות לאימייל כל שבוע?