Skip to content

Commit 30b11e2

Browse files
author
alllexey
committed
Make QR code resizable, render with the background
1 parent 2c29c18 commit 30b11e2

File tree

8 files changed

+170
-95
lines changed

8 files changed

+170
-95
lines changed

app/src/main/java/me/alllexey123/itmowidgets/ui/qr/QrCodeActivity.kt

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,6 @@ import androidx.appcompat.app.AppCompatActivity
88
import com.google.android.material.floatingactionbutton.FloatingActionButton
99
import me.alllexey123.itmowidgets.ItmoWidgetsApp
1010
import me.alllexey123.itmowidgets.R
11-
import me.alllexey123.itmowidgets.ui.widgets.QrCodeWidget
12-
13-
private const val PIXELS_PER_MODULE: Int =
14-
(QrCodeWidget.Companion.PIXELS_PER_MODULE * 1.5).toInt()
1511

1612
class QrCodeActivity : AppCompatActivity() {
1713

@@ -62,17 +58,18 @@ class QrCodeActivity : AppCompatActivity() {
6258

6359
val bitmap = when (state) {
6460
is QrCodeUiState.Loading -> {
65-
renderer.renderFull(21 * PIXELS_PER_MODULE, PIXELS_PER_MODULE / 2F, dynamicColors)
61+
renderer.renderFull(dynamic = dynamicColors)
6662
}
6763

6864
is QrCodeUiState.Success -> {
6965
val qrCode = generator.generate(state.qrCodeHex)
70-
renderer.render(qrCode, PIXELS_PER_MODULE, dynamicColors)
66+
val qrCodeBooleans = generator.toBooleans(qrCode)
67+
renderer.render(qrCodeBooleans, dynamic = dynamicColors)
7168
}
7269

7370
is QrCodeUiState.Error -> {
7471
Toast.makeText(this, state.message, Toast.LENGTH_LONG).show()
75-
renderer.renderEmpty(21 * PIXELS_PER_MODULE, PIXELS_PER_MODULE / 2F, dynamicColors)
72+
renderer.renderEmpty(dynamic = dynamicColors)
7673
}
7774
}
7875

app/src/main/java/me/alllexey123/itmowidgets/ui/widgets/QrCodeWidget.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class QrCodeWidget : AppWidgetProvider() {
3939

4040
val appWidgetManager = AppWidgetManager.getInstance(context)
4141
val views = RemoteViews(context.packageName, R.layout.qr_code_widget)
42-
val bitmap = renderer.renderFull(21 * PIXELS_PER_MODULE, PIXELS_PER_MODULE / 2F, dynamicColors)
42+
val bitmap = renderer.renderFull(dynamic = dynamicColors)
4343

4444
views.setImageViewBitmap(R.id.qr_code_image, bitmap)
4545

@@ -50,8 +50,6 @@ class QrCodeWidget : AppWidgetProvider() {
5050

5151
companion object {
5252

53-
const val PIXELS_PER_MODULE = 20
54-
5553
const val ACTION_WIDGET_CLICK: String = "me.alllexey123.itmowidgets.action.QR_WIDGET_CLICK"
5654

5755
fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, bitmap: Bitmap, bgColor: Int) {

app/src/main/java/me/alllexey123/itmowidgets/util/QrBitmapRenderer.kt

Lines changed: 143 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -7,65 +7,151 @@ import android.graphics.Color
77
import android.graphics.Paint
88
import android.graphics.Path
99
import android.graphics.RectF
10+
import android.util.TypedValue
1011
import android.view.ContextThemeWrapper
1112
import androidx.core.graphics.createBitmap
1213
import com.google.android.material.color.MaterialColors
13-
import io.nayuki.qrcodegen.QrCode
1414
import me.alllexey123.itmowidgets.R
1515

1616
class QrBitmapRenderer(
1717
private val context: Context
1818
) {
1919

20-
fun render(qrCode: QrCode, pixelsPerModule: Int, dynamic: Boolean): Bitmap {
20+
21+
fun render(
22+
qrCode: List<List<Boolean>>,
23+
qrSidePixels: Int = defaultSidePixels(),
24+
relativePadding: Float = defaultRelativePadding(),
25+
bgRoundingPixels: Float = defaultBgRounding(),
26+
dynamic: Boolean
27+
): Bitmap {
28+
val colors = getQrColors(dynamic)
29+
return render(
30+
qrCode,
31+
qrSidePixels,
32+
colors.first,
33+
colors.second,
34+
relativePadding,
35+
bgRoundingPixels
36+
)
37+
}
38+
39+
fun renderEmpty(
40+
qrSidePixels: Int = defaultSidePixels(),
41+
relativePadding: Float = defaultRelativePadding(),
42+
bgRoundingPixels: Float = defaultBgRounding(),
43+
dynamic: Boolean
44+
): Bitmap {
2145
val colors = getQrColors(dynamic)
46+
return renderFilled(
47+
qrSidePixels,
48+
colors.first,
49+
colors.first,
50+
relativePadding,
51+
bgRoundingPixels
52+
)
53+
}
54+
55+
fun renderFull(
56+
qrSidePixels: Int = defaultSidePixels(),
57+
relativePadding: Float = defaultRelativePadding(),
58+
bgRoundingPixels: Float = defaultBgRounding(),
59+
dynamic: Boolean
60+
): Bitmap {
61+
val colors = getQrColors(dynamic)
62+
return renderFilled(
63+
qrSidePixels,
64+
colors.first,
65+
colors.second,
66+
relativePadding,
67+
bgRoundingPixels
68+
)
69+
}
2270

71+
fun renderFilled(
72+
qrSidePixels: Int = defaultSidePixels(),
73+
backgroundColor: Int,
74+
foregroundColor: Int,
75+
relativePadding: Float = defaultRelativePadding(),
76+
bgRoundingPixels: Float = defaultBgRounding()
77+
): Bitmap {
78+
val booleans = List(21) { MutableList(21) { true } }
79+
return render(
80+
booleans,
81+
qrSidePixels,
82+
backgroundColor,
83+
foregroundColor,
84+
relativePadding,
85+
bgRoundingPixels
86+
)
87+
}
88+
89+
90+
// padding: [0, 0.5]
91+
fun render(
92+
qrCode: List<List<Boolean>>,
93+
qrSidePixels: Int,
94+
backgroundColor: Int,
95+
foregroundColor: Int,
96+
relativePadding: Float,
97+
bgRoundingPixels: Float
98+
): Bitmap {
2399
val qrSize = qrCode.size
24-
val bitmap = createBitmap(qrSize * pixelsPerModule, qrSize * pixelsPerModule)
100+
val bitmap = createBitmap(qrSidePixels, qrSidePixels)
25101
val canvas = Canvas(bitmap)
26102

27-
val bgColor = colors.first
28-
val fgColor = colors.second
29-
canvas.drawColor(bgColor)
30-
31103
val foregroundPaint = Paint().apply {
32-
color = fgColor
104+
color = foregroundColor
33105
isAntiAlias = true
34106
}
35107

36108
val backgroundPaint = Paint().apply {
37-
color = bgColor
109+
color = backgroundColor
38110
isAntiAlias = true
39111
}
40112

41-
val cornerRadius = pixelsPerModule * 0.5f
113+
val moduleSidePixels = qrSidePixels * (1 - relativePadding * 2) / qrSize
114+
val moduleCornerRadius = moduleSidePixels * 0.5f
115+
116+
// draw rounded background
117+
val bg = Path().apply {
118+
addRoundRect(
119+
RectF(0F, 0F, qrSidePixels.toFloat(), qrSidePixels.toFloat()),
120+
bgRoundingPixels,
121+
bgRoundingPixels,
122+
Path.Direction.CW
123+
)
124+
}
125+
126+
val paddingPixels = qrSidePixels * relativePadding
127+
canvas.drawPath(bg, backgroundPaint)
42128

43129
// draw the black modules
44130
for (x in 0 until qrSize) {
45131
for (y in 0 until qrSize) {
46-
if (qrCode.getModule(x, y)) {
47-
val left = (x * pixelsPerModule).toFloat()
48-
val top = (y * pixelsPerModule).toFloat()
49-
val right = left + pixelsPerModule
50-
val bottom = top + pixelsPerModule
132+
if (qrCode[x][y]) {
133+
val left = x * moduleSidePixels + paddingPixels
134+
val top = y * moduleSidePixels + paddingPixels
135+
val right = left + moduleSidePixels
136+
val bottom = top + moduleSidePixels
51137

52-
val topN = qrCode.getModule(x, y - 1)
53-
val bottomN = qrCode.getModule(x, y + 1)
54-
val leftN = qrCode.getModule(x - 1, y)
55-
val rightN = qrCode.getModule(x + 1, y)
138+
val topN = qrCode.getOrNull(x)?.getOrNull(y - 1) == true
139+
val bottomN = qrCode.getOrNull(x)?.getOrNull(y + 1) == true
140+
val leftN = qrCode.getOrNull(x - 1)?.getOrNull(y) == true
141+
val rightN = qrCode.getOrNull(x + 1)?.getOrNull(y) == true
56142

57143
val radii = floatArrayOf(
58-
if (!topN && !leftN) cornerRadius else 0f,
59-
if (!topN && !leftN) cornerRadius else 0f,
144+
if (!topN && !leftN) moduleCornerRadius else 0f,
145+
if (!topN && !leftN) moduleCornerRadius else 0f,
60146

61-
if (!topN && !rightN) cornerRadius else 0f,
62-
if (!topN && !rightN) cornerRadius else 0f,
147+
if (!topN && !rightN) moduleCornerRadius else 0f,
148+
if (!topN && !rightN) moduleCornerRadius else 0f,
63149

64-
if (!bottomN && !rightN) cornerRadius else 0f,
65-
if (!bottomN && !rightN) cornerRadius else 0f,
150+
if (!bottomN && !rightN) moduleCornerRadius else 0f,
151+
if (!bottomN && !rightN) moduleCornerRadius else 0f,
66152

67-
if (!bottomN && !leftN) cornerRadius else 0f,
68-
if (!bottomN && !leftN) cornerRadius else 0f
153+
if (!bottomN && !leftN) moduleCornerRadius else 0f,
154+
if (!bottomN && !leftN) moduleCornerRadius else 0f
69155
)
70156

71157
val path = Path().apply {
@@ -79,37 +165,37 @@ class QrBitmapRenderer(
79165
// redraw the white modules (round corners)
80166
for (x in 0 until qrSize) {
81167
for (y in 0 until qrSize) {
82-
if (!qrCode.getModule(x, y)) {
83-
val x1 = (x * pixelsPerModule).toFloat()
84-
val y1 = (y * pixelsPerModule).toFloat()
85-
val x2 = x1 + pixelsPerModule
86-
val y2 = y1 + pixelsPerModule
168+
if (!qrCode[x][y]) {
169+
val x1 = x * moduleSidePixels + paddingPixels
170+
val y1 = y * moduleSidePixels + paddingPixels
171+
val x2 = x1 + moduleSidePixels
172+
val y2 = y1 + moduleSidePixels
87173
val rect = RectF(x1, y1, x2, y2)
88174

89175
var bottomRight = 0f
90176
var bottomLeft = 0f
91177
var topLeft = 0f
92178
var topRight = 0f
93179

94-
val top = qrCode.getModule(x, y - 1)
95-
val right = qrCode.getModule(x + 1, y)
96-
val bottom = qrCode.getModule(x, y + 1)
97-
val left = qrCode.getModule(x - 1, y)
180+
val top = qrCode.getOrNull(x)?.getOrNull(y - 1) == true
181+
val right = qrCode.getOrNull(x + 1)?.getOrNull(y) == true
182+
val bottom = qrCode.getOrNull(x)?.getOrNull(y + 1) == true
183+
val left = qrCode.getOrNull(x - 1)?.getOrNull(y) == true
98184

99-
if (right && bottom && qrCode.getModule(x + 1, y + 1)) {
100-
bottomRight = cornerRadius
185+
if (right && bottom && qrCode.getOrNull(x + 1)?.getOrNull(y + 1) == true) {
186+
bottomRight = moduleCornerRadius
101187
}
102188

103-
if (left && bottom && qrCode.getModule(x - 1, y + 1)) {
104-
bottomLeft = cornerRadius
189+
if (left && bottom && qrCode.getOrNull(x - 1)?.getOrNull(y + 1) == true) {
190+
bottomLeft = moduleCornerRadius
105191
}
106192

107-
if (right && top && qrCode.getModule(x + 1, y - 1)) {
108-
topRight = cornerRadius
193+
if (right && top && qrCode.getOrNull(x + 1)?.getOrNull(y - 1) == true) {
194+
topRight = moduleCornerRadius
109195
}
110196

111-
if (left && top && qrCode.getModule(x - 1, y - 1)) {
112-
topLeft = cornerRadius
197+
if (left && top && qrCode.getOrNull(x - 1)?.getOrNull(y - 1) == true) {
198+
topLeft = moduleCornerRadius
113199
}
114200

115201
canvas.drawRect(rect, foregroundPaint)
@@ -128,38 +214,24 @@ class QrBitmapRenderer(
128214
return bitmap
129215
}
130216

131-
fun renderEmpty(side: Int, rounding: Float, dynamic: Boolean): Bitmap {
132-
val colors = getQrColors(dynamic)
133-
return renderFilled(side, rounding, colors.first, colors.first)
217+
fun dpToPx(dp: Float): Float {
218+
return TypedValue.applyDimension(
219+
TypedValue.COMPLEX_UNIT_DIP,
220+
dp,
221+
context.resources.displayMetrics
222+
)
134223
}
135224

136-
fun renderFull(side: Int, rounding: Float, dynamic: Boolean): Bitmap {
137-
val colors = getQrColors(dynamic)
138-
return renderFilled(side, rounding, colors.first, colors.second)
225+
fun defaultBgRounding(): Float {
226+
return dpToPx(16F)
139227
}
140228

141-
fun renderFilled(side: Int, rounding: Float, bgColor: Int, fillColor: Int): Bitmap {
142-
val bitmap = createBitmap(side, side, Bitmap.Config.RGB_565)
143-
val canvas = Canvas(bitmap)
144-
canvas.drawColor(bgColor)
145-
146-
val paint = Paint().apply {
147-
color = fillColor
148-
isAntiAlias = true
149-
}
150-
151-
val path = Path().apply {
152-
addRoundRect(
153-
RectF(0F, 0F, side.toFloat(), side.toFloat()),
154-
rounding,
155-
rounding,
156-
Path.Direction.CW
157-
)
158-
}
159-
160-
canvas.drawPath(path, paint)
229+
fun defaultSidePixels(): Int {
230+
return 420
231+
}
161232

162-
return bitmap
233+
fun defaultRelativePadding(): Float {
234+
return 0.05F
163235
}
164236

165237
// [background, foreground]

app/src/main/java/me/alllexey123/itmowidgets/util/QrCodeGenerator.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,23 @@ package me.alllexey123.itmowidgets.util
33
import io.nayuki.qrcodegen.QrCode
44
import io.nayuki.qrcodegen.QrSegment
55
import java.nio.charset.StandardCharsets
6+
import java.util.stream.Collectors
7+
import java.util.stream.IntStream
68

79
class QrCodeGenerator {
810

911
fun generate(hex: String): QrCode {
1012
val segment = QrSegment.makeBytes(hex.toByteArray(StandardCharsets.ISO_8859_1))
1113
return QrCode.encodeSegments(listOf(segment), QrCode.Ecc.LOW, 1, 1, -1, false)
1214
}
15+
16+
fun toBooleans(qrCode: QrCode): List<List<Boolean>> {
17+
return IntStream.range(0, qrCode.size)
18+
.mapToObj { i ->
19+
IntStream.range(0, qrCode.size)
20+
.mapToObj { j -> qrCode.getModule(i, j) }
21+
.collect(Collectors.toList())
22+
}
23+
.collect(Collectors.toList())
24+
}
1325
}

app/src/main/java/me/alllexey123/itmowidgets/workers/QrWidgetUpdateWorker.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import androidx.work.WorkManager
1111
import androidx.work.WorkerParameters
1212
import me.alllexey123.itmowidgets.ItmoWidgetsApp
1313
import me.alllexey123.itmowidgets.ui.widgets.QrCodeWidget
14-
import me.alllexey123.itmowidgets.ui.widgets.QrCodeWidget.Companion.PIXELS_PER_MODULE
1514
import java.time.LocalDateTime
1615
import java.util.concurrent.TimeUnit
1716

@@ -38,10 +37,11 @@ class QrWidgetUpdateWorker(val appContext: Context, workerParams: WorkerParamete
3837
val bitmap: Bitmap = try {
3938
val qrHex = repository.getQrHex()
4039
val qrCode = generator.generate(qrHex)
41-
renderer.render(qrCode, PIXELS_PER_MODULE, dynamicColors)
40+
val qrCodeBooleans = generator.toBooleans(qrCode)
41+
renderer.render(qrCode = qrCodeBooleans, dynamic = dynamicColors)
4242
} catch (e: Exception) {
4343
storage.setErrorLog("[${javaClass.name}] at ${LocalDateTime.now()}: ${e.stackTraceToString()}")
44-
renderer.renderEmpty(21 * PIXELS_PER_MODULE, PIXELS_PER_MODULE / 2F, dynamicColors)
44+
renderer.renderEmpty(dynamic = dynamicColors)
4545
}
4646

4747
val colors = renderer.getQrColors(dynamicColors)

0 commit comments

Comments
 (0)