Compare commits

..
13 Commits
Author SHA1 Message Date
Markus Fisch 62d3fd7e94 Advance version number to 1.30.1 2020-06-02 21:48:56 +02:00
Markus Fisch acdac4ca30 Improve german translation for ROI handle setting 2020-06-02 17:36:19 +02:00
Markus Fisch 221cf11576 Convert ROI handle positions to integer points
Because they're always discrete values and can never be inbetween.

Also, calling `drawBitmap()` with float values can lead to slightly
distorted renderings because the image is interpolated between
pixels then.

So let's just avoid this by using integers.
2020-06-02 16:04:39 +02:00
Markus Fisch d34fceb48f Move ROI button back in place if ROI is too small
It's probably better to restore the original situation than
having the button stay where it is.

Also clean up DetectorView a bit and refactor some variables.
2020-06-02 09:59:18 +02:00
Markus Fisch 4cc4cef4ca Show plus in FAB if flash is not available
So it doesn't look awkward when a device doesn't have a flash.
2020-06-01 22:49:25 +02:00
Markus Fisch 969ffd3d0a Move ROI button above FAB
Looks tidier.
2020-06-01 22:48:42 +02:00
Markus Fisch 81a505c752 Refactor Candidates to Dots
The dots aren't just candidates but may already be result points
so this name doesn't really fit.
2020-06-01 20:04:06 +02:00
Markus Fisch eafcf0dba6 Rename ic_crop_handle to button_crop
It's not an icon but a button.
2020-06-01 19:58:29 +02:00
zmniandGitHub 0539468d3d Update Indonesian translation 2020-06-01 19:48:09 +02:00
Markus Fisch 5e9361ac77 Keep ROI over orientation changes 2020-06-01 10:44:48 +02:00
Markus Fisch 54fb31d6a5 Show a default ROI when button is just tapped
Nobody knows the button is a handle that can be used.

So the natural thing to do is to tap on it which should do the
next best thing and that is showing a default region of interest.

Maybe the button should be positioned above the fab and have a
different color when inactive.
2020-05-31 22:11:43 +02:00
Markus Fisch 0f7da91e77 Draw round corners for ROI box
Looks much better.
2020-05-31 19:29:00 +02:00
Markus Fisch 13bbb3e435 Update build tools and gradle wrapper 2020-05-31 13:15:46 +02:00
15 changed files with 264 additions and 99 deletions
+5
View File
@@ -1,5 +1,10 @@
# Change Log
## 1.30.1
* Improve usability of cropping limiter
* Draw round corners around region of interest
* Update Indonesian translation
## 1.30.0
* Add a handle to define a region of interest
* Add a setting to show/hide cropping limiter
+2 -2
View File
@@ -9,8 +9,8 @@ android {
minSdkVersion 9
targetSdkVersion sdk_version
versionCode 66
versionName '1.30.0'
versionCode 67
versionName '1.30.1'
// it's recommended to set this value to the lowest API level
// able to provide all the functionality
@@ -10,6 +10,7 @@ import android.hardware.Camera
import android.net.Uri
import android.os.Bundle
import android.os.Vibrator
import android.support.design.widget.FloatingActionButton
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.support.v8.renderscript.RSRuntimeException
@@ -28,6 +29,7 @@ import de.markusfisch.android.binaryeye.graphics.Mapping
import de.markusfisch.android.binaryeye.graphics.frameToView
import de.markusfisch.android.binaryeye.graphics.isPortrait
import de.markusfisch.android.binaryeye.rs.Preprocessor
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
import de.markusfisch.android.binaryeye.widget.DetectorView
import de.markusfisch.android.binaryeye.widget.toast
import de.markusfisch.android.binaryeye.zxing.Zxing
@@ -53,7 +55,7 @@ class CameraActivity : AppCompatActivity() {
private lateinit var cameraView: CameraView
private lateinit var detectorView: DetectorView
private lateinit var zoomBar: SeekBar
private lateinit var flashFab: View
private lateinit var flashFab: FloatingActionButton
private var preprocessor: Preprocessor? = null
private var nativeMapping: Mapping? = null
@@ -110,13 +112,13 @@ class CameraActivity : AppCompatActivity() {
cameraView = findViewById(R.id.camera_view) as CameraView
detectorView = findViewById(R.id.detector_view) as DetectorView
zoomBar = findViewById(R.id.zoom) as SeekBar
flashFab = findViewById(R.id.flash)
flashFab.setOnClickListener { toggleTorchMode() }
flashFab = findViewById(R.id.flash) as FloatingActionButton
initCameraView()
initZoomBar()
restoreZoom()
detectorView.updateRoi = { recreatePreprocessor = true }
detectorView.setPaddingFromWindowInsets()
if (intent?.action == Intent.ACTION_SEND &&
intent.type == "text/plain"
@@ -193,7 +195,7 @@ class CameraActivity : AppCompatActivity() {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.create -> {
startActivity(MainActivity.getEncodeIntent(this))
createBarcode()
true
}
R.id.history -> {
@@ -228,6 +230,10 @@ class CameraActivity : AppCompatActivity() {
}
}
private fun createBarcode() {
startActivity(MainActivity.getEncodeIntent(this))
}
private fun switchCamera() {
closeCamera()
frontFacing = frontFacing xor true
@@ -412,11 +418,13 @@ class CameraActivity : AppCompatActivity() {
)
}
private fun updateFlashFab(available: Boolean) {
flashFab.visibility = if (available) {
View.GONE
private fun updateFlashFab(unavailable: Boolean) {
if (unavailable) {
flashFab.setImageResource(R.drawable.ic_action_create)
flashFab.setOnClickListener { createBarcode() }
} else {
View.VISIBLE
flashFab.setImageResource(R.drawable.ic_action_flash)
flashFab.setOnClickListener { toggleTorchMode() }
}
}
@@ -0,0 +1,15 @@
package de.markusfisch.android.binaryeye.graphics
import android.content.Context
import android.graphics.DashPathEffect
import android.graphics.Paint
import android.support.v4.content.ContextCompat
import de.markusfisch.android.binaryeye.R
fun Context.getDashedBorderPaint() = Paint(Paint.ANTI_ALIAS_FLAG).apply {
val dp = resources.displayMetrics.density
color = ContextCompat.getColor(applicationContext, R.color.crop_bound)
style = Paint.Style.STROKE
strokeWidth = dp * 2f
pathEffect = DashPathEffect(floatArrayOf(10f * dp, 10f * dp), 0f)
}
@@ -7,14 +7,14 @@ import android.graphics.Point
import android.support.v4.content.ContextCompat
import de.markusfisch.android.binaryeye.R
class Candidates(context: Context) {
class Dots(context: Context) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val radius = 8f * context.resources.displayMetrics.density
init {
paint.color = ContextCompat.getColor(
context,
R.color.candidate
R.color.dot
)
paint.style = Paint.Style.FILL
}
@@ -1,11 +1,13 @@
package de.markusfisch.android.binaryeye.widget
import android.content.Context
import android.graphics.*
import android.support.v4.content.ContextCompat
import android.graphics.Canvas
import android.graphics.Point
import android.graphics.Rect
import android.graphics.RectF
import android.util.AttributeSet
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.graphics.Candidates
import de.markusfisch.android.binaryeye.graphics.Dots
import de.markusfisch.android.binaryeye.graphics.getDashedBorderPaint
import kotlin.math.roundToInt
class CropImageView(context: Context, attr: AttributeSet) :
@@ -14,25 +16,20 @@ class CropImageView(context: Context, attr: AttributeSet) :
var onScan: (() -> List<Point>?)? = null
private val candidates = Candidates(context)
private val boundsPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val dots = Dots(context)
private val boundsPaint = context.getDashedBorderPaint()
private val lastMappedRect = RectF()
private val dp = context.resources.displayMetrics.density
private val padding: Int = (dp * 24f).roundToInt()
private val padding: Int = (24f * context.resources.displayMetrics.density).roundToInt()
private val onScanRunnable = Runnable {
onScan?.invoke()?.let {
candidatePoints = it
resultPoints = it
invalidate()
}
}
private var candidatePoints: List<Point>? = null
private var resultPoints: List<Point>? = null
init {
boundsPaint.color = ContextCompat.getColor(context, R.color.crop_bound)
boundsPaint.style = Paint.Style.STROKE
boundsPaint.strokeWidth = dp * 2f
boundsPaint.pathEffect = DashPathEffect(floatArrayOf(dp * 10f, dp * 10f), 0f)
scaleType = ScaleType.CENTER_CROP
}
@@ -85,10 +82,10 @@ class CropImageView(context: Context, attr: AttributeSet) :
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawRect(bounds, boundsPaint)
candidatePoints?.let {
candidates.draw(canvas, it)
resultPoints?.let {
dots.draw(canvas, it)
}
candidatePoints = null
resultPoints = null
val mr = mappedRect ?: return
if (mr != lastMappedRect) {
removeCallbacks(onScanRunnable)
@@ -4,16 +4,18 @@ import android.annotation.SuppressLint
import android.content.Context
import android.graphics.*
import android.os.Build
import android.os.Parcel
import android.os.Parcelable
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import de.markusfisch.android.binaryeye.R
import de.markusfisch.android.binaryeye.app.prefs
import de.markusfisch.android.binaryeye.graphics.Candidates
import de.markusfisch.android.binaryeye.graphics.Dots
import de.markusfisch.android.binaryeye.graphics.getBitmapFromDrawable
import de.markusfisch.android.binaryeye.graphics.getDashedBorderPaint
import kotlin.math.abs
import kotlin.math.min
import kotlin.math.round
import kotlin.math.roundToInt
class DetectorView : View {
@@ -21,34 +23,42 @@ class DetectorView : View {
var updateRoi: (() -> Unit)? = null
private val candidates = Candidates(context)
private val dots = Dots(context)
private val invalidateRunnable: Runnable = Runnable {
marks = null
invalidate()
}
private val roiPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val roiPaint = context.getDashedBorderPaint()
private val handleBitmap = resources.getBitmapFromDrawable(
R.drawable.ic_crop_handle
R.drawable.button_crop
)
private val handleXRadius = handleBitmap.width / 2
private val handleYRadius = handleBitmap.height / 2
private val distToFull: Float
private val handleHome = Point()
private val handlePos = Point(-1, -1)
private val center = Point()
private val touchDown = Point()
private val distToFull: Int
private val minMoveThresholdSq: Int
private val cornerRadius: Int
private val fabHeight: Int
private val padding: Int
private var marks: List<Point>? = null
private var center = PointF()
private var handlePos = PointF()
private var orientation = resources.configuration.orientation
private var handleGrabbed = false
private var handleMoved = false
private var shadeColor = 0
init {
val dp = context.resources.displayMetrics.density
distToFull = 24f * dp
roiPaint.apply {
style = Paint.Style.STROKE
color = 0xffffffff.toInt()
strokeWidth = 2f * dp
pathEffect = DashPathEffect(floatArrayOf(10f, 20f), 0f)
}
distToFull = (24f * dp).roundToInt()
val minMoveThreshold = (8f * dp).roundToInt()
minMoveThresholdSq = minMoveThreshold * minMoveThreshold
cornerRadius = (8f * dp).roundToInt()
fabHeight = (92f * dp).roundToInt()
padding = (20f * dp).roundToInt()
isSaveEnabled = true
}
constructor(context: Context, attrs: AttributeSet) :
@@ -64,14 +74,46 @@ class DetectorView : View {
postDelayed(invalidateRunnable, 500)
}
override fun onSaveInstanceState(): Parcelable? {
if (!handleMoved) {
return super.onSaveInstanceState()
}
return SavedState(super.onSaveInstanceState()).apply {
savedHandlePos.set(handlePos)
savedOrientation = orientation
}
}
override fun onRestoreInstanceState(state: Parcelable) {
super.onRestoreInstanceState(
if (state is SavedState) {
if (state.savedOrientation == orientation) {
handlePos.set(state.savedHandlePos)
} else {
handlePos.set(
state.savedHandlePos.y,
state.savedHandlePos.x
)
}
handleMoved = true
state.superState
} else {
state
}
)
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent?): Boolean {
event ?: return super.onTouchEvent(event)
val x = event.x.roundToInt()
val y = event.y.roundToInt()
return when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
if (prefs.showCropHandle) {
handleGrabbed = abs(event.x - handlePos.x) < handleXRadius &&
abs(event.y - handlePos.y) < handleYRadius
touchDown.set(x, y)
handleGrabbed = abs(x - handlePos.x) < handleXRadius &&
abs(y - handlePos.y) < handleYRadius
handleGrabbed
} else {
false
@@ -79,7 +121,10 @@ class DetectorView : View {
}
MotionEvent.ACTION_MOVE -> {
if (handleGrabbed) {
handlePos.set(event.x, event.y)
handlePos.set(x, y)
if (distSq(handlePos, touchDown) > minMoveThresholdSq) {
handleMoved = true
}
invalidate()
true
} else {
@@ -88,14 +133,23 @@ class DetectorView : View {
}
MotionEvent.ACTION_CANCEL -> {
if (handleGrabbed) {
snap(event.x, event.y)
snap(x, y)
handleGrabbed = false
}
false
}
MotionEvent.ACTION_UP -> {
if (handleGrabbed) {
snap(event.x, event.y)
if (!handleMoved) {
handlePos.set(
(center.x * 1.5f).roundToInt(),
(center.y * 1.25f).roundToInt()
)
handleMoved = true
invalidate()
} else {
snap(x, y)
}
updateRoi?.invoke()
handleGrabbed = false
}
@@ -105,13 +159,12 @@ class DetectorView : View {
}
}
private fun snap(x: Float, y: Float) {
if (abs(x - center.x) < distToFull) {
handlePos.x = center.x
invalidate()
}
if (abs(y - center.y) < distToFull) {
handlePos.y = center.y
private fun snap(x: Int, y: Int) {
if (abs(x - center.x) < distToFull ||
abs(y - center.y) < distToFull
) {
handlePos.set(handleHome)
handleMoved = false
invalidate()
}
}
@@ -121,62 +174,153 @@ class DetectorView : View {
val width = right - left
val height = bottom - top
center.set(
(left + (width / 2)).toFloat(),
(top + (height / 2)).toFloat()
left + (width / 2),
top + (height / 2)
)
if (width > height) {
handlePos.set(round(right * .75f), center.y)
} else {
handlePos.set(center.x, round(bottom * .75f))
handleHome.set(
width - handleXRadius - paddingRight - padding,
height - handleYRadius - paddingBottom - fabHeight
)
if (handlePos.x < 0) {
handlePos.set(handleHome)
}
}
override fun onDraw(canvas: Canvas) {
canvas.drawColor(0, PorterDuff.Mode.CLEAR)
updateClipRect()
if (roi.height() > 0 && roi.width() > 0) {
// canvas.clipRect() doesn't work reliably below KITKAT
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
canvas.save()
canvas.clipOutRectCompat(roi)
canvas.drawColor(shadeColor, PorterDuff.Mode.SRC)
canvas.restore()
} else {
canvas.drawRect(roi, roiPaint)
}
if (handleMoved) {
drawClip(canvas)
}
marks?.let {
candidates.draw(canvas, it)
dots.draw(canvas, it)
}
if (prefs.showCropHandle) {
canvas.drawBitmap(
handleBitmap,
handlePos.x - handleXRadius,
handlePos.y - handleYRadius,
(handlePos.x - handleXRadius).toFloat(),
(handlePos.y - handleYRadius).toFloat(),
null
)
}
}
private fun updateClipRect() {
private fun drawClip(canvas: Canvas) {
val minDist = updateClipRect()
if (minDist < 1) {
return
}
// canvas.clipRect() doesn't work reliably below KITKAT
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
val radius = min(minDist / 2, cornerRadius).toFloat()
canvas.save()
canvas.clipOutPathCompat(
calculateRoundedRectPath(
roi.left.toFloat(),
roi.top.toFloat(),
roi.right.toFloat(),
roi.bottom.toFloat(),
radius,
radius
)
)
canvas.drawColor(shadeColor, PorterDuff.Mode.SRC)
canvas.restore()
} else {
canvas.drawRect(roi, roiPaint)
}
}
private fun updateClipRect(): Int {
val dx = abs(handlePos.x - center.x)
val dy = abs(handlePos.y - center.y)
val d = min(dx, dy)
shadeColor = (min(1f, d / distToFull) * 128f).toInt() shl 24
shadeColor = (min(
1f,
d.toFloat() / distToFull.toFloat()
) * 128f).toInt() shl 24
roi.set(
(center.x - dx).roundToInt(),
(center.y - dy).roundToInt(),
(center.x + dx).roundToInt(),
(center.y + dy).roundToInt()
center.x - dx,
center.y - dy,
center.x + dx,
center.y + dy
)
return d
}
internal class SavedState : BaseSavedState {
val savedHandlePos = Point()
var savedOrientation = 0
constructor(superState: Parcelable?) : super(superState)
private constructor(parcel: Parcel) : super(parcel) {
savedHandlePos.set(
parcel.readInt(),
parcel.readInt()
)
savedOrientation = parcel.readInt()
}
override fun writeToParcel(out: Parcel, flags: Int) {
super.writeToParcel(out, flags)
out.writeInt(savedHandlePos.x)
out.writeInt(savedHandlePos.y)
out.writeInt(savedOrientation)
}
companion object {
@JvmField
val CREATOR = object : Parcelable.Creator<SavedState> {
override fun createFromParcel(source: Parcel) = SavedState(source)
override fun newArray(size: Int): Array<SavedState?> = arrayOfNulls(size)
}
}
}
}
private fun Canvas.clipOutRectCompat(rect: Rect) {
private fun distSq(a: Point, b: Point): Int {
val dx = a.x - b.x
val dy = a.y - b.y
return dx * dx + dy * dy
}
private fun Point.set(point: Point) {
x = point.x
y = point.y
}
private fun Canvas.clipOutPathCompat(path: Path) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
clipOutRect(rect)
clipOutPath(path)
} else {
@Suppress("DEPRECATION")
clipRect(rect, Region.Op.DIFFERENCE)
clipPath(path, Region.Op.DIFFERENCE)
}
}
private fun calculateRoundedRectPath(
left: Float,
top: Float,
right: Float,
bottom: Float,
rx: Float,
ry: Float
): Path {
val width = right - left
val height = bottom - top
val widthMinusCorners = width - 2 * rx
val heightMinusCorners = height - 2 * ry
return Path().apply {
moveTo(right, top + ry)
rQuadTo(0f, -ry, -rx, -ry)
rLineTo(-widthMinusCorners, 0f)
rQuadTo(-rx, 0f, -rx, ry)
rLineTo(0f, heightMinusCorners)
rQuadTo(0f, ry, rx, ry)
rLineTo(widthMinusCorners, 0f)
rQuadTo(rx, 0f, rx, -ry)
rLineTo(0f, -heightMinusCorners)
close()
}
}
+1 -1
View File
@@ -38,7 +38,7 @@
<string name="switch_camera">Kamera wechseln</string>
<string name="history">Gespeicherte Codes</string>
<string name="preferences">Einstellungen</string>
<string name="show_crop_handle">Ausschnittseinschränker anzeigen</string>
<string name="show_crop_handle">Bereich verkleinern anzeigen</string>
<string name="zoom_by_swiping">Hoch/Runter streichen um zu zoomen</string>
<string name="auto_rotate">Vertikale Erkennung von 1D Barcodes</string>
<string name="try_harder">Suche länger und gründlicher nach Barcodes</string>
+5 -4
View File
@@ -8,6 +8,7 @@
<string name="binary_data">(data biner)</string>
<plurals name="barcode_info">
<item quantity="other">%1$s, %2$d karakter</item>
</plurals>
<string name="error_correction_level">Level koreksi kesalahan</string>
<string name="issue_number">Nomor pemindaian</string>
@@ -37,9 +38,9 @@
<string name="switch_camera">Ganti kamera</string>
<string name="history">Riwayat</string>
<string name="preferences">Preferensi</string>
<string name="show_crop_handle">Show cropping limiter</string>
<string name="show_crop_handle">Tampilkan pembatas pemotongan</string>
<string name="zoom_by_swiping">Perbesar kamera dengan menggeser ke atas/bawah</string>
<string name="auto_rotate">Recognize 1D barcodes vertically</string>
<string name="auto_rotate">Kenali barcode 1D secara vertikal</string>
<string name="try_harder">Optimalkan akurasi, bukan kecepatan</string>
<string name="vibrate">Bergetar saat mendeteksi</string>
<string name="use_history">Simpan riwayat pemindaian</string>
@@ -50,9 +51,9 @@
<string name="open_with_url">Buka data tak dikenal dengan URL</string>
<string name="really_remove_scan">Yakin menghapus pemindaian?</string>
<string name="really_remove_all_scans">Yakin menghapus semua pemindaian?</string>
<string name="really_remove_selected_scans">Really remove selected scans?</string>
<string name="really_remove_selected_scans">Yakin menghapus semua pemindaian?</string>
<string name="clear_history">Hapus riwayat</string>
<string name="copy_scan">Copy scan</string>
<string name="copy_scan">Salin pemindaian</string>
<string name="edit_scan">Sunting label</string>
<string name="remove_scan">Buang pindaian</string>
<string name="enter_name">Masukkan keterangan pemindaian</string>
+1 -1
View File
@@ -9,5 +9,5 @@
<color name="selected_row">#222</color>
<color name="separator">#333</color>
<color name="crop_bound">#fff</color>
<color name="candidate">#80b6d46f</color>
<color name="dot">#80b6d46f</color>
</resources>
+1 -1
View File
@@ -1,7 +1,7 @@
buildscript {
ext {
kotlin_version = '1.3.72'
tools_version = '3.6.3'
tools_version = '4.0.0'
build_tools_version = '29.0.3'
sdk_version = 29
support_version = '25.3.1'
+2 -2
View File
@@ -1,6 +1,6 @@
#Tue May 07 14:37:45 CEST 2019
#Sun May 31 13:11:56 CEST 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip

Before

Width:  |  Height:  |  Size: 885 B

After

Width:  |  Height:  |  Size: 885 B

-5
View File
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg height="48px" viewBox="0 0 48 48" width="48px" xmlns="http://www.w3.org/2000/svg">
<circle style="fill: rgb(182, 212, 111);" cx="24" cy="24" r="24"/>
<path d="M 29 27 L 31 27 L 31 19 C 31 17.9 30.1 17 29 17 L 21 17 L 21 19 L 29 19 L 29 27 Z M 19 29 L 19 13 L 17 13 L 17 17 L 13 17 L 13 19 L 17 19 L 17 29 C 17 30.1 17.9 31 19 31 L 29 31 L 29 35 L 31 35 L 31 31 L 35 31 L 35 29 L 19 29 Z" style="fill: rgb(255, 255, 255);"/>
</svg>

Before

Width:  |  Height:  |  Size: 478 B