diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ImageFullScreenView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ImageFullScreenView.kt index e19473d364..29becc9f0f 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ImageFullScreenView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ImageFullScreenView.kt @@ -1,12 +1,14 @@ import android.graphics.Bitmap import androidx.activity.compose.BackHandler import androidx.compose.foundation.* +import androidx.compose.foundation.gestures.detectTransformGestures import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.* +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.stringResource import chat.simplex.app.R @@ -20,11 +22,35 @@ fun ImageFullScreenView(imageBitmap: Bitmap, close: () -> Unit) { .background(Color.Black) .clickable(onClick = close) ) { + var scale by remember { mutableStateOf(1f) } + var translationX by remember { mutableStateOf(0f) } + var translationY by remember { mutableStateOf(0f) } Image( imageBitmap.asImageBitmap(), contentDescription = stringResource(R.string.image_descr), - modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Fit, + modifier = Modifier + .graphicsLayer( + scaleX = scale, + scaleY = scale, + translationX = translationX, + translationY = translationY, + ) + .pointerInput(Unit) { + detectTransformGestures( + onGesture = { _, pan, gestureZoom, _ -> + scale = (scale * gestureZoom).coerceIn(1f, 20f) + if (scale > 1) { + translationX += pan.x * scale + translationY += pan.y * scale + } else { + translationX = 0f + translationY = 0f + } + } + ) + } + .fillMaxSize(), ) } } diff --git a/apps/ios/Shared/Views/Helpers/CIImageView.swift b/apps/ios/Shared/Views/Helpers/CIImageView.swift index c4af213e6e..7fd1b6ded9 100644 --- a/apps/ios/Shared/Views/Helpers/CIImageView.swift +++ b/apps/ios/Shared/Views/Helpers/CIImageView.swift @@ -23,9 +23,14 @@ struct CIImageView: View { .fullScreenCover(isPresented: $showFullScreenImage) { ZStack { Color.black.edgesIgnoringSafeArea(.all) - Image(uiImage: uiImage) - .resizable() - .scaledToFit() + ZoomableScrollView { + ZStack { + Color.black.edgesIgnoringSafeArea(.all) + Image(uiImage: uiImage) + .resizable() + .scaledToFit() + } + } } .onTapGesture { showFullScreenImage = false } .gesture( diff --git a/apps/ios/Shared/Views/ZoomableScrollView.swift b/apps/ios/Shared/Views/ZoomableScrollView.swift new file mode 100644 index 0000000000..83528b593a --- /dev/null +++ b/apps/ios/Shared/Views/ZoomableScrollView.swift @@ -0,0 +1,60 @@ +// +// ZoomableScrollView.swift +// SimpleX +// +// Created by Evgeny on 13/05/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +struct ZoomableScrollView: UIViewRepresentable { + private var content: Content + + init(@ViewBuilder content: () -> Content) { + self.content = content() + } + + func makeUIView(context: Context) -> UIScrollView { + // set up the UIScrollView + let scrollView = UIScrollView() + scrollView.delegate = context.coordinator // for viewForZooming(in:) + scrollView.maximumZoomScale = 20 + scrollView.minimumZoomScale = 1 + scrollView.bouncesZoom = true + scrollView.backgroundColor = .black + scrollView.insetsLayoutMarginsFromSafeArea = false + + // create a UIHostingController to hold our SwiftUI content + let hostedView = context.coordinator.hostingController.view! + hostedView.translatesAutoresizingMaskIntoConstraints = true + hostedView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + hostedView.frame = scrollView.bounds + scrollView.addSubview(hostedView) + + return scrollView + } + + func makeCoordinator() -> Coordinator { + Coordinator(hostingController: UIHostingController(rootView: self.content)) + } + + func updateUIView(_ uiView: UIScrollView, context: Context) { + // update the hosting controller's SwiftUI content + context.coordinator.hostingController.rootView = self.content + assert(context.coordinator.hostingController.view.superview == uiView) + } + + // MARK: - Coordinator + class Coordinator: NSObject, UIScrollViewDelegate { + var hostingController: UIHostingController + + init(hostingController: UIHostingController) { + self.hostingController = hostingController + } + + func viewForZooming(in scrollView: UIScrollView) -> UIView? { + hostingController.view + } + } +} diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 8e0d9867c9..d2debf35a3 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -90,11 +90,18 @@ 5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CE4407827ADB701007B033A /* EmojiItemView.swift */; }; 5CEACCE327DE9246000BD591 /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCE227DE9246000BD591 /* ComposeView.swift */; }; 5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */; }; - 640DA1A5282D50D700AB8E8D /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 640DA1A0282D50D700AB8E8D /* libffi.a */; }; - 640DA1A6282D50D700AB8E8D /* libHSsimplex-chat-2.0.0-JAEoKkoXJYeE6TLyZconIN-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 640DA1A1282D50D700AB8E8D /* libHSsimplex-chat-2.0.0-JAEoKkoXJYeE6TLyZconIN-ghc8.10.7.a */; }; - 640DA1A7282D50D700AB8E8D /* libHSsimplex-chat-2.0.0-JAEoKkoXJYeE6TLyZconIN.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 640DA1A2282D50D700AB8E8D /* libHSsimplex-chat-2.0.0-JAEoKkoXJYeE6TLyZconIN.a */; }; - 640DA1A8282D50D700AB8E8D /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 640DA1A3282D50D700AB8E8D /* libgmpxx.a */; }; - 640DA1A9282D50D700AB8E8D /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 640DA1A4282D50D700AB8E8D /* libgmp.a */; }; + 5CFE0916282EE05E0002594B /* libHSsimplex-chat-2.0.1-4e10XUnKa54A7I6rlOZBZ.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CFE0911282EE05E0002594B /* libHSsimplex-chat-2.0.1-4e10XUnKa54A7I6rlOZBZ.a */; }; + 5CFE0917282EE05E0002594B /* libHSsimplex-chat-2.0.1-4e10XUnKa54A7I6rlOZBZ.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CFE0911282EE05E0002594B /* libHSsimplex-chat-2.0.1-4e10XUnKa54A7I6rlOZBZ.a */; }; + 5CFE0918282EE05E0002594B /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CFE0912282EE05E0002594B /* libffi.a */; }; + 5CFE0919282EE05E0002594B /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CFE0912282EE05E0002594B /* libffi.a */; }; + 5CFE091A282EE05E0002594B /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CFE0913282EE05E0002594B /* libgmpxx.a */; }; + 5CFE091B282EE05E0002594B /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CFE0913282EE05E0002594B /* libgmpxx.a */; }; + 5CFE091C282EE05E0002594B /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CFE0914282EE05E0002594B /* libgmp.a */; }; + 5CFE091D282EE05E0002594B /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CFE0914282EE05E0002594B /* libgmp.a */; }; + 5CFE091E282EE05E0002594B /* libHSsimplex-chat-2.0.1-4e10XUnKa54A7I6rlOZBZ-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CFE0915282EE05E0002594B /* libHSsimplex-chat-2.0.1-4e10XUnKa54A7I6rlOZBZ-ghc8.10.7.a */; }; + 5CFE091F282EE05E0002594B /* libHSsimplex-chat-2.0.1-4e10XUnKa54A7I6rlOZBZ-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CFE0915282EE05E0002594B /* libHSsimplex-chat-2.0.1-4e10XUnKa54A7I6rlOZBZ-ghc8.10.7.a */; }; + 5CFE0921282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; }; + 5CFE0922282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; }; 640F50E327CF991C001E05C2 /* SMPServers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640F50E227CF991C001E05C2 /* SMPServers.swift */; }; 6454036F2822A9750090DDFF /* ComposeFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6454036E2822A9750090DDFF /* ComposeFileView.swift */; }; 648010AB281ADD15009009B9 /* CIFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648010AA281ADD15009009B9 /* CIFileView.swift */; }; @@ -199,11 +206,12 @@ 5CE4407827ADB701007B033A /* EmojiItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiItemView.swift; sourceTree = ""; }; 5CEACCE227DE9246000BD591 /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = ""; }; 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsgContentView.swift; sourceTree = ""; }; - 640DA1A0282D50D700AB8E8D /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 640DA1A1282D50D700AB8E8D /* libHSsimplex-chat-2.0.0-JAEoKkoXJYeE6TLyZconIN-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-2.0.0-JAEoKkoXJYeE6TLyZconIN-ghc8.10.7.a"; sourceTree = ""; }; - 640DA1A2282D50D700AB8E8D /* libHSsimplex-chat-2.0.0-JAEoKkoXJYeE6TLyZconIN.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-2.0.0-JAEoKkoXJYeE6TLyZconIN.a"; sourceTree = ""; }; - 640DA1A3282D50D700AB8E8D /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; - 640DA1A4282D50D700AB8E8D /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; + 5CFE0911282EE05E0002594B /* libHSsimplex-chat-2.0.1-4e10XUnKa54A7I6rlOZBZ.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-2.0.1-4e10XUnKa54A7I6rlOZBZ.a"; sourceTree = ""; }; + 5CFE0912282EE05E0002594B /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; + 5CFE0913282EE05E0002594B /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + 5CFE0914282EE05E0002594B /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; + 5CFE0915282EE05E0002594B /* libHSsimplex-chat-2.0.1-4e10XUnKa54A7I6rlOZBZ-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-2.0.1-4e10XUnKa54A7I6rlOZBZ-ghc8.10.7.a"; sourceTree = ""; }; + 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZoomableScrollView.swift; path = Shared/Views/ZoomableScrollView.swift; sourceTree = SOURCE_ROOT; }; 640F50E227CF991C001E05C2 /* SMPServers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMPServers.swift; sourceTree = ""; }; 6454036E2822A9750090DDFF /* ComposeFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeFileView.swift; sourceTree = ""; }; 648010AA281ADD15009009B9 /* CIFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIFileView.swift; sourceTree = ""; }; @@ -220,14 +228,14 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 640DA1A6282D50D700AB8E8D /* libHSsimplex-chat-2.0.0-JAEoKkoXJYeE6TLyZconIN-ghc8.10.7.a in Frameworks */, + 5CFE091A282EE05E0002594B /* libgmpxx.a in Frameworks */, + 5CFE091E282EE05E0002594B /* libHSsimplex-chat-2.0.1-4e10XUnKa54A7I6rlOZBZ-ghc8.10.7.a in Frameworks */, + 5CFE091C282EE05E0002594B /* libgmp.a in Frameworks */, + 5CFE0916282EE05E0002594B /* libHSsimplex-chat-2.0.1-4e10XUnKa54A7I6rlOZBZ.a in Frameworks */, 5C8F01CD27A6F0D8007D2C8D /* CodeScanner in Frameworks */, - 640DA1A9282D50D700AB8E8D /* libgmp.a in Frameworks */, 5C764E83279C748B000C6508 /* libz.tbd in Frameworks */, + 5CFE0918282EE05E0002594B /* libffi.a in Frameworks */, 5C764E82279C748B000C6508 /* libiconv.tbd in Frameworks */, - 640DA1A5282D50D700AB8E8D /* libffi.a in Frameworks */, - 640DA1A7282D50D700AB8E8D /* libHSsimplex-chat-2.0.0-JAEoKkoXJYeE6TLyZconIN.a in Frameworks */, - 640DA1A8282D50D700AB8E8D /* libgmpxx.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -242,8 +250,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 5CFE091F282EE05E0002594B /* libHSsimplex-chat-2.0.1-4e10XUnKa54A7I6rlOZBZ-ghc8.10.7.a in Frameworks */, 5CDCAD5F28187D6900503DA2 /* libiconv.tbd in Frameworks */, + 5CFE091B282EE05E0002594B /* libgmpxx.a in Frameworks */, 5CDCAD6128187D8000503DA2 /* libz.tbd in Frameworks */, + 5CFE0917282EE05E0002594B /* libHSsimplex-chat-2.0.1-4e10XUnKa54A7I6rlOZBZ.a in Frameworks */, + 5CFE091D282EE05E0002594B /* libgmp.a in Frameworks */, + 5CFE0919282EE05E0002594B /* libffi.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -292,11 +305,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 640DA1A0282D50D700AB8E8D /* libffi.a */, - 640DA1A4282D50D700AB8E8D /* libgmp.a */, - 640DA1A3282D50D700AB8E8D /* libgmpxx.a */, - 640DA1A1282D50D700AB8E8D /* libHSsimplex-chat-2.0.0-JAEoKkoXJYeE6TLyZconIN-ghc8.10.7.a */, - 640DA1A2282D50D700AB8E8D /* libHSsimplex-chat-2.0.0-JAEoKkoXJYeE6TLyZconIN.a */, + 5CFE0912282EE05E0002594B /* libffi.a */, + 5CFE0914282EE05E0002594B /* libgmp.a */, + 5CFE0913282EE05E0002594B /* libgmpxx.a */, + 5CFE0915282EE05E0002594B /* libHSsimplex-chat-2.0.1-4e10XUnKa54A7I6rlOZBZ-ghc8.10.7.a */, + 5CFE0911282EE05E0002594B /* libHSsimplex-chat-2.0.1-4e10XUnKa54A7I6rlOZBZ.a */, ); path = Libraries; sourceTree = ""; @@ -339,6 +352,7 @@ 649BCDA12805D6EF00C3A862 /* CIImageView.swift */, 648010AA281ADD15009009B9 /* CIFileView.swift */, 6454036E2822A9750090DDFF /* ComposeFileView.swift */, + 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */, ); path = Helpers; sourceTree = ""; @@ -675,6 +689,7 @@ 649BCDA0280460FD00C3A862 /* ComposeImageView.swift in Sources */, 5CA059ED279559F40002BEB4 /* ContentView.swift in Sources */, 5CCD403427A5F6DF00368C90 /* AddContactView.swift in Sources */, + 5CFE0921282EEAF60002594B /* ZoomableScrollView.swift in Sources */, 5C3A88CE27DF50170060F1C2 /* DetermineWidth.swift in Sources */, 5CB0BA962827143500B3292C /* MakeConnection.swift in Sources */, 5C7505A527B679EE00BE3227 /* NavLinkPlain.swift in Sources */, @@ -729,6 +744,7 @@ 5CDCAD82281A7E2700503DA2 /* Notifications.swift in Sources */, 5CDCAD7C2818924D00503DA2 /* FileUtils.swift in Sources */, 5CDCAD682818876500503DA2 /* JSON.swift in Sources */, + 5CFE0922282EEAF60002594B /* ZoomableScrollView.swift in Sources */, 5CDCAD7828188FD300503DA2 /* ChatTypes.swift in Sources */, 5CDCAD7E2818941F00503DA2 /* API.swift in Sources */, 5CDCAD5428186F9700503DA2 /* GroupDefaults.swift in Sources */,