-
-
Notifications
You must be signed in to change notification settings - Fork 245
fix internal image zoom #3184
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
fix internal image zoom #3184
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,96 @@ | ||||||
| package protect.card_locker.cardview | ||||||
|
|
||||||
| import android.content.Context | ||||||
| import android.content.Intent | ||||||
| import android.graphics.Bitmap | ||||||
| import android.os.Bundle | ||||||
| import android.view.WindowManager | ||||||
| import android.widget.Toast | ||||||
| import androidx.activity.compose.setContent | ||||||
| import protect.card_locker.CatimaComponentActivity | ||||||
| import protect.card_locker.ImageLocationType | ||||||
| import protect.card_locker.R | ||||||
| import protect.card_locker.Utils | ||||||
| import protect.card_locker.compose.LoyaltyCardImageScreen | ||||||
| import protect.card_locker.compose.theme.CatimaTheme | ||||||
| import protect.card_locker.preferences.Settings | ||||||
|
|
||||||
| class LoyaltyCardImageActivity : CatimaComponentActivity() { | ||||||
| override fun onCreate(savedInstanceState: Bundle?) { | ||||||
| super.onCreate(savedInstanceState) | ||||||
|
|
||||||
| fixedEdgeToEdge() | ||||||
| applyWindowPreferences() | ||||||
|
|
||||||
| val imageLocationType = intent.imageLocationType() | ||||||
| val loyaltyCardId = intent.getIntExtra(BUNDLE_ID, 0) | ||||||
| val bitmap = imageLocationType?.let { | ||||||
| Utils.retrieveCardImage(this, loyaltyCardId, it) | ||||||
| } | ||||||
|
|
||||||
| if (loyaltyCardId == 0 || imageLocationType == null || bitmap == null) { | ||||||
| Toast.makeText(this, R.string.failedToRetrieveImageFile, Toast.LENGTH_SHORT).show() | ||||||
| finish() | ||||||
| return | ||||||
| } | ||||||
|
|
||||||
| title = getString(imageLocationType.descriptionRes()) | ||||||
|
|
||||||
| setContent { | ||||||
| CatimaTheme { | ||||||
| LoyaltyCardImageScreen( | ||||||
| bitmap = bitmap, | ||||||
| contentDescriptionRes = imageLocationType.descriptionRes(), | ||||||
| onBack = { onBackPressedDispatcher.onBackPressed() } | ||||||
| ) | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| private fun applyWindowPreferences() { | ||||||
| val settings = Settings(this) | ||||||
| val currentWindow = window ?: return | ||||||
|
|
||||||
| currentWindow.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN) | ||||||
|
Comment on lines
+53
to
+54
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We're not doing any input so this is unneeded
Suggested change
|
||||||
|
|
||||||
| val attributes = currentWindow.attributes | ||||||
| if (settings.useMaxBrightnessDisplayingBarcode()) { | ||||||
| attributes.screenBrightness = 1F | ||||||
| } | ||||||
|
|
||||||
| if (settings.keepScreenOn) { | ||||||
| currentWindow.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) | ||||||
| } | ||||||
|
|
||||||
| currentWindow.attributes = attributes | ||||||
| } | ||||||
|
|
||||||
| private fun Intent.imageLocationType(): ImageLocationType? { | ||||||
| val name = getStringExtra(BUNDLE_IMAGE_LOCATION_TYPE) ?: return null | ||||||
| return runCatching { ImageLocationType.valueOf(name) }.getOrNull() | ||||||
| } | ||||||
|
|
||||||
| private fun ImageLocationType.descriptionRes(): Int { | ||||||
| return when (this) { | ||||||
| ImageLocationType.icon -> R.string.thumbnailDescription | ||||||
| ImageLocationType.front -> R.string.frontImageDescription | ||||||
| ImageLocationType.back -> R.string.backImageDescription | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| companion object { | ||||||
| const val BUNDLE_ID = "id" | ||||||
| const val BUNDLE_IMAGE_LOCATION_TYPE = "imageLocationType" | ||||||
|
|
||||||
| @JvmStatic | ||||||
| fun createIntent( | ||||||
| context: Context, | ||||||
| loyaltyCardId: Int, | ||||||
| imageLocationType: ImageLocationType | ||||||
| ): Intent { | ||||||
| return Intent(context, LoyaltyCardImageActivity::class.java) | ||||||
| .putExtra(BUNDLE_ID, loyaltyCardId) | ||||||
| .putExtra(BUNDLE_IMAGE_LOCATION_TYPE, imageLocationType.name) | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,5 @@ | ||
| package protect.card_locker.cardview; | ||
|
|
||
| import android.content.ActivityNotFoundException; | ||
| import android.content.ClipData; | ||
| import android.content.ClipboardManager; | ||
| import android.content.Intent; | ||
|
|
@@ -34,7 +33,6 @@ | |
| import androidx.appcompat.app.ActionBar; | ||
| import androidx.appcompat.app.AlertDialog; | ||
| import androidx.appcompat.widget.Toolbar; | ||
| import androidx.core.content.FileProvider; | ||
| import androidx.core.graphics.BlendModeColorFilterCompat; | ||
| import androidx.core.graphics.BlendModeCompat; | ||
| import androidx.core.graphics.ColorUtils; | ||
|
|
@@ -44,7 +42,6 @@ | |
| import com.google.android.material.dialog.MaterialAlertDialogBuilder; | ||
| import com.google.android.material.snackbar.Snackbar; | ||
|
|
||
| import java.io.File; | ||
| import java.io.UnsupportedEncodingException; | ||
| import java.math.BigDecimal; | ||
| import java.nio.charset.Charset; | ||
|
|
@@ -136,46 +133,28 @@ public void onMainImageTap() { | |
| return; | ||
| } | ||
|
|
||
| // If this is an image, open it in the gallery. | ||
| openImageInGallery(imageType); | ||
| openImage(imageType); | ||
| } | ||
|
|
||
| private void openImageInGallery(LoyaltyCardImageType imageType) { | ||
| File file = null; | ||
|
|
||
| private void openImage(LoyaltyCardImageType imageType) { | ||
| switch (imageType) { | ||
| case NONE: | ||
| case BARCODE: | ||
| return; | ||
| case ICON: | ||
| file = Utils.retrieveCardImageAsFile(this, loyaltyCardId, ImageLocationType.icon); | ||
| break; | ||
| startImageActivity(ImageLocationType.icon); | ||
| return; | ||
| case IMAGE_FRONT: | ||
| file = Utils.retrieveCardImageAsFile(this, loyaltyCardId, ImageLocationType.front); | ||
| break; | ||
| startImageActivity(ImageLocationType.front); | ||
| return; | ||
| case IMAGE_BACK: | ||
| file = Utils.retrieveCardImageAsFile(this, loyaltyCardId, ImageLocationType.back); | ||
| break; | ||
| case BARCODE: | ||
| Toast.makeText(this, R.string.barcodeLongPressMessage, Toast.LENGTH_SHORT).show(); | ||
| startImageActivity(ImageLocationType.back); | ||
| return; | ||
| } | ||
|
Comment on lines
140
to
153
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Android Studio complains about the unnecessary return. How about this? Then we even have logging in case something stupid gets done in the future :) |
||
| } | ||
|
|
||
| // Do nothing if there is no file | ||
| if (file == null) { | ||
| Toast.makeText(this, R.string.failedToRetrieveImageFile, Toast.LENGTH_SHORT).show(); | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| Intent intent = new Intent(Intent.ACTION_VIEW) | ||
| .setDataAndType(FileProvider.getUriForFile(this, BuildConfig.APPLICATION_ID, file), "image/*") | ||
| .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); | ||
| startActivity(intent); | ||
| } catch (ActivityNotFoundException e) { | ||
| // Display a toast message if an image viewer is not installed on device | ||
| Toast.makeText(this, R.string.failedLaunchingPhotoPicker, Toast.LENGTH_SHORT).show(); | ||
| e.printStackTrace(); | ||
| } | ||
| private void startImageActivity(ImageLocationType imageLocationType) { | ||
| startActivity(LoyaltyCardImageActivity.createIntent(this, loyaltyCardId, imageLocationType)); | ||
| } | ||
|
|
||
| @Override | ||
|
|
@@ -291,7 +270,7 @@ protected void onCreate(Bundle savedInstanceState) { | |
|
|
||
| binding.iconContainer.setOnClickListener(view -> { | ||
| if (loyaltyCard.getImageThumbnail(this) != null) { | ||
| openImageInGallery(LoyaltyCardImageType.ICON); | ||
| openImage(LoyaltyCardImageType.ICON); | ||
| } else { | ||
| Toast.makeText(LoyaltyCardViewActivity.this, R.string.icon_header_click_text, Toast.LENGTH_LONG).show(); | ||
| } | ||
|
|
@@ -887,9 +866,9 @@ private void updateMainImageAccessibility() { | |
| int accessibilityClickAction; | ||
| LoyaltyCardImageType currentImageType = cardNavigator.getCurrent(); | ||
| if (currentImageType == LoyaltyCardImageType.IMAGE_FRONT) { | ||
| accessibilityClickAction = R.string.openFrontImageInGalleryApp; | ||
| accessibilityClickAction = R.string.zoomFrontImage; | ||
| } else if (currentImageType == LoyaltyCardImageType.IMAGE_BACK) { | ||
| accessibilityClickAction = R.string.openBackImageInGalleryApp; | ||
| accessibilityClickAction = R.string.zoomBackImage; | ||
| } else { | ||
| accessibilityClickAction = R.string.moveBarcodeToTopOfScreen; | ||
| } | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, I asked you already to move this to compose, but I think it may be best for this to use the same package as the activity. So maybe move this to |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| package protect.card_locker.compose | ||
|
|
||
| import android.graphics.Bitmap | ||
| import androidx.annotation.StringRes | ||
| import androidx.compose.foundation.Image | ||
| import androidx.compose.foundation.background | ||
| import androidx.compose.foundation.gestures.detectTapGestures | ||
| import androidx.compose.foundation.gestures.rememberTransformableState | ||
| import androidx.compose.foundation.gestures.transformable | ||
| import androidx.compose.foundation.layout.Box | ||
| import androidx.compose.foundation.layout.fillMaxSize | ||
| import androidx.compose.foundation.layout.padding | ||
| import androidx.compose.foundation.layout.statusBarsPadding | ||
| import androidx.compose.material.icons.Icons | ||
| import androidx.compose.material.icons.automirrored.filled.ArrowBack | ||
| import androidx.compose.material3.Icon | ||
| import androidx.compose.material3.IconButton | ||
| import androidx.compose.material3.MaterialTheme | ||
| import androidx.compose.runtime.Composable | ||
| import androidx.compose.runtime.getValue | ||
| import androidx.compose.runtime.mutableStateOf | ||
| import androidx.compose.runtime.remember | ||
| import androidx.compose.runtime.setValue | ||
| import androidx.compose.ui.Alignment | ||
| import androidx.compose.ui.Modifier | ||
| import androidx.compose.ui.geometry.Offset | ||
| import androidx.compose.ui.graphics.asImageBitmap | ||
| import androidx.compose.ui.graphics.graphicsLayer | ||
| import androidx.compose.ui.input.pointer.pointerInput | ||
| import androidx.compose.ui.layout.ContentScale | ||
| import androidx.compose.ui.layout.onSizeChanged | ||
| import androidx.compose.ui.platform.testTag | ||
| import androidx.compose.ui.res.stringResource | ||
| import androidx.compose.ui.unit.IntSize | ||
| import androidx.compose.ui.unit.dp | ||
| import protect.card_locker.R | ||
|
|
||
| @Composable | ||
| fun LoyaltyCardImageScreen( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given with the other renames, maybe |
||
| bitmap: Bitmap, | ||
| @StringRes contentDescriptionRes: Int, | ||
| onBack: () -> Unit, | ||
| ) { | ||
| var scale by remember { mutableStateOf(1F) } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Android Studio suggestion:
|
||
| var offset by remember { mutableStateOf(Offset.Zero) } | ||
| var size by remember { mutableStateOf(IntSize.Zero) } | ||
| val zoomState = rememberTransformableState { _, zoomChange, panChange, _ -> | ||
| val nextScale = (scale * zoomChange).coerceIn(1F, MAX_IMAGE_SCALE) | ||
| scale = nextScale | ||
| offset = if (nextScale == 1F) { | ||
| Offset.Zero | ||
| } else { | ||
| (offset + panChange).clampTo(size, nextScale) | ||
| } | ||
| } | ||
|
|
||
| Box( | ||
| modifier = Modifier | ||
| .fillMaxSize() | ||
| .background(MaterialTheme.colorScheme.background) | ||
| .testTag("card_image_zoom") | ||
| ) { | ||
| Image( | ||
| bitmap = bitmap.asImageBitmap(), | ||
| contentDescription = stringResource(contentDescriptionRes), | ||
| contentScale = ContentScale.Fit, | ||
| modifier = Modifier | ||
| .fillMaxSize() | ||
| .onSizeChanged { size = it } | ||
| .pointerInput(bitmap) { | ||
| detectTapGestures( | ||
| onDoubleTap = { | ||
| scale = 1F | ||
| offset = Offset.Zero | ||
| } | ||
| ) | ||
| } | ||
| .transformable(zoomState) | ||
| .graphicsLayer { | ||
| scaleX = scale | ||
| scaleY = scale | ||
| translationX = offset.x | ||
| translationY = offset.y | ||
| } | ||
| .testTag("card_image") | ||
| ) | ||
|
|
||
| IconButton( | ||
| onClick = onBack, | ||
| modifier = Modifier | ||
| .align(Alignment.TopStart) | ||
| .statusBarsPadding() | ||
| .padding(8.dp) | ||
| .testTag("card_image_back") | ||
| ) { | ||
| Icon( | ||
| imageVector = Icons.AutoMirrored.Filled.ArrowBack, | ||
| contentDescription = stringResource(R.string.back) | ||
| ) | ||
|
Comment on lines
+96
to
+99
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The back icon remains black in dark mode. Maybe using the CatimaTopAppBar like in the AboutActivity makes sense? Also standardizes the UI more, which should help future maintenance. |
||
| } | ||
| } | ||
| } | ||
|
|
||
| private fun Offset.clampTo(size: IntSize, scale: Float): Offset { | ||
| if (size.width == 0 || size.height == 0) { | ||
| return this | ||
| } | ||
|
|
||
| val maxX = size.width * (scale - 1F) / 2F | ||
| val maxY = size.height * (scale - 1F) / 2F | ||
| return Offset( | ||
| x = x.coerceIn(-maxX, maxX), | ||
| y = y.coerceIn(-maxY, maxY) | ||
| ) | ||
| } | ||
|
|
||
| private const val MAX_IMAGE_SCALE = 5F | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe move this into it's own
cardimageviewpackage and rename it toLoyaltyCardImageViewActivity.kt, given how stand-alone it is and to make clear this is specifically for viewing images, nothing else?