mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-22 20:18:36 +00:00
Update username link QR code styling.
This commit is contained in:
committed by
Clark Chen
parent
e1570e9512
commit
8372c699f7
@@ -131,7 +131,7 @@ class AppSettingsFragment : DSLSettingsFragment(
|
||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_manageProfileActivity)
|
||||
},
|
||||
onQrButtonClicked = {
|
||||
if (Recipient.self().getUsername().isPresent()) {
|
||||
if (Recipient.self().username.isPresent && Recipient.self().username.get().isNotEmpty()) {
|
||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_usernameLinkSettingsFragment)
|
||||
} else {
|
||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_usernameEducationFragment)
|
||||
|
||||
@@ -1,27 +1,30 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.usernamelinks
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.geometry.RoundRect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.res.imageResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.IntRect
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.thoughtcrime.securesms.R
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.floor
|
||||
|
||||
/**
|
||||
* Shows a QRCode that represents the provided data. Includes a Signal logo in the middle.
|
||||
@@ -32,7 +35,7 @@ fun QrCode(
|
||||
modifier: Modifier = Modifier,
|
||||
foregroundColor: Color = Color.Black,
|
||||
backgroundColor: Color = Color.White,
|
||||
deadzonePercent: Float = 0.4f
|
||||
deadzonePercent: Float = 0.35f
|
||||
) {
|
||||
val logo = ImageBitmap.imageResource(R.drawable.qrcode_logo)
|
||||
|
||||
@@ -58,53 +61,65 @@ private fun DrawScope.drawQr(
|
||||
deadzonePercent: Float,
|
||||
logo: ImageBitmap
|
||||
) {
|
||||
val deadzonePaddingPercent = 0.07f
|
||||
|
||||
// We want an even number of dots on either side of the deadzone
|
||||
val candidateDeadzoneWidth: Int = (data.width * deadzonePercent).toInt()
|
||||
val deadzoneWidth: Int = if ((data.width - candidateDeadzoneWidth) % 2 == 0) {
|
||||
candidateDeadzoneWidth
|
||||
} else {
|
||||
candidateDeadzoneWidth + 1
|
||||
}
|
||||
|
||||
val candidateDeadzoneHeight: Int = (data.height * deadzonePercent).toInt()
|
||||
val deadzoneHeight: Int = if ((data.height - candidateDeadzoneHeight) % 2 == 0) {
|
||||
candidateDeadzoneHeight
|
||||
} else {
|
||||
candidateDeadzoneHeight + 1
|
||||
}
|
||||
|
||||
val deadzoneStartX: Int = (data.width - deadzoneWidth) / 2
|
||||
val deadzoneEndX: Int = deadzoneStartX + deadzoneWidth
|
||||
val deadzoneStartY: Int = (data.height - deadzoneHeight) / 2
|
||||
val deadzoneEndY: Int = deadzoneStartY + deadzoneHeight
|
||||
val deadzoneRadius: Int = (data.height * (deadzonePercent + deadzonePaddingPercent)).toInt().let { candidateDeadzoneHeight ->
|
||||
if ((data.height - candidateDeadzoneHeight) % 2 == 0) {
|
||||
candidateDeadzoneHeight
|
||||
} else {
|
||||
candidateDeadzoneHeight + 1
|
||||
}
|
||||
} / 2
|
||||
|
||||
val cellWidthPx: Float = size.width / data.width
|
||||
val cellRadiusPx = cellWidthPx / 2
|
||||
val cornerRadius = CornerRadius(7f, 7f)
|
||||
val deadzone = Circle(center = IntOffset(data.width / 2, data.height / 2), radius = deadzoneRadius)
|
||||
|
||||
for (x in 0 until data.width) {
|
||||
for (y in 0 until data.height) {
|
||||
if (x < deadzoneStartX || x >= deadzoneEndX || y < deadzoneStartY || y >= deadzoneEndY) {
|
||||
drawCircle(
|
||||
color = if (data.get(x, y)) foregroundColor else backgroundColor,
|
||||
radius = cellRadiusPx,
|
||||
center = Offset(x * cellWidthPx + cellRadiusPx, y * cellWidthPx + cellRadiusPx)
|
||||
val position = IntOffset(x, y)
|
||||
|
||||
if (data.get(position) && !deadzone.contains(position)) {
|
||||
val filledAbove = IntOffset(x, y - 1).let { data.get(it) && !deadzone.contains(it) }
|
||||
val filledBelow = IntOffset(x, y + 1).let { data.get(it) && !deadzone.contains(it) }
|
||||
val filledLeft = IntOffset(x - 1, y).let { data.get(it) && !deadzone.contains(it) }
|
||||
val filledRight = IntOffset(x + 1, y).let { data.get(it) && !deadzone.contains(it) }
|
||||
|
||||
val path = Path().apply {
|
||||
addRoundRect(
|
||||
RoundRect(
|
||||
rect = Rect(
|
||||
topLeft = Offset(floor(x * cellWidthPx), floor(y * cellWidthPx - 1)),
|
||||
bottomRight = Offset(ceil((x + 1) * cellWidthPx), ceil((y + 1) * cellWidthPx + 1))
|
||||
),
|
||||
topLeft = if (filledAbove || filledLeft) CornerRadius.Zero else cornerRadius,
|
||||
topRight = if (filledAbove || filledRight) CornerRadius.Zero else cornerRadius,
|
||||
bottomLeft = if (filledBelow || filledLeft) CornerRadius.Zero else cornerRadius,
|
||||
bottomRight = if (filledBelow || filledRight) CornerRadius.Zero else cornerRadius
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
drawPath(
|
||||
path = path,
|
||||
color = if (data.get(position)) foregroundColor else backgroundColor
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Logo border
|
||||
val deadzonePaddingPercent = 0.03f
|
||||
val logoBorderRadiusPx = ((deadzonePercent - deadzonePaddingPercent) * size.width) / 2
|
||||
drawCircle(
|
||||
color = foregroundColor,
|
||||
radius = logoBorderRadiusPx,
|
||||
style = Stroke(width = 4.dp.toPx()),
|
||||
style = Stroke(width = cellWidthPx * 0.75f),
|
||||
center = this.center
|
||||
)
|
||||
|
||||
// Logo
|
||||
val logoWidthPx = ((deadzonePercent / 2) * size.width).toInt()
|
||||
val logoWidthPx = (((deadzonePercent - deadzonePaddingPercent) * 0.6f) * size.width).toInt()
|
||||
val logoOffsetPx = ((size.width - logoWidthPx) / 2).toInt()
|
||||
drawImage(
|
||||
image = logo,
|
||||
@@ -112,43 +127,6 @@ private fun DrawScope.drawQr(
|
||||
dstSize = IntSize(logoWidthPx, logoWidthPx),
|
||||
colorFilter = ColorFilter.tint(foregroundColor)
|
||||
)
|
||||
|
||||
for (eye in data.eyes()) {
|
||||
val strokeWidth = cellWidthPx
|
||||
|
||||
// Clear the already-drawn dots
|
||||
drawRect(
|
||||
color = backgroundColor,
|
||||
topLeft = Offset(
|
||||
x = eye.position.first * cellWidthPx,
|
||||
y = eye.position.second * cellWidthPx
|
||||
),
|
||||
size = Size(eye.size * cellWidthPx + cellRadiusPx, eye.size * cellWidthPx)
|
||||
)
|
||||
|
||||
// Outer square
|
||||
drawRoundRect(
|
||||
color = foregroundColor,
|
||||
topLeft = Offset(
|
||||
x = eye.position.first * cellWidthPx + strokeWidth / 2,
|
||||
y = eye.position.second * cellWidthPx + strokeWidth / 2
|
||||
),
|
||||
size = Size((eye.size - 1) * cellWidthPx, (eye.size - 1) * cellWidthPx),
|
||||
cornerRadius = CornerRadius(cellRadiusPx * 2, cellRadiusPx * 2),
|
||||
style = Stroke(width = strokeWidth)
|
||||
)
|
||||
|
||||
// Inner square
|
||||
drawRoundRect(
|
||||
color = foregroundColor,
|
||||
topLeft = Offset(
|
||||
x = (eye.position.first + 2) * cellWidthPx,
|
||||
y = (eye.position.second + 2) * cellWidthPx
|
||||
),
|
||||
size = Size((eye.size - 4) * cellWidthPx, (eye.size - 4) * cellWidthPx),
|
||||
cornerRadius = CornerRadius(cellRadiusPx, cellRadiusPx)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@@ -157,8 +135,17 @@ private fun Preview() {
|
||||
Surface {
|
||||
QrCode(
|
||||
data = QrCodeData.forData("https://signal.org", 64),
|
||||
modifier = Modifier.size(200.dp),
|
||||
deadzonePercent = 0.3f
|
||||
modifier = Modifier.size(350.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private data class Circle(
|
||||
val center: IntOffset,
|
||||
val radius: Int
|
||||
) {
|
||||
fun contains(position: IntOffset): Boolean {
|
||||
val diff = center - position
|
||||
return diff.x * diff.x + diff.y * diff.y < radius * radius
|
||||
}
|
||||
}
|
||||
@@ -74,7 +74,7 @@ fun QrCodeBadge(data: QrCodeData?, colorScheme: UsernameQrCodeColorScheme, usern
|
||||
if (data != null) {
|
||||
QrCode(
|
||||
data = data,
|
||||
modifier = Modifier.padding(20.dp),
|
||||
modifier = Modifier.padding(16.dp),
|
||||
foregroundColor = foregroundColor,
|
||||
backgroundColor = Color.White
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.usernamelinks
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.EncodeHintType
|
||||
import com.google.zxing.qrcode.QRCodeWriter
|
||||
@@ -17,93 +18,18 @@ class QrCodeData(
|
||||
private val bits: BitSet
|
||||
) {
|
||||
|
||||
fun get(x: Int, y: Int): Boolean {
|
||||
return bits.get(y * width + x)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the position of the "eyes" of the QR code -- the big squares in the three corners.
|
||||
* Returns true if the bit in the QR code is "on" for the specified position, false if it is "off" or out of bounds.
|
||||
*/
|
||||
fun eyes(): List<Eye> {
|
||||
val eyes: MutableList<Eye> = mutableListOf()
|
||||
|
||||
val size: Int = getPossibleEyeSize()
|
||||
|
||||
// Top left
|
||||
if (
|
||||
horizontalLineExists(0, 0, size) &&
|
||||
horizontalLineExists(0, size - 1, size) &&
|
||||
verticalLineExists(0, 0, size) &&
|
||||
verticalLineExists(size - 1, 0, size)
|
||||
) {
|
||||
eyes += Eye(
|
||||
position = 0 to 0,
|
||||
size = size
|
||||
)
|
||||
fun get(position: IntOffset): Boolean {
|
||||
val (x, y) = position
|
||||
return if (x < 0 || y < 0 || x >= width || y >= height) {
|
||||
false
|
||||
} else {
|
||||
bits.get(y * width + x)
|
||||
}
|
||||
|
||||
// Bottom left
|
||||
if (
|
||||
horizontalLineExists(0, height - size, size) &&
|
||||
horizontalLineExists(0, size - 1, size) &&
|
||||
verticalLineExists(0, height - size, size) &&
|
||||
verticalLineExists(size - 1, height - size, size)
|
||||
) {
|
||||
eyes += Eye(
|
||||
position = 0 to height - size,
|
||||
size = size
|
||||
)
|
||||
}
|
||||
|
||||
// Top right
|
||||
if (
|
||||
horizontalLineExists(width - size, 0, size) &&
|
||||
horizontalLineExists(width - size, size - 1, size) &&
|
||||
verticalLineExists(width - size, 0, size) &&
|
||||
verticalLineExists(width - 1, 0, size)
|
||||
) {
|
||||
eyes += Eye(
|
||||
position = width - size to 0,
|
||||
size = size
|
||||
)
|
||||
}
|
||||
|
||||
return eyes
|
||||
}
|
||||
|
||||
private fun getPossibleEyeSize(): Int {
|
||||
var x = 0
|
||||
|
||||
while (get(x, 0)) {
|
||||
x++
|
||||
}
|
||||
|
||||
return x
|
||||
}
|
||||
|
||||
private fun horizontalLineExists(x: Int, y: Int, length: Int): Boolean {
|
||||
for (p in x until x + length) {
|
||||
if (!get(p, y)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun verticalLineExists(x: Int, y: Int, length: Int): Boolean {
|
||||
for (p in y until y + length) {
|
||||
if (!get(x, p)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
data class Eye(
|
||||
val position: Pair<Int, Int>,
|
||||
val size: Int
|
||||
)
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
|
||||
@@ -70,7 +70,7 @@ fun UsernameQrScanScreen(
|
||||
AndroidView(
|
||||
factory = { context ->
|
||||
val view = QrScannerView(context)
|
||||
disposables += view.qrData.subscribe { data ->
|
||||
disposables += view.qrData.distinctUntilChanged().subscribe { data ->
|
||||
onQrCodeScanned(data)
|
||||
}
|
||||
view
|
||||
|
||||
Reference in New Issue
Block a user