From 57f51110b531dc71a9039b4d910b620e3da50126 Mon Sep 17 00:00:00 2001 From: Rinaldy Adin <16521390@mahasiswa.itb.ac.id> Date: Fri, 5 Apr 2024 13:17:06 +0700 Subject: [PATCH] feat: transform preview correctly --- app/build.gradle.kts | 2 + .../example/abe/ui/twibbon/TwibbonFragment.kt | 135 ++++++++++++++---- app/src/main/res/layout/fragment_twibbon.xml | 53 ++++--- gradle/libs.versions.toml | 2 + 4 files changed, 139 insertions(+), 53 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cbf15de..5fc3d1e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -58,6 +58,7 @@ dependencies { implementation(libs.camera.lifecycle) implementation(libs.camera.view) implementation(libs.glide) + implementation(libs.androidx.exifinterface) annotationProcessor(libs.glideCompiler) implementation(libs.androidx.activity) implementation(libs.play.services.location) @@ -72,4 +73,5 @@ dependencies { androidTestImplementation(libs.androidx.espresso.core) implementation("com.github.PhilJay:MPAndroidChart:v3.1.0") implementation("org.jsoup:jsoup:1.14.3") + } \ No newline at end of file diff --git a/app/src/main/java/com/example/abe/ui/twibbon/TwibbonFragment.kt b/app/src/main/java/com/example/abe/ui/twibbon/TwibbonFragment.kt index 766cbb6..ffbf96d 100644 --- a/app/src/main/java/com/example/abe/ui/twibbon/TwibbonFragment.kt +++ b/app/src/main/java/com/example/abe/ui/twibbon/TwibbonFragment.kt @@ -1,7 +1,6 @@ package com.example.abe.ui.twibbon import android.Manifest -import android.R.attr import android.app.Dialog import android.content.pm.PackageManager import android.graphics.Bitmap @@ -11,7 +10,6 @@ import android.graphics.Canvas import android.graphics.Color import android.graphics.Matrix import android.graphics.drawable.ColorDrawable -import android.media.ExifInterface import android.net.Uri import android.os.Bundle import android.util.Log @@ -28,6 +26,7 @@ import androidx.camera.core.ImageCaptureException import androidx.camera.core.Preview import androidx.camera.lifecycle.ProcessCameraProvider import androidx.core.content.ContextCompat +import androidx.exifinterface.media.ExifInterface import androidx.fragment.app.Fragment import com.bumptech.glide.Glide import com.example.abe.R @@ -36,7 +35,6 @@ import java.io.ByteArrayOutputStream import java.io.File import java.io.FileNotFoundException import java.io.FileOutputStream -import java.io.InputStream import java.net.URI import java.text.SimpleDateFormat import java.util.Locale @@ -48,7 +46,6 @@ class TwibbonFragment : Fragment() { private val binding get() = _binding!! private var imageCapture: ImageCapture? = null - private lateinit var cameraExecutor: ExecutorService companion object { private const val TAG = "ABE-TWB" @@ -68,7 +65,6 @@ class TwibbonFragment : Fragment() { } binding.btnCaptureTwibbon.setOnClickListener { previewTwibbon() } - binding.ibtnCustomTwibbon.setOnClickListener { setCustomTwibbon() } return binding.root } @@ -92,9 +88,10 @@ class TwibbonFragment : Fragment() { cameraProvider.unbindAll() cameraProvider.bindToLifecycle( - this, cameraSelector, preview, imageCapture) + this, cameraSelector, preview, imageCapture + ) - } catch(exc: Exception) { + } catch (exc: Exception) { Log.e(TAG, "Use case binding failed", exc) } @@ -103,14 +100,17 @@ class TwibbonFragment : Fragment() { private val cameraPermissionsLauncher = registerForActivityResult( - ActivityResultContracts.RequestPermission()) + ActivityResultContracts.RequestPermission() + ) { isGranted -> if (isGranted) { startCamera() } else { - Toast.makeText(requireContext(), + Toast.makeText( + requireContext(), "Please allow camera to use Twibbon", - Toast.LENGTH_SHORT).show() + Toast.LENGTH_SHORT + ).show() } } @@ -120,7 +120,12 @@ class TwibbonFragment : Fragment() { val photoFile = File( requireContext().cacheDir, - "twibbon_${SimpleDateFormat(FILENAME_FORMAT, Locale.US).format(System.currentTimeMillis())}.jpg" + "twibbon_${ + SimpleDateFormat( + FILENAME_FORMAT, + Locale.US + ).format(System.currentTimeMillis()) + }.jpg" ) val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build() @@ -146,28 +151,100 @@ class TwibbonFragment : Fragment() { private fun overlayTwibbonToImage(imageUri: Uri) { try { Log.d(TAG, "Start overlaying twibbon") - val ims: InputStream = requireActivity().contentResolver.openInputStream(imageUri) ?: return - - val photoBitmap = BitmapFactory.decodeStream(ims) - val orientation = ExifInterface(ims).getAttributeInt(ExifInterface.TAG_ORIENTATION, 1) - val photoMatrix = Matrix() - if (orientation == 6) { - photoMatrix.postRotate(90f) - } else if (orientation == 3) { - photoMatrix.postRotate(180f) - } else if (orientation == 8) { - photoMatrix.postRotate(270f) + + val exifIms = requireActivity().contentResolver.openInputStream(imageUri) ?: return + val exif = ExifInterface(exifIms) + val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 1) + + val overlayBitmap = BitmapFactory.decodeResource( + requireContext().resources, + R.drawable.img_default_twibbon + ) + + val bitmapIms = requireActivity().contentResolver.openInputStream(imageUri) ?: return + val originalPhotoBitmap = BitmapFactory.decodeStream(bitmapIms) + val rotationMatrix = Matrix() + + when (orientation) { + 6 -> { + rotationMatrix.postRotate(90f) + } + + 3 -> { + rotationMatrix.postRotate(180f) + } + + 8 -> { + rotationMatrix.postRotate(270f) + } + } + + val rotatedBitmap = Bitmap.createBitmap( + originalPhotoBitmap, + 0, + 0, + originalPhotoBitmap.width, + originalPhotoBitmap.height, + rotationMatrix, + true + ) + + val flipMatrix = Matrix().apply { + postScale( + -1f, + 1f, + rotatedBitmap.width.toFloat() / 2, + rotatedBitmap.height.toFloat() + ) + } + val flippedBitmap = Bitmap.createBitmap( + rotatedBitmap, + 0, + 0, + rotatedBitmap.width, + rotatedBitmap.height, + flipMatrix, + true + ) + + val scaleMatrix = Matrix().apply { + val scale = overlayBitmap.width.toFloat() / flippedBitmap.width.toFloat() + postScale( + scale, + scale, + flippedBitmap.width.toFloat(), + flippedBitmap.height.toFloat() + ) } - val overlayBitmap = BitmapFactory.decodeResource(requireContext().resources, R.drawable.img_default_twibbon) + val photoBitmap = Bitmap.createBitmap( + flippedBitmap, + 0, + 0, + flippedBitmap.width, + flippedBitmap.height, + scaleMatrix, + true + ) + val translateMatrix = Matrix().apply { + postTranslate( + 0f, + (overlayBitmap.height.toFloat() - photoBitmap.height.toFloat()) / 2f + ) + } - val bmOverlay = Bitmap.createBitmap(overlayBitmap.height, overlayBitmap.width, photoBitmap.getConfig()) - val canvas = Canvas(bmOverlay) - canvas.drawBitmap(photoBitmap, photoMatrix, null) + val resultBitmap = + Bitmap.createBitmap( + overlayBitmap.width, + overlayBitmap.height, + overlayBitmap.getConfig() + ) + val canvas = Canvas(resultBitmap) + canvas.drawBitmap(photoBitmap, translateMatrix, null) canvas.drawBitmap(overlayBitmap, Matrix(), null) val bos = ByteArrayOutputStream() - bmOverlay.compress(CompressFormat.JPEG, 100 /*ignored for PNG*/, bos) + resultBitmap.compress(CompressFormat.JPEG, 100, bos) val bitmapData = bos.toByteArray() val f = File(URI(imageUri.toString())) @@ -223,8 +300,4 @@ class TwibbonFragment : Fragment() { private fun requestCameraPermissions() { cameraPermissionsLauncher.launch(Manifest.permission.CAMERA) } - - private fun setCustomTwibbon() { - TODO("Not yet implemented") - } } \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_twibbon.xml b/app/src/main/res/layout/fragment_twibbon.xml index 80f3b08..1d197f8 100644 --- a/app/src/main/res/layout/fragment_twibbon.xml +++ b/app/src/main/res/layout/fragment_twibbon.xml @@ -1,29 +1,53 @@ <?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".ui.twibbon.TwibbonFragment"> - <androidx.camera.view.PreviewView - android:id="@+id/prvTwibbon" + <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="0dp" + android:background="@color/grayLight" app:layout_constraintBottom_toTopOf="@+id/clPreviewBottonBar" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" - app:layout_constraintVertical_chainStyle="packed"> + app:layout_constraintTop_toTopOf="parent"> + + <androidx.camera.view.PreviewView + android:id="@+id/prvTwibbon" + android:layout_width="match_parent" + android:layout_height="0dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintDimensionRatio="1:1" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_chainStyle="packed" /> - </androidx.camera.view.PreviewView> + <ImageView + android:id="@+id/ivTwibbonOverlay" + android:layout_width="match_parent" + android:layout_height="0dp" + android:src="@drawable/img_default_twibbon" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintDimensionRatio="1:1" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_bias="0.5" /> + + </androidx.constraintlayout.widget.ConstraintLayout> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/clPreviewBottonBar" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="@drawable/bottom_bar_scan" + android:background="@color/primaryActive" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent"> @@ -41,21 +65,6 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> - <ImageButton - android:id="@+id/ibtnCustomTwibbon" - android:layout_width="52dp" - android:layout_height="52dp" - android:adjustViewBounds="true" - android:background="?attr/selectableItemBackgroundBorderless" - android:contentDescription="Get Photo from Gallery" - android:padding="10dp" - android:scaleType="fitCenter" - android:src="@drawable/ic_gallery" - app:layout_constraintBottom_toBottomOf="@+id/btnCaptureTwibbon" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toEndOf="@+id/btnCaptureTwibbon" - app:layout_constraintTop_toTopOf="@+id/btnCaptureTwibbon" /> - </androidx.constraintlayout.widget.ConstraintLayout> diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e5e09b1..df9a53d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -27,6 +27,7 @@ playServicesLocation = "21.2.0" camerax = "1.4.0-alpha04" glide = "4.12.0" okhttp = "4.9.0" +exifinterface = "1.3.7" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -59,6 +60,7 @@ glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" } glideCompiler = { module = "com.github.bumptech.glide:compiler", version.ref = "glide" } okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" } +androidx-exifinterface = { group = "androidx.exifinterface", name = "exifinterface", version.ref = "exifinterface" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } -- GitLab