Fix username QR code sharing.
This commit is contained in:
parent
7f2b6a874e
commit
423719e7bc
6 changed files with 147 additions and 41 deletions
|
@ -53,12 +53,12 @@ fun QrCode(
|
|||
}
|
||||
}
|
||||
|
||||
private fun DrawScope.drawQr(
|
||||
fun DrawScope.drawQr(
|
||||
data: QrCodeData,
|
||||
foregroundColor: Color,
|
||||
backgroundColor: Color,
|
||||
deadzonePercent: Float,
|
||||
logo: ImageBitmap
|
||||
logo: ImageBitmap?
|
||||
) {
|
||||
val deadzonePaddingPercent = 0.045f
|
||||
|
||||
|
@ -120,12 +120,14 @@ private fun DrawScope.drawQr(
|
|||
// Logo
|
||||
val logoWidthPx = (((deadzonePercent - deadzonePaddingPercent) * 0.6f) * size.width).toInt()
|
||||
val logoOffsetPx = ((size.width - logoWidthPx) / 2).toInt()
|
||||
drawImage(
|
||||
image = logo,
|
||||
dstOffset = IntOffset(logoOffsetPx, logoOffsetPx),
|
||||
dstSize = IntSize(logoWidthPx, logoWidthPx),
|
||||
colorFilter = ColorFilter.tint(foregroundColor)
|
||||
)
|
||||
if (logo != null) {
|
||||
drawImage(
|
||||
image = logo,
|
||||
dstOffset = IntOffset(logoOffsetPx, logoOffsetPx),
|
||||
dstSize = IntSize(logoWidthPx, logoWidthPx),
|
||||
colorFilter = ColorFilter.tint(foregroundColor)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
|
|
|
@ -24,17 +24,12 @@ import androidx.compose.material3.Surface
|
|||
import androidx.compose.material3.Text
|
||||
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.draw.clip
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
|
@ -45,8 +40,6 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.compose.ui.unit.sp
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.compose.ScreenshotController
|
||||
import org.thoughtcrime.securesms.compose.getScreenshotBounds
|
||||
|
||||
/**
|
||||
* Renders a QR code and username as a badge.
|
||||
|
@ -57,23 +50,16 @@ fun QrCodeBadge(
|
|||
colorScheme: UsernameQrCodeColorScheme,
|
||||
username: String,
|
||||
modifier: Modifier = Modifier,
|
||||
screenshotController: ScreenshotController? = null,
|
||||
usernameCopyable: Boolean = false,
|
||||
onClick: (() -> Unit) = {}
|
||||
) {
|
||||
val borderColor by animateColorAsState(targetValue = colorScheme.borderColor, label = "border")
|
||||
val foregroundColor by animateColorAsState(targetValue = colorScheme.foregroundColor, label = "foreground")
|
||||
val elevation by animateFloatAsState(targetValue = if (colorScheme == UsernameQrCodeColorScheme.White) 10f else 0f, label = "elevation")
|
||||
val textColor by animateColorAsState(targetValue = if (colorScheme == UsernameQrCodeColorScheme.White) Color.Black else Color.White, label = "textColor")
|
||||
var badgeBounds by remember {
|
||||
mutableStateOf<Rect?>(null)
|
||||
}
|
||||
screenshotController?.bind(LocalView.current, badgeBounds)
|
||||
val textColor by animateColorAsState(targetValue = colorScheme.textColor, label = "textColor")
|
||||
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.onGloballyPositioned {
|
||||
badgeBounds = it.getScreenshotBounds()
|
||||
},
|
||||
modifier = modifier,
|
||||
color = borderColor,
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
shadowElevation = elevation.dp
|
||||
|
@ -99,8 +85,8 @@ fun QrCodeBadge(
|
|||
data = data.data,
|
||||
modifier = Modifier
|
||||
.border(
|
||||
width = if (colorScheme == UsernameQrCodeColorScheme.White) 2.dp else 0.dp,
|
||||
color = Color(0xFFE9E9E9),
|
||||
width = 2.dp,
|
||||
color = colorScheme.outlineColor,
|
||||
shape = RoundedCornerShape(size = 12.dp)
|
||||
)
|
||||
.padding(16.dp),
|
||||
|
|
|
@ -8,6 +8,8 @@ import androidx.compose.ui.graphics.Color
|
|||
enum class UsernameQrCodeColorScheme(
|
||||
val borderColor: Color,
|
||||
val foregroundColor: Color,
|
||||
val textColor: Color = Color.White,
|
||||
val outlineColor: Color = Color.Transparent,
|
||||
private val key: String
|
||||
) {
|
||||
Blue(
|
||||
|
@ -18,6 +20,8 @@ enum class UsernameQrCodeColorScheme(
|
|||
White(
|
||||
borderColor = Color(0xFFFFFFFF),
|
||||
foregroundColor = Color(0xFF000000),
|
||||
textColor = Color.Black,
|
||||
outlineColor = Color(0xFFE9E9E9),
|
||||
key = "white"
|
||||
),
|
||||
Grey(
|
||||
|
|
|
@ -114,9 +114,8 @@ class UsernameLinkSettingsFragment : ComposeFragment() {
|
|||
modifier = Modifier.padding(contentPadding),
|
||||
navController = navController,
|
||||
onShareBadge = {
|
||||
shareQrBadge(it)
|
||||
shareQrBadge(viewModel.generateQrCodeImage())
|
||||
},
|
||||
screenshotController = screenshotController,
|
||||
onResetClicked = { showResetDialog = true },
|
||||
onLinkResultHandled = { viewModel.onUsernameLinkResetResultHandled() }
|
||||
)
|
||||
|
@ -278,7 +277,11 @@ class UsernameLinkSettingsFragment : ComposeFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun shareQrBadge(badge: Bitmap) {
|
||||
private fun shareQrBadge(badge: Bitmap?) {
|
||||
if (badge == null) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
ByteArrayOutputStream().use { byteArrayOutputStream ->
|
||||
badge.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream)
|
||||
|
|
|
@ -1,7 +1,23 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.usernamelinks.main
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.graphics.Typeface
|
||||
import android.os.Build
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Canvas
|
||||
import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.lifecycle.ViewModel
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
|
@ -10,8 +26,10 @@ import io.reactivex.rxjava3.kotlin.plusAssign
|
|||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import io.reactivex.rxjava3.subjects.BehaviorSubject
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeData
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeState
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.drawQr
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.main.UsernameLinkSettingsState.ActiveTab
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
|
@ -165,4 +183,106 @@ class UsernameLinkSettingsViewModel : ViewModel() {
|
|||
url.map { QrCodeData.forData(it, 64) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fun fact: there's no way to draw a composable to a bitmap. You'd think there would be, but there isn't. You can "screenshot" it if it's 100% on-screen,
|
||||
* but if it's partially offscreen you're SOL. So, we get to go through the fun process of re-drawing the QR badge to an image for sharing ourselves.
|
||||
*
|
||||
* Sizes were picked arbitrarily.
|
||||
*
|
||||
* I hate this as much as you do.
|
||||
*/
|
||||
fun generateQrCodeImage(): Bitmap? {
|
||||
val state: UsernameLinkSettingsState = _state.value
|
||||
|
||||
if (state.qrCodeState !is QrCodeState.Present) {
|
||||
Log.w(TAG, "Invalid state to generate QR code! ${state.qrCodeState.javaClass.simpleName}")
|
||||
return null
|
||||
}
|
||||
|
||||
val qrCodeData: QrCodeData = state.qrCodeState.data
|
||||
|
||||
val width = 480
|
||||
val height = 525
|
||||
val qrSize = 300f
|
||||
val qrPadding = 25f
|
||||
val borderSizeX = 64f
|
||||
val borderSizeY = 52f
|
||||
|
||||
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).apply {
|
||||
eraseColor(Color.TRANSPARENT)
|
||||
}
|
||||
|
||||
val androidCanvas = android.graphics.Canvas(bitmap)
|
||||
val composeCanvas = Canvas(androidCanvas)
|
||||
val canvasDrawScope = CanvasDrawScope()
|
||||
|
||||
// Draw the background
|
||||
androidCanvas.drawRoundRect(0f, 0f, width.toFloat(), height.toFloat(), 30f, 30f, Paint().apply { color = state.qrCodeColorScheme.borderColor.toArgb() })
|
||||
androidCanvas.drawRoundRect(borderSizeX, borderSizeY, borderSizeX + qrSize + qrPadding * 2, borderSizeY + qrSize + qrPadding * 2, 15f, 15f, Paint().apply { color = Color.WHITE })
|
||||
androidCanvas.drawRoundRect(
|
||||
borderSizeX,
|
||||
borderSizeY,
|
||||
borderSizeX + qrSize + qrPadding * 2,
|
||||
borderSizeY + qrSize + qrPadding * 2,
|
||||
15f,
|
||||
15f,
|
||||
Paint().apply {
|
||||
color = state.qrCodeColorScheme.outlineColor.toArgb()
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = 4f
|
||||
}
|
||||
)
|
||||
|
||||
// Draw the QR code
|
||||
composeCanvas.translate((width / 2) - (qrSize / 2), 80f)
|
||||
canvasDrawScope.draw(
|
||||
density = object : Density {
|
||||
override val density: Float = 1f
|
||||
override val fontScale: Float = 1f
|
||||
},
|
||||
layoutDirection = LayoutDirection.Ltr,
|
||||
canvas = composeCanvas,
|
||||
size = Size(qrSize, qrSize)
|
||||
) {
|
||||
drawQr(
|
||||
data = qrCodeData,
|
||||
foregroundColor = state.qrCodeColorScheme.foregroundColor,
|
||||
backgroundColor = state.qrCodeColorScheme.borderColor,
|
||||
deadzonePercent = 0.35f,
|
||||
logo = null
|
||||
)
|
||||
}
|
||||
composeCanvas.translate(-90f, -80f)
|
||||
|
||||
// Draw the signal logo -- unfortunately can't have the normal QR code drawing handle it because it requires a composable ImageBitmap
|
||||
BitmapFactory.decodeResource(ApplicationDependencies.getApplication().resources, R.drawable.qrcode_logo).also { logoBitmap ->
|
||||
val tintedPaint = Paint().apply {
|
||||
colorFilter = PorterDuffColorFilter(state.qrCodeColorScheme.foregroundColor.toArgb(), PorterDuff.Mode.SRC_IN)
|
||||
}
|
||||
val sourceRect = Rect(0, 0, logoBitmap.width, logoBitmap.height)
|
||||
val destRect = RectF(210f, 200f, 270f, 260f)
|
||||
androidCanvas.drawBitmap(logoBitmap, sourceRect, destRect, tintedPaint)
|
||||
}
|
||||
|
||||
// Draw the text
|
||||
val textPaint = Paint().apply {
|
||||
color = state.qrCodeColorScheme.textColor.toArgb()
|
||||
textSize = 34f
|
||||
typeface = if (Build.VERSION.SDK_INT < 26) {
|
||||
Typeface.DEFAULT_BOLD
|
||||
} else {
|
||||
Typeface.Builder("")
|
||||
.setFallback("sans-serif")
|
||||
.setWeight(600)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
val textBounds = Rect()
|
||||
textPaint.getTextBounds(state.username, 0, state.username.length, textBounds)
|
||||
|
||||
androidCanvas.drawText(state.username, (width / 2f) - (textBounds.width() / 2f), 465f, textPaint)
|
||||
|
||||
return bitmap
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.usernamelinks.main
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Bitmap
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
|
@ -43,7 +42,6 @@ import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeDa
|
|||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeState
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.UsernameQrCodeColorScheme
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.main.UsernameLinkSettingsState.ActiveTab
|
||||
import org.thoughtcrime.securesms.compose.ScreenshotController
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
|
@ -57,9 +55,8 @@ fun UsernameLinkShareScreen(
|
|||
snackbarHostState: SnackbarHostState,
|
||||
scope: CoroutineScope,
|
||||
navController: NavController,
|
||||
onShareBadge: (Bitmap) -> Unit,
|
||||
onShareBadge: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
screenshotController: ScreenshotController? = null,
|
||||
onResetClicked: () -> Unit
|
||||
) {
|
||||
when (state.usernameLinkResetResult) {
|
||||
|
@ -82,7 +79,6 @@ fun UsernameLinkShareScreen(
|
|||
data = state.qrCodeState,
|
||||
colorScheme = state.qrCodeColorScheme,
|
||||
username = state.username,
|
||||
screenshotController = screenshotController,
|
||||
usernameCopyable = true,
|
||||
modifier = Modifier.padding(horizontal = 58.dp, vertical = 24.dp),
|
||||
onClick = {
|
||||
|
@ -93,12 +89,7 @@ fun UsernameLinkShareScreen(
|
|||
)
|
||||
|
||||
ButtonBar(
|
||||
onShareClicked = {
|
||||
val badgeBitmap = screenshotController?.screenshot()
|
||||
if (badgeBitmap != null) {
|
||||
onShareBadge.invoke(badgeBitmap)
|
||||
}
|
||||
},
|
||||
onShareClicked = onShareBadge,
|
||||
onColorClicked = { navController.safeNavigate(R.id.action_usernameLinkSettingsFragment_to_usernameLinkQrColorPickerFragment) }
|
||||
)
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue