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)
|
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_manageProfileActivity)
|
||||||
},
|
},
|
||||||
onQrButtonClicked = {
|
onQrButtonClicked = {
|
||||||
if (Recipient.self().getUsername().isPresent()) {
|
if (Recipient.self().username.isPresent && Recipient.self().username.get().isNotEmpty()) {
|
||||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_usernameLinkSettingsFragment)
|
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_usernameLinkSettingsFragment)
|
||||||
} else {
|
} else {
|
||||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_usernameEducationFragment)
|
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_usernameEducationFragment)
|
||||||
|
|||||||
@@ -1,27 +1,30 @@
|
|||||||
package org.thoughtcrime.securesms.components.settings.app.usernamelinks
|
package org.thoughtcrime.securesms.components.settings.app.usernamelinks
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.drawBehind
|
import androidx.compose.ui.draw.drawBehind
|
||||||
import androidx.compose.ui.geometry.CornerRadius
|
import androidx.compose.ui.geometry.CornerRadius
|
||||||
import androidx.compose.ui.geometry.Offset
|
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.Color
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
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.DrawScope
|
||||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
import androidx.compose.ui.res.imageResource
|
import androidx.compose.ui.res.imageResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
|
import androidx.compose.ui.unit.IntRect
|
||||||
import androidx.compose.ui.unit.IntSize
|
import androidx.compose.ui.unit.IntSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import org.thoughtcrime.securesms.R
|
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.
|
* Shows a QRCode that represents the provided data. Includes a Signal logo in the middle.
|
||||||
@@ -32,7 +35,7 @@ fun QrCode(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
foregroundColor: Color = Color.Black,
|
foregroundColor: Color = Color.Black,
|
||||||
backgroundColor: Color = Color.White,
|
backgroundColor: Color = Color.White,
|
||||||
deadzonePercent: Float = 0.4f
|
deadzonePercent: Float = 0.35f
|
||||||
) {
|
) {
|
||||||
val logo = ImageBitmap.imageResource(R.drawable.qrcode_logo)
|
val logo = ImageBitmap.imageResource(R.drawable.qrcode_logo)
|
||||||
|
|
||||||
@@ -58,53 +61,65 @@ private fun DrawScope.drawQr(
|
|||||||
deadzonePercent: Float,
|
deadzonePercent: Float,
|
||||||
logo: ImageBitmap
|
logo: ImageBitmap
|
||||||
) {
|
) {
|
||||||
|
val deadzonePaddingPercent = 0.07f
|
||||||
|
|
||||||
// We want an even number of dots on either side of the deadzone
|
// We want an even number of dots on either side of the deadzone
|
||||||
val candidateDeadzoneWidth: Int = (data.width * deadzonePercent).toInt()
|
val deadzoneRadius: Int = (data.height * (deadzonePercent + deadzonePaddingPercent)).toInt().let { candidateDeadzoneHeight ->
|
||||||
val deadzoneWidth: Int = if ((data.width - candidateDeadzoneWidth) % 2 == 0) {
|
if ((data.height - candidateDeadzoneHeight) % 2 == 0) {
|
||||||
candidateDeadzoneWidth
|
candidateDeadzoneHeight
|
||||||
} else {
|
} else {
|
||||||
candidateDeadzoneWidth + 1
|
candidateDeadzoneHeight + 1
|
||||||
}
|
}
|
||||||
|
} / 2
|
||||||
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 cellWidthPx: Float = size.width / data.width
|
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 (x in 0 until data.width) {
|
||||||
for (y in 0 until data.height) {
|
for (y in 0 until data.height) {
|
||||||
if (x < deadzoneStartX || x >= deadzoneEndX || y < deadzoneStartY || y >= deadzoneEndY) {
|
val position = IntOffset(x, y)
|
||||||
drawCircle(
|
|
||||||
color = if (data.get(x, y)) foregroundColor else backgroundColor,
|
if (data.get(position) && !deadzone.contains(position)) {
|
||||||
radius = cellRadiusPx,
|
val filledAbove = IntOffset(x, y - 1).let { data.get(it) && !deadzone.contains(it) }
|
||||||
center = Offset(x * cellWidthPx + cellRadiusPx, y * cellWidthPx + cellRadiusPx)
|
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
|
// Logo border
|
||||||
val deadzonePaddingPercent = 0.03f
|
|
||||||
val logoBorderRadiusPx = ((deadzonePercent - deadzonePaddingPercent) * size.width) / 2
|
val logoBorderRadiusPx = ((deadzonePercent - deadzonePaddingPercent) * size.width) / 2
|
||||||
drawCircle(
|
drawCircle(
|
||||||
color = foregroundColor,
|
color = foregroundColor,
|
||||||
radius = logoBorderRadiusPx,
|
radius = logoBorderRadiusPx,
|
||||||
style = Stroke(width = 4.dp.toPx()),
|
style = Stroke(width = cellWidthPx * 0.75f),
|
||||||
center = this.center
|
center = this.center
|
||||||
)
|
)
|
||||||
|
|
||||||
// Logo
|
// Logo
|
||||||
val logoWidthPx = ((deadzonePercent / 2) * size.width).toInt()
|
val logoWidthPx = (((deadzonePercent - deadzonePaddingPercent) * 0.6f) * size.width).toInt()
|
||||||
val logoOffsetPx = ((size.width - logoWidthPx) / 2).toInt()
|
val logoOffsetPx = ((size.width - logoWidthPx) / 2).toInt()
|
||||||
drawImage(
|
drawImage(
|
||||||
image = logo,
|
image = logo,
|
||||||
@@ -112,43 +127,6 @@ private fun DrawScope.drawQr(
|
|||||||
dstSize = IntSize(logoWidthPx, logoWidthPx),
|
dstSize = IntSize(logoWidthPx, logoWidthPx),
|
||||||
colorFilter = ColorFilter.tint(foregroundColor)
|
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
|
@Preview
|
||||||
@@ -157,8 +135,17 @@ private fun Preview() {
|
|||||||
Surface {
|
Surface {
|
||||||
QrCode(
|
QrCode(
|
||||||
data = QrCodeData.forData("https://signal.org", 64),
|
data = QrCodeData.forData("https://signal.org", 64),
|
||||||
modifier = Modifier.size(200.dp),
|
modifier = Modifier.size(350.dp)
|
||||||
deadzonePercent = 0.3f
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
if (data != null) {
|
||||||
QrCode(
|
QrCode(
|
||||||
data = data,
|
data = data,
|
||||||
modifier = Modifier.padding(20.dp),
|
modifier = Modifier.padding(16.dp),
|
||||||
foregroundColor = foregroundColor,
|
foregroundColor = foregroundColor,
|
||||||
backgroundColor = Color.White
|
backgroundColor = Color.White
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.thoughtcrime.securesms.components.settings.app.usernamelinks
|
package org.thoughtcrime.securesms.components.settings.app.usernamelinks
|
||||||
|
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import com.google.zxing.BarcodeFormat
|
import com.google.zxing.BarcodeFormat
|
||||||
import com.google.zxing.EncodeHintType
|
import com.google.zxing.EncodeHintType
|
||||||
import com.google.zxing.qrcode.QRCodeWriter
|
import com.google.zxing.qrcode.QRCodeWriter
|
||||||
@@ -17,93 +18,18 @@ class QrCodeData(
|
|||||||
private val bits: BitSet
|
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> {
|
fun get(position: IntOffset): Boolean {
|
||||||
val eyes: MutableList<Eye> = mutableListOf()
|
val (x, y) = position
|
||||||
|
return if (x < 0 || y < 0 || x >= width || y >= height) {
|
||||||
val size: Int = getPossibleEyeSize()
|
false
|
||||||
|
} else {
|
||||||
// Top left
|
bits.get(y * width + x)
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
companion object {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ fun UsernameQrScanScreen(
|
|||||||
AndroidView(
|
AndroidView(
|
||||||
factory = { context ->
|
factory = { context ->
|
||||||
val view = QrScannerView(context)
|
val view = QrScannerView(context)
|
||||||
disposables += view.qrData.subscribe { data ->
|
disposables += view.qrData.distinctUntilChanged().subscribe { data ->
|
||||||
onQrCodeScanned(data)
|
onQrCodeScanned(data)
|
||||||
}
|
}
|
||||||
view
|
view
|
||||||
|
|||||||
Reference in New Issue
Block a user