Related
I need to create the pdf reader, I am sending images bitmap array to lazy compose view, it works fine when there is no zoom but with zoom the issue I am facing is that while scrolling to bottom it became too slow to scroll.
Following is the code
#OptIn(ExperimentalPagerApi::class, ExperimentalFoundationApi::class)
#Composable
fun ViewPager(
list: List<Pair<ImageBitmap, List<Fields>>>,
minScale: Float = 1f,
maxScale: Float = 3f,
scrollEnabled: MutableState<Boolean>,
isRotation: Boolean = false
) {
var targetScale by remember { mutableStateOf(1f) }
val scale = animateFloatAsState(targetValue = maxOf(minScale, minOf(maxScale, targetScale)))
var rotationState by remember { mutableStateOf(1f) }
var offsetX by remember { mutableStateOf(1f) }
var offsetY by remember { mutableStateOf(1f) }
val configuration = LocalConfiguration.current
val screenWidthPx = with(LocalDensity.current) { configuration.screenWidthDp.dp.toPx() }
val screenHeightPx = with(LocalDensity.current) { configuration.screenHeightDp.dp.toPx() }
val focusManager = LocalFocusManager.current
VerticalPager(
modifier= Modifier
.wrapContentSize()
// .verticalScroll(scrollState)
.fillMaxSize(1f)
.background(color = Color.Blue)
.combinedClickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = {
focusManager.clearFocus()
},
onDoubleClick = {
if (targetScale >= 2f) {
targetScale = 1f
offsetX = 1f
offsetY = 1f
scrollEnabled.value = true
} else targetScale = 3f
}
)
.graphicsLayer {
this.scaleX = scale.value
this.scaleY = scale.value
if (isRotation) {
rotationZ = rotationState
}
this.translationX = offsetX
this.translationY = offsetY
Log.e("Ramesh lazycolumn size", "${this.size.width} ${this.size.height}")
}
.pointerInput(Unit) {
forEachGesture {
awaitPointerEventScope {
awaitFirstDown()
do {
val event = awaitPointerEvent()
val zoom = event.calculateZoom()
targetScale *= zoom
val offset = event.calculatePan()
if (targetScale <= 1) {
offsetX = 1f
offsetY = 1f
targetScale = 1f
scrollEnabled.value = true
} else {
offsetX += offset.x
// todo change
if (offsetY + offset.y < screenHeightPx && offsetY + offset.y > -screenHeightPx) {
offsetY += offset.y
}
if (zoom > 1) {
scrollEnabled.value = false
rotationState += event.calculateRotation()
}
val imageWidth = screenWidthPx * scale.value
val borderReached =
imageWidth - screenWidthPx - 2 * abs(offsetX)
scrollEnabled.value = borderReached <= 0
if (borderReached < 0) {
offsetX =
((imageWidth - screenWidthPx) / 2f).withSign(offsetX)
if (offset.x != 0f) offsetY -= offset.y
}
}
Log.e("Ramesh lazycolumn xy ", "$offsetX $offsetY")
} while (event.changes.any { it.pressed })
}
}
},
count = list.size,
contentPadding = PaddingValues(0.dp),
flingBehavior = ScrollableDefaults.flingBehavior()
) { page ->
val it = list[page]
Page(
pageContent = {
PageContent(
modifier = Modifier,
imagePainter = BitmapPainter(it.first),
fields = it.second
)
},
fields = it.second,
color = if (page % 2 == 0) Color.Yellow else Color.Cyan
)
}
}
I have used the LazyColumn, Pager, Scrollable column but still same issue.
#Composable
fun Page(
modifier: Modifier = Modifier,
fields: List<Fields>,
pageContent: #Composable () -> Unit,
color: Color
) {
Box(
modifier = modifier
// Modifier.fillMaxHeight().background(color)
// .height(placeables[0].height)
) {
Layout(
content = pageContent
) { measurables, constraints ->
val placeables = measurables.map { measurable -> measurable.measure(constraints) }
Log.e(
"Ramesh",
"Placeable ${placeables[0].height} ${placeables[0].width} \nConstraints ${constraints.maxWidth}, ${constraints.maxHeight}"
)
val factor = placeables[0].height
layout(
width = constraints.maxWidth,
height = placeables[0].height
) {
placeables[0].place(0, 0, 0f)
for (index in 1..placeables.lastIndex) {
val fieldValue = fields[index - 1]
val offset = IntOffset(fieldValue.x, fieldValue.y - 70)
placeables[index].placeRelative(offset, 0f)
// placeables[index].placeRelative(fieldValue.x, fieldValue.y, 0f)
// placeables[index].place(fieldValue.x, fieldValue.y - 120, 0f)
}
}
}
}
}
#Composable
fun PageContent(
modifier: Modifier = Modifier,
fields: List<Fields>,
imagePainter: Painter
) {
Image(
painter = imagePainter,
contentDescription = null,
contentScale = ContentScale.Fit,
modifier = modifier
// .onGloballyPositioned {
// val position = it.positionInRoot()
// Log.e("ramesh image coordinates", "x = ${position.x} y = ${position.y}")
// Log.e("ramesh image size", "x = ${it.size.width} y = ${it.size.height}")
// }
)
// fields.forEach {
// InputField(it)
// }
}
Can anyone help me on this?
thanks
was wondering if anyone knows how to produce elliptical/arched list in Compose?
Something along these lines:
Not sure If I'm overlooking an 'easy' way of doing it in Compose. Cheers!
I have an article here that shows how to do this. It is not a LazyList in that it computes all the items (but only renders the visible ones); you can use this as a starting point to build upon.
The full code is below as well:
data class CircularListConfig(
val contentHeight: Float = 0f,
val numItems: Int = 0,
val visibleItems: Int = 0,
val circularFraction: Float = 1f,
val overshootItems: Int = 0,
)
#Stable
interface CircularListState {
val verticalOffset: Float
val firstVisibleItem: Int
val lastVisibleItem: Int
suspend fun snapTo(value: Float)
suspend fun decayTo(velocity: Float, value: Float)
suspend fun stop()
fun offsetFor(index: Int): IntOffset
fun setup(config: CircularListConfig)
}
class CircularListStateImpl(
currentOffset: Float = 0f,
) : CircularListState {
private val animatable = Animatable(currentOffset)
private var itemHeight = 0f
private var config = CircularListConfig()
private var initialOffset = 0f
private val decayAnimationSpec = FloatSpringSpec(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessLow,
)
private val minOffset: Float
get() = -(config.numItems - 1) * itemHeight
override val verticalOffset: Float
get() = animatable.value
override val firstVisibleItem: Int
get() = ((-verticalOffset - initialOffset) / itemHeight).toInt().coerceAtLeast(0)
override val lastVisibleItem: Int
get() = (((-verticalOffset - initialOffset) / itemHeight).toInt() + config.visibleItems)
.coerceAtMost(config.numItems - 1)
override suspend fun snapTo(value: Float) {
val minOvershoot = -(config.numItems - 1 + config.overshootItems) * itemHeight
val maxOvershoot = config.overshootItems * itemHeight
animatable.snapTo(value.coerceIn(minOvershoot, maxOvershoot))
}
override suspend fun decayTo(velocity: Float, value: Float) {
val constrainedValue = value.coerceIn(minOffset, 0f).absoluteValue
val remainder = (constrainedValue / itemHeight) - (constrainedValue / itemHeight).toInt()
val extra = if (remainder <= 0.5f) 0 else 1
val target =((constrainedValue / itemHeight).toInt() + extra) * itemHeight
animatable.animateTo(
targetValue = -target,
initialVelocity = velocity,
animationSpec = decayAnimationSpec,
)
}
override suspend fun stop() {
animatable.stop()
}
override fun setup(config: CircularListConfig) {
this.config = config
itemHeight = config.contentHeight / config.visibleItems
initialOffset = (config.contentHeight - itemHeight) / 2f
}
override fun offsetFor(index: Int): IntOffset {
val maxOffset = config.contentHeight / 2f + itemHeight / 2f
val y = (verticalOffset + initialOffset + index * itemHeight)
val deltaFromCenter = (y - initialOffset)
val radius = config.contentHeight / 2f
val scaledY = deltaFromCenter.absoluteValue * (config.contentHeight / 2f / maxOffset)
val x = if (scaledY < radius) {
sqrt((radius * radius - scaledY * scaledY))
} else {
0f
}
return IntOffset(
x = (x * config.circularFraction).roundToInt(),
y = y.roundToInt()
)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as CircularListStateImpl
if (animatable.value != other.animatable.value) return false
if (itemHeight != other.itemHeight) return false
if (config != other.config) return false
if (initialOffset != other.initialOffset) return false
if (decayAnimationSpec != other.decayAnimationSpec) return false
return true
}
override fun hashCode(): Int {
var result = animatable.value.hashCode()
result = 31 * result + itemHeight.hashCode()
result = 31 * result + config.hashCode()
result = 31 * result + initialOffset.hashCode()
result = 31 * result + decayAnimationSpec.hashCode()
return result
}
companion object {
val Saver = Saver<CircularListStateImpl, List<Any>>(
save = { listOf(it.verticalOffset) },
restore = {
CircularListStateImpl(it[0] as Float)
}
)
}
}
#Composable
fun rememberCircularListState(): CircularListState {
val state = rememberSaveable(saver = CircularListStateImpl.Saver) {
CircularListStateImpl()
}
return state
}
#Composable
fun CircularList(
visibleItems: Int,
modifier: Modifier = Modifier,
state: CircularListState = rememberCircularListState(),
circularFraction: Float = 1f,
overshootItems: Int = 3,
content: #Composable () -> Unit,
) {
check(visibleItems > 0) { "Visible items must be positive" }
check(circularFraction > 0f) { "Circular fraction must be positive" }
Layout(
modifier = modifier.clipToBounds().drag(state),
content = content,
) { measurables, constraints ->
val itemHeight = constraints.maxHeight / visibleItems
val itemConstraints = Constraints.fixed(width = constraints.maxWidth, height = itemHeight)
val placeables = measurables.map { measurable -> measurable.measure(itemConstraints) }
state.setup(
CircularListConfig(
contentHeight = constraints.maxHeight.toFloat(),
numItems = placeables.size,
visibleItems = visibleItems,
circularFraction = circularFraction,
overshootItems = overshootItems,
)
)
layout(
width = constraints.maxWidth,
height = constraints.maxHeight,
) {
for (i in state.firstVisibleItem..state.lastVisibleItem) {
placeables[i].placeRelative(state.offsetFor(i))
}
}
}
}
private fun Modifier.drag(
state: CircularListState,
) = pointerInput(Unit) {
val decay = splineBasedDecay<Float>(this)
coroutineScope {
while (true) {
val pointerId = awaitPointerEventScope { awaitFirstDown().id }
state.stop()
val tracker = VelocityTracker()
awaitPointerEventScope {
verticalDrag(pointerId) { change ->
val verticalDragOffset = state.verticalOffset + change.positionChange().y
launch {
state.snapTo(verticalDragOffset)
}
tracker.addPosition(change.uptimeMillis, change.position)
change.consumePositionChange()
}
}
val velocity = tracker.calculateVelocity().y
val targetValue = decay.calculateTargetValue(state.verticalOffset, velocity)
launch {
state.decayTo(velocity, targetValue)
}
}
}
}
I have scalable image view - it can be zoomed or scrolled. In this image I able to set points with pin. These points are zoomable and scrollable too with the image. But also I need to draw several circles - the path between pin A and pin B. This path should be zoomable and scrollable too. The color of each circle can be various too. Example:
I creating pins here:
override fun onLongPress(e: MotionEvent) {
super.onLongPress(e)
pinsCoordinates.add(CoordinatesEntity(e.x, e.y))
val touchCoords = floatArrayOf(e.x, e.y)
val matrixInverse = Matrix()
mMatrix!!.invert(matrixInverse) // XY to UV mapping matrix.
matrixInverse.mapPoints(touchCoords) // Touch point in bitmap-U,V coords.
val entity = CoordinatesEntity(touchCoords[0], touchCoords[1])
invertedPinsCoordinates.add(entity)
pinCountersMap["${entity.x}${entity.y}"] = (++pinCounter).toString()
RxBus.publish(IndoorPinsCountChanged(pinsCoordinates.size))
invalidate()
}
And drawing here:
private fun getPinForCoordinates(coordinates: CoordinatesEntity): Bitmap {
val paint = Paint()
paint.style = Paint.Style.FILL
paint.color = Color.parseColor("#1417BF")
val text = pinCountersMap["${coordinates.x}${coordinates.y}"] ?: ""
val copiedBitmapPin = pinBitmap.copy(pinBitmap.config, pinBitmap.isMutable)
val canvas = Canvas(copiedBitmapPin)
val xStart = when (text.length) {
1 -> copiedBitmapPin.width / 2f - 8
2 -> copiedBitmapPin.width / 2f - 12
else -> copiedBitmapPin.width / 2f - 36
}
val yStart = when (text.length) {
1 -> copiedBitmapPin.height / 2f - 2
else -> copiedBitmapPin.height / 2f - 5
}
paint.textSize = when (text.length) {
1 -> 30f
else -> 20f
}
canvas.drawText(text, xStart, yStart, paint)
return BitmapDrawable(context.resources, copiedBitmapPin).bitmap
}
#SuppressLint("DrawAllocation")
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
invertedPinsCoordinates.forEach {
val marker = getPinForCoordinates(it)
val matrixMarker = Matrix()
matrixMarker.setTranslate(it.x, it.y)
matrixMarker.postConcat(mMatrix)
canvas.drawBitmap(marker, matrixMarker, null)
}
}
My whole class:
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.*
import android.graphics.drawable.BitmapDrawable
import android.util.AttributeSet
import android.util.Log
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import android.widget.Toast
import androidx.appcompat.widget.AppCompatImageView
import com.signalsense.signalsenseapp.R
import com.signalsense.signalsenseapp.mvp.models.entities.CoordinatesEntity
import com.signalsense.signalsenseapp.rxbus.RxBus
import com.signalsense.signalsenseapp.rxbus.events.IndoorPinsCountChanged
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sqrt
class ScaleImageView : AppCompatImageView, View.OnTouchListener {
private val dirtyRect = RectF()
private val savedContext: Context
private val maxScale = 2f
private val matrixValues = FloatArray(9)
private var lastTouchX = 0f
private var lastTouchY = 0f
private var paint = Paint()
var tag = "ScaleImageView"
// display width height.
private var mWidth = 0
private var mHeight = 0
private var mIntrinsicWidth = 0
private var mIntrinsicHeight = 0
private var mScale = 0f
private var mMinScale = 1f
private var mPrevDistance = 0f
private var isScaling = false
private var mPrevMoveX = 0
private var mPrevMoveY = 0
private var mDetector: GestureDetector? = null
private val pinsCoordinates = ArrayList<CoordinatesEntity>()
private var invertedPinsCoordinates = ArrayList<CoordinatesEntity>()
private val pinCountersMap = HashMap<String, String>()
private var pinCounter = 0
private lateinit var pinBitmap: Bitmap
constructor(context: Context, attr: AttributeSet?) : super(context, attr) {
savedContext = context
initialize()
}
constructor(context: Context) : super(context) {
savedContext = context
initialize()
}
private fun resetDirtyRect(eventX: Float, eventY: Float) {
dirtyRect.left = min(lastTouchX, eventX)
dirtyRect.right = max(lastTouchX, eventX)
dirtyRect.top = min(lastTouchY, eventY)
dirtyRect.bottom = max(lastTouchY, eventY)
}
override fun setImageBitmap(bm: Bitmap) {
super.setImageBitmap(bm)
initialize()
}
override fun setImageResource(resId: Int) {
super.setImageResource(resId)
initialize()
}
private fun initialize() {
this.scaleType = ScaleType.MATRIX
mMatrix = Matrix()
val d = drawable
paint.isAntiAlias = true
paint.color = Color.RED
paint.style = Paint.Style.STROKE
paint.strokeJoin = Paint.Join.ROUND
paint.strokeWidth = STROKE_WIDTH
if (d != null) {
mIntrinsicWidth = d.intrinsicWidth
mIntrinsicHeight = d.intrinsicHeight
setOnTouchListener(this)
}
mDetector = GestureDetector(savedContext,
object : GestureDetector.SimpleOnGestureListener() {
override fun onDoubleTap(e: MotionEvent): Boolean {
maxZoomTo(e.x.toInt(), e.y.toInt())
cutting()
return super.onDoubleTap(e)
}
override fun onLongPress(e: MotionEvent) {
super.onLongPress(e)
pinsCoordinates.add(CoordinatesEntity(e.x, e.y))
val touchCoords = floatArrayOf(e.x, e.y)
val matrixInverse = Matrix()
mMatrix!!.invert(matrixInverse) // XY to UV mapping matrix.
matrixInverse.mapPoints(touchCoords) // Touch point in bitmap-U,V coords.
val entity = CoordinatesEntity(touchCoords[0], touchCoords[1])
invertedPinsCoordinates.add(entity)
pinCountersMap["${entity.x}${entity.y}"] = (++pinCounter).toString()
RxBus.publish(IndoorPinsCountChanged(pinsCoordinates.size))
invalidate()
}
override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
val delta = 25
val pinIndex = pinsCoordinates.indexOfFirst {
val itXStart = it.x - delta
val itXEnd = it.x + delta
val itYStart = it.y - delta
val itYEnd = it.y + delta
val tapX = e?.x ?: 0f
val tapY = e?.y ?: 0f
(tapX in itXStart..itXEnd) && (tapY in itYStart..itYEnd)
}
if (pinIndex >= 0) Toast.makeText(context, pinIndex.toString(), Toast.LENGTH_SHORT).show()
return super.onSingleTapConfirmed(e)
}
})
pinBitmap = BitmapFactory.decodeResource(
context.resources, R.drawable.ic_pin_blue_no_middle_32
).copy(Bitmap.Config.ARGB_8888, true)
}
override fun setFrame(l: Int, t: Int, r: Int, b: Int): Boolean {
mWidth = r - l
mHeight = b - t
mMatrix!!.reset()
val rNorm = r - l
mScale = rNorm.toFloat() / mIntrinsicWidth.toFloat()
var paddingHeight = 0
var paddingWidth = 0
// scaling vertical
if (mScale * mIntrinsicHeight > mHeight) {
mScale = mHeight.toFloat() / mIntrinsicHeight.toFloat()
mMatrix!!.postScale(mScale, mScale)
paddingWidth = (r - mWidth) / 2
paddingHeight = 0
// scaling horizontal
} else {
mMatrix!!.postScale(mScale, mScale)
paddingHeight = (b - mHeight) / 2
paddingWidth = 0
}
mMatrix!!.postTranslate(paddingWidth.toFloat(), paddingHeight.toFloat())
imageMatrix = mMatrix
mMinScale = mScale
Log.i(tag, "MinScale: $mMinScale")
zoomTo(mScale, mWidth / 2, mHeight / 2)
cutting()
return super.setFrame(l, t, r, b)
}
fun deleteLastPin() {
if (pinsCoordinates.isNotEmpty()) pinsCoordinates.removeLast()
if (invertedPinsCoordinates.isNotEmpty()) invertedPinsCoordinates.removeLast()
RxBus.publish(IndoorPinsCountChanged(pinsCoordinates.size))
pinCounter--
invalidate()
}
private fun getValue(matrix: Matrix?, whichValue: Int): Float {
matrix!!.getValues(matrixValues)
return matrixValues[whichValue]
}
private val scale: Float
get() = getValue(mMatrix, Matrix.MSCALE_X)
private val translateX: Float
get() = getValue(mMatrix, Matrix.MTRANS_X)
private val translateY: Float
get() = getValue(mMatrix, Matrix.MTRANS_Y)
private fun maxZoomTo(x: Int, y: Int) {
if (mMinScale != getCalculatedScale() && getCalculatedScale() - mMinScale > 0.1f) {
// threshold 0.1f
val scale = mMinScale / getCalculatedScale()
zoomTo(scale, x, y)
} else {
val scale = maxScale / getCalculatedScale()
zoomTo(scale, x, y)
}
}
private fun zoomTo(scale: Float, x: Int, y: Int) {
if (getCalculatedScale() * scale < mMinScale) {
return
}
if (scale >= 1 && getCalculatedScale() * scale > maxScale) {
return
}
Log.i(tag, "Scale: $scale, multiplied: ${scale * scale}")
mMatrix!!.postScale(scale, scale)
// move to center
mMatrix!!.postTranslate(-(mWidth * scale - mWidth) / 2,
-(mHeight * scale - mHeight) / 2)
// move x and y distance
mMatrix!!.postTranslate(-(x - mWidth / 2) * scale, 0f)
mMatrix!!.postTranslate(0f, -(y - mHeight / 2) * scale)
imageMatrix = mMatrix
}
private fun cutting() {
val width = (mIntrinsicWidth * getCalculatedScale()).toInt()
val height = (mIntrinsicHeight * getCalculatedScale()).toInt()
imageWidth = width
imageHeight = height
if (translateX < -(width - mWidth)) {
mMatrix!!.postTranslate(-(translateX + width - mWidth), 0f)
}
if (translateX > 0) {
mMatrix!!.postTranslate(-translateX, 0f)
}
if (translateY < -(height - mHeight)) {
mMatrix!!.postTranslate(0f, -(translateY + height - mHeight))
}
if (translateY > 0) {
mMatrix!!.postTranslate(0f, -translateY)
}
if (width < mWidth) {
mMatrix!!.postTranslate(((mWidth - width) / 2).toFloat(), 0f)
}
if (height < mHeight) {
mMatrix!!.postTranslate(0f, ((mHeight - height) / 2).toFloat())
}
imageMatrix = mMatrix
}
private fun distance(x0: Float, x1: Float, y0: Float, y1: Float): Float {
val x = x0 - x1
val y = y0 - y1
return sqrt((x * x + y * y).toDouble()).toFloat()
}
private fun dispDistance(): Float {
return sqrt((mWidth * mWidth + mHeight * mHeight).toDouble()).toFloat()
}
fun clear() {
path.reset()
invalidate()
}
fun save() {
val returnedBitmap = Bitmap.createBitmap(
width,
height,
Bitmap.Config.ARGB_8888)
val canvas = Canvas(returnedBitmap)
draw(canvas)
setImageBitmap(returnedBitmap)
}
#Suppress("DEPRECATION")
#SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
if (mDetector!!.onTouchEvent(event)) {
return true
}
val touchCount = event.pointerCount
when (event.action) {
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_1_DOWN, MotionEvent.ACTION_POINTER_2_DOWN -> {
if (touchCount >= 2) {
val distance = distance(event.getX(0), event.getX(1),
event.getY(0), event.getY(1))
mPrevDistance = distance
isScaling = true
} else {
mPrevMoveX = event.x.toInt()
mPrevMoveY = event.y.toInt()
}
if (touchCount >= 2 && isScaling) {
val dist = distance(event.getX(0), event.getX(1),
event.getY(0), event.getY(1))
var scale = (dist - mPrevDistance) / dispDistance()
mPrevDistance = dist
scale += 1f
scale *= scale
zoomTo(scale, mWidth / 2, mHeight / 2)
cutting()
} else if (!isScaling) {
val distanceX = mPrevMoveX - event.x.toInt()
val distanceY = mPrevMoveY - event.y.toInt()
mPrevMoveX = event.x.toInt()
mPrevMoveY = event.y.toInt()
mMatrix!!.postTranslate(-distanceX.toFloat(), -distanceY.toFloat())
cutting()
}
}
MotionEvent.ACTION_MOVE -> if (touchCount >= 2 && isScaling) {
val dist = distance(event.getX(0), event.getX(1),
event.getY(0), event.getY(1))
var scale = (dist - mPrevDistance) / dispDistance()
mPrevDistance = dist
scale += 1f
scale *= scale
zoomTo(scale, mWidth / 2, mHeight / 2)
cutting()
} else if (!isScaling) {
val distanceX = mPrevMoveX - event.x.toInt()
val distanceY = mPrevMoveY - event.y.toInt()
mPrevMoveX = event.x.toInt()
mPrevMoveY = event.y.toInt()
mMatrix!!.postTranslate(-distanceX.toFloat(), -distanceY.toFloat())
cutting()
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP, MotionEvent.ACTION_POINTER_2_UP -> if (event.pointerCount <= 1) {
isScaling = false
}
}
return true
}
private fun getCalculatedScale() = getValue(mMatrix, Matrix.MSCALE_X)
private fun getPinForCoordinates(coordinates: CoordinatesEntity): Bitmap {
val paint = Paint()
paint.style = Paint.Style.FILL
paint.color = Color.parseColor("#1417BF")
val text = pinCountersMap["${coordinates.x}${coordinates.y}"] ?: ""
val copiedBitmapPin = pinBitmap.copy(pinBitmap.config, pinBitmap.isMutable)
val canvas = Canvas(copiedBitmapPin)
val xStart = when (text.length) {
1 -> copiedBitmapPin.width / 2f - 8
2 -> copiedBitmapPin.width / 2f - 12
else -> copiedBitmapPin.width / 2f - 36
}
val yStart = when (text.length) {
1 -> copiedBitmapPin.height / 2f - 2
else -> copiedBitmapPin.height / 2f - 5
}
paint.textSize = when (text.length) {
1 -> 30f
else -> 20f
}
canvas.drawText(text, xStart, yStart, paint)
return BitmapDrawable(context.resources, copiedBitmapPin).bitmap
}
#SuppressLint("DrawAllocation")
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
invertedPinsCoordinates.forEach {
val marker = getPinForCoordinates(it)
val matrixMarker = Matrix()
matrixMarker.setTranslate(it.x, it.y)
matrixMarker.postConcat(mMatrix)
canvas.drawBitmap(marker, matrixMarker, null)
}
}
#SuppressLint("ClickableViewAccessibility")
override fun onTouch(v: View, event: MotionEvent): Boolean {
return super.onTouchEvent(event)
}
companion object {
const val STROKE_WIDTH = 10f
const val HALF_STROKE_WIDTH = STROKE_WIDTH / 2
var path = Path()
var imageHeight = 0
var imageWidth = 0
private var mMatrix: Matrix? = null
}
}
How to create this path from circles? And make it scrollable and zoomable with other elements (picture and pins, that alreasy scrollable and zoomable)? Please help!
If anybody interested, I found the solution. This is the algorithm to get middle points between poina a and point b:
private fun getPointsToDraw(a: CoordinatesEntity, b: CoordinatesEntity): List<CoordinatesEntity> {
val points = ArrayList<CoordinatesEntity>()
val numberOfPoints = 5
val stepX = (b.x - a.x) / numberOfPoints
val stepY = (b.y - a.y) / numberOfPoints
points.add(a)
for (i in 0 until numberOfPoints) {
val lastPoint = points.last()
points.add(CoordinatesEntity(lastPoint.x + stepX, lastPoint.y + stepY))
}
points.add(b)
return points
}
data class:
data class CoordinatesEntity(var x: Float, var y: Float)
Of course we can get only fixed points between, cause there is indefinite count.
Here is how to write them. I using prepared bitmaps in array (here is I took 4th index from this bitmap):
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
invertedPinsCoordinates.forEach {
val marker = getPinForCoordinates(it)
val matrixMarker = Matrix()
matrixMarker.setTranslate(it.x, it.y)
matrixMarker.postConcat(mMatrix)
canvas.drawBitmap(marker, matrixMarker, null)
}
//Starting to draw path with points
if(invertedPinsCoordinates.size > 1) {
val points = getPointsToDraw(invertedPinsCoordinates[invertedPinsCoordinates.size - 2],
invertedPinsCoordinates.last())
points.forEach {
val marker = levelsBitmapArray[4]
val matrixMarker = Matrix()
matrixMarker.setTranslate(it.x, it.y)
matrixMarker.postConcat(mMatrix)
canvas.drawBitmap(marker, matrixMarker, null)
}
}
}
I want to add to the image several pins at once. This image can zoom and scroll. But when I'm adding many pins (10 and more), the image starting to freezing when I zooming or scrolling it.
I'm adding new pin in "onLongPress":
data class CoordinatesEntity(var x: Float, var y: Float)
class ScaleImageView : AppCompatImageView, View.OnTouchListener {
...
private val pinsCoordinates = ArrayList<CoordinatesEntity>()
private var invertedPinsCoordinates = ArrayList<CoordinatesEntity>()
private val pinCountersMap = HashMap<String, String>()
...
mDetector = GestureDetector(savedContext,
object : GestureDetector.SimpleOnGestureListener() {
override fun onDoubleTap(e: MotionEvent): Boolean {
...
}
override fun onLongPress(e: MotionEvent) {
super.onLongPress(e)
pinsCoordinates.add(CoordinatesEntity(e.x, e.y))
val touchCoords = floatArrayOf(e.x, e.y)
val matrixInverse = Matrix()
mMatrix!!.invert(matrixInverse) // XY to UV mapping matrix.
matrixInverse.mapPoints(touchCoords) // Touch point in bitmap-U,V coords.
val entity = CoordinatesEntity(touchCoords[0], touchCoords[1])
invertedPinsCoordinates.add(entity)
pinCountersMap["${entity.x}${entity.y}"] = (++pinCounter).toString()
invalidate()
}
})
...
}
and drawing all of my pins in "onDraw". My pin - it's image plus text, that I adding programmatically to bitmap with pin:
class ScaleImageView : AppCompatImageView, View.OnTouchListener {
...
private fun getPinForCoordinates(coordinates: CoordinatesEntity): Bitmap {
val bm: Bitmap = BitmapFactory.decodeResource(
context.resources, R.drawable.ic_pin_raster_no_middle_32
).copy(Bitmap.Config.ARGB_8888, true)
val paint = Paint()
paint.style = Paint.Style.FILL
paint.color = Color.parseColor("#3874A4")
paint.textSize = 20f
val text = pinCountersMap["${coordinates.x}${coordinates.y}"] ?: ""
val canvas = Canvas(bm)
val xStart = when (text.length) {
1 -> bm.width / 2f - 6
2 -> bm.width / 2f - 12
else -> bm.width / 2f - 36
}
canvas.drawText(text, xStart, bm.height / 2f, paint)
return BitmapDrawable(context.resources, bm).bitmap
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
invertedPinsCoordinates.forEach {
val marker = getPinForCoordinates(it)
val matrixMarker = Matrix()
matrixMarker.setTranslate(it.x, it.y)
matrixMarker.postConcat(mMatrix)
canvas.drawBitmap(marker, matrixMarker, null)
}
}
...
}
I think it's freezing because of the loop inside of "onDraw". But I don't know how to draw several pins at once and keep them zoomable and scrollable with image. Please help!
My whole class:
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.*
import android.graphics.drawable.BitmapDrawable
import android.util.AttributeSet
import android.util.Log
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import androidx.appcompat.widget.AppCompatImageView
import com.signalsense.signalsenseapp.R
import com.signalsense.signalsenseapp.interactors.image_interactor.ImageInteractor
import com.signalsense.signalsenseapp.interactors.image_interactor.ImageInteractorImpl
import com.signalsense.signalsenseapp.mvp.models.entities.CoordinatesEntity
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sqrt
class ScaleImageView : AppCompatImageView, View.OnTouchListener {
private val dirtyRect = RectF()
private val savedContext: Context
private val maxScale = 2f
private val matrixValues = FloatArray(9)
private var lastTouchX = 0f
private var lastTouchY = 0f
private var paint = Paint()
var tag = "ScaleImageView"
// display width height.
private var mWidth = 0
private var mHeight = 0
private var mIntrinsicWidth = 0
private var mIntrinsicHeight = 0
private var mScale = 0f
private var mMinScale = 1f
private var mPrevDistance = 0f
private var isScaling = false
private var mPrevMoveX = 0
private var mPrevMoveY = 0
private var mDetector: GestureDetector? = null
private val pinsCoordinates = ArrayList<CoordinatesEntity>()
private var invertedPinsCoordinates = ArrayList<CoordinatesEntity>()
private var pinCounter = 0
private val pinCountersMap = HashMap<String, String>()
constructor(context: Context, attr: AttributeSet?) : super(context, attr) {
savedContext = context
initialize()
}
constructor(context: Context) : super(context) {
savedContext = context
initialize()
}
private fun resetDirtyRect(eventX: Float, eventY: Float) {
dirtyRect.left = min(lastTouchX, eventX)
dirtyRect.right = max(lastTouchX, eventX)
dirtyRect.top = min(lastTouchY, eventY)
dirtyRect.bottom = max(lastTouchY, eventY)
}
override fun setImageBitmap(bm: Bitmap) {
super.setImageBitmap(bm)
initialize()
}
override fun setImageResource(resId: Int) {
super.setImageResource(resId)
initialize()
}
private fun initialize() {
this.scaleType = ScaleType.MATRIX
mMatrix = Matrix()
val d = drawable
paint.isAntiAlias = true
paint.color = Color.RED
paint.style = Paint.Style.STROKE
paint.strokeJoin = Paint.Join.ROUND
paint.strokeWidth = STROKE_WIDTH
if (d != null) {
mIntrinsicWidth = d.intrinsicWidth
mIntrinsicHeight = d.intrinsicHeight
setOnTouchListener(this)
}
mDetector = GestureDetector(savedContext,
object : GestureDetector.SimpleOnGestureListener() {
override fun onDoubleTap(e: MotionEvent): Boolean {
maxZoomTo(e.x.toInt(), e.y.toInt())
cutting()
return super.onDoubleTap(e)
}
override fun onLongPress(e: MotionEvent) {
super.onLongPress(e)
pinsCoordinates.add(CoordinatesEntity(e.x, e.y))
val touchCoords = floatArrayOf(e.x, e.y)
val matrixInverse = Matrix()
mMatrix!!.invert(matrixInverse) // XY to UV mapping matrix.
matrixInverse.mapPoints(touchCoords) // Touch point in bitmap-U,V coords.
val entity = CoordinatesEntity(touchCoords[0], touchCoords[1])
invertedPinsCoordinates.add(entity)
pinCountersMap["${entity.x}${entity.y}"] = (++pinCounter).toString()
invalidate()
}
})
}
override fun setFrame(l: Int, t: Int, r: Int, b: Int): Boolean {
mWidth = r - l
mHeight = b - t
mMatrix!!.reset()
val rNorm = r - l
mScale = rNorm.toFloat() / mIntrinsicWidth.toFloat()
var paddingHeight = 0
var paddingWidth = 0
// scaling vertical
if (mScale * mIntrinsicHeight > mHeight) {
mScale = mHeight.toFloat() / mIntrinsicHeight.toFloat()
mMatrix!!.postScale(mScale, mScale)
paddingWidth = (r - mWidth) / 2
paddingHeight = 0
// scaling horizontal
} else {
mMatrix!!.postScale(mScale, mScale)
paddingHeight = (b - mHeight) / 2
paddingWidth = 0
}
mMatrix!!.postTranslate(paddingWidth.toFloat(), paddingHeight.toFloat())
imageMatrix = mMatrix
mMinScale = mScale
Log.i(tag, "MinScale: $mMinScale")
zoomTo(mScale, mWidth / 2, mHeight / 2)
cutting()
return super.setFrame(l, t, r, b)
}
private fun getValue(matrix: Matrix?, whichValue: Int): Float {
matrix!!.getValues(matrixValues)
return matrixValues[whichValue]
}
private val scale: Float
get() = getValue(mMatrix, Matrix.MSCALE_X)
private val translateX: Float
get() = getValue(mMatrix, Matrix.MTRANS_X)
private val translateY: Float
get() = getValue(mMatrix, Matrix.MTRANS_Y)
private fun maxZoomTo(x: Int, y: Int) {
if (mMinScale != getCalculatedScale() && getCalculatedScale() - mMinScale > 0.1f) {
// threshold 0.1f
val scale = mMinScale / getCalculatedScale()
zoomTo(scale, x, y)
} else {
val scale = maxScale / getCalculatedScale()
zoomTo(scale, x, y)
}
}
private fun zoomTo(scale: Float, x: Int, y: Int) {
if (getCalculatedScale() * scale < mMinScale) {
return
}
if (scale >= 1 && getCalculatedScale() * scale > maxScale) {
return
}
Log.i(tag, "Scale: $scale, multiplied: ${scale * scale}")
mMatrix!!.postScale(scale, scale)
// move to center
mMatrix!!.postTranslate(-(mWidth * scale - mWidth) / 2,
-(mHeight * scale - mHeight) / 2)
// move x and y distance
mMatrix!!.postTranslate(-(x - mWidth / 2) * scale, 0f)
mMatrix!!.postTranslate(0f, -(y - mHeight / 2) * scale)
imageMatrix = mMatrix
}
private fun cutting() {
val width = (mIntrinsicWidth * getCalculatedScale()).toInt()
val height = (mIntrinsicHeight * getCalculatedScale()).toInt()
imageWidth = width
imageHeight = height
if (translateX < -(width - mWidth)) {
mMatrix!!.postTranslate(-(translateX + width - mWidth), 0f)
}
if (translateX > 0) {
mMatrix!!.postTranslate(-translateX, 0f)
}
if (translateY < -(height - mHeight)) {
mMatrix!!.postTranslate(0f, -(translateY + height - mHeight))
}
if (translateY > 0) {
mMatrix!!.postTranslate(0f, -translateY)
}
if (width < mWidth) {
mMatrix!!.postTranslate(((mWidth - width) / 2).toFloat(), 0f)
}
if (height < mHeight) {
mMatrix!!.postTranslate(0f, ((mHeight - height) / 2).toFloat())
}
imageMatrix = mMatrix
}
private fun distance(x0: Float, x1: Float, y0: Float, y1: Float): Float {
val x = x0 - x1
val y = y0 - y1
return sqrt((x * x + y * y).toDouble()).toFloat()
}
private fun dispDistance(): Float {
return sqrt((mWidth * mWidth + mHeight * mHeight).toDouble()).toFloat()
}
fun clear() {
path.reset()
invalidate()
}
fun save() {
val returnedBitmap = Bitmap.createBitmap(
width,
height,
Bitmap.Config.ARGB_8888)
val canvas = Canvas(returnedBitmap)
draw(canvas)
setImageBitmap(returnedBitmap)
}
#Suppress("DEPRECATION")
#SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
if (mDetector!!.onTouchEvent(event)) {
return true
}
val touchCount = event.pointerCount
when (event.action) {
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_1_DOWN, MotionEvent.ACTION_POINTER_2_DOWN -> {
if (touchCount >= 2) {
val distance = distance(event.getX(0), event.getX(1),
event.getY(0), event.getY(1))
mPrevDistance = distance
isScaling = true
} else {
mPrevMoveX = event.x.toInt()
mPrevMoveY = event.y.toInt()
}
if (touchCount >= 2 && isScaling) {
val dist = distance(event.getX(0), event.getX(1),
event.getY(0), event.getY(1))
var scale = (dist - mPrevDistance) / dispDistance()
mPrevDistance = dist
scale += 1f
scale *= scale
zoomTo(scale, mWidth / 2, mHeight / 2)
cutting()
} else if (!isScaling) {
val distanceX = mPrevMoveX - event.x.toInt()
val distanceY = mPrevMoveY - event.y.toInt()
mPrevMoveX = event.x.toInt()
mPrevMoveY = event.y.toInt()
mMatrix!!.postTranslate(-distanceX.toFloat(), -distanceY.toFloat())
cutting()
}
}
MotionEvent.ACTION_MOVE -> if (touchCount >= 2 && isScaling) {
val dist = distance(event.getX(0), event.getX(1),
event.getY(0), event.getY(1))
var scale = (dist - mPrevDistance) / dispDistance()
mPrevDistance = dist
scale += 1f
scale *= scale
zoomTo(scale, mWidth / 2, mHeight / 2)
cutting()
} else if (!isScaling) {
val distanceX = mPrevMoveX - event.x.toInt()
val distanceY = mPrevMoveY - event.y.toInt()
mPrevMoveX = event.x.toInt()
mPrevMoveY = event.y.toInt()
mMatrix!!.postTranslate(-distanceX.toFloat(), -distanceY.toFloat())
cutting()
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP, MotionEvent.ACTION_POINTER_2_UP -> if (event.pointerCount <= 1) {
isScaling = false
}
}
return true
}
private fun getCalculatedScale() = getValue(mMatrix, Matrix.MSCALE_X)
private fun getPinForCoordinates(coordinates: CoordinatesEntity): Bitmap {
val bm: Bitmap = BitmapFactory.decodeResource(
context.resources, R.drawable.ic_pin_raster_no_middle_32
).copy(Bitmap.Config.ARGB_8888, true)
val paint = Paint()
paint.style = Paint.Style.FILL
paint.color = Color.parseColor("#3874A4")
paint.textSize = 20f
val text = pinCountersMap["${coordinates.x}${coordinates.y}"] ?: ""
val canvas = Canvas(bm)
val xStart = when (text.length) {
1 -> bm.width / 2f - 6
2 -> bm.width / 2f - 12
else -> bm.width / 2f - 36
}
canvas.drawText(text, xStart, bm.height / 2f, paint)
return BitmapDrawable(context.resources, bm).bitmap
}
#SuppressLint("DrawAllocation")
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
invertedPinsCoordinates.forEach {
val marker = getPinForCoordinates(it)
val matrixMarker = Matrix()
matrixMarker.setTranslate(it.x, it.y)
matrixMarker.postConcat(mMatrix)
canvas.drawBitmap(marker, matrixMarker, null)
}
}
#SuppressLint("ClickableViewAccessibility")
override fun onTouch(v: View, event: MotionEvent): Boolean {
return super.onTouchEvent(event)
}
companion object {
const val STROKE_WIDTH = 10f
const val HALF_STROKE_WIDTH = STROKE_WIDTH / 2
var path = Path()
var imageHeight = 0
var imageWidth = 0
private var mMatrix: Matrix? = null
}
}
I understand - the reason not in the loop mainly, but in "BitmapFactory.decodeResource". Here is we're reading the file from the disk - it's long operation and if we keep it in the memory, without decoding from resources again and again, everything will be ok.
I have successfully achieved saving to the gallery after I finish drawing on canvas, but now I need to open an image from galley ( for example my old drawing) and use it as a background, after some searching there no answer which can help me. I already started some attempts to do this, but any advice or solutions how I can achieve it will be great.
There is my custom view with Canvas:
(Permission for READ and WRITE is already granted and handled in another class)
var drawingColor: Int = ResourcesCompat.getColor(resources, R.color.colorBlack, null)
var strokeDrawWidth: Float = 12f
private var path = Path()
private val paths = ArrayList<Triple<Path, Int, Float>>()
private val undonePaths = ArrayList<Triple<Path, Int, Float>>()
private val extraCanvas: Canvas? = null
private var bitmapBackground: Bitmap? = null
private var motionTouchEventX = 0f
private var motionTouchEventY = 0f
private var currentX = 0f
private var currentY = 0f
private val touchTolerance = ViewConfiguration.get(context).scaledTouchSlop
private val paint = Paint().apply {
color = drawingColor
isAntiAlias = true
isDither = true
style = Paint.Style.STROKE
strokeJoin = Paint.Join.ROUND
strokeCap = Paint.Cap.ROUND
strokeWidth = strokeDrawWidth
}
fun loadCanvasBackground(bitmap: Bitmap) {
bitmapBackground = bitmap
invalidate()
}
fun saveCanvasDrawing() {
canvasCustomView.isDrawingCacheEnabled = true
val extraBitmap: Bitmap = canvasCustomView.drawingCache
MediaStore.Images.Media.insertImage(context.contentResolver, extraBitmap, "drawing", "Paint R")
}
fun resetCanvasDrawing() {
path.reset()
paths.clear()
invalidate()
}
fun undoCanvasDrawing() {
if (paths.size > 0) {
undonePaths.add(paths.removeAt(paths.size - 1))
invalidate()
} else {
Log.d("UNDO_ERROR", "Something went wrong with UNDO action")
}
}
fun redoCanvasDrawing() {
if (undonePaths.size > 0) {
paths.add(undonePaths.removeAt(undonePaths.size - 1))
invalidate()
} else {
Log.d("REDO_ERROR", "Something went wrong with REDO action")
}
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
if (bitmapBackground != null) {
extraCanvas?.drawBitmap(bitmapBackground!!, 0f, 0f, paint)
}
for (p in paths) {
paint.strokeWidth = p.third
paint.color = p.second
canvas?.drawPath(p.first, paint)
}
paint.color = drawingColor
paint.strokeWidth = strokeDrawWidth
canvas?.drawPath(path, paint)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
if (event == null)
return false
motionTouchEventX = event.x
motionTouchEventY = event.y
when (event.action) {
MotionEvent.ACTION_DOWN -> {
undonePaths.clear()
path.reset()
path.moveTo(motionTouchEventX, motionTouchEventY)
currentX = motionTouchEventX
currentY = motionTouchEventY
invalidate()
}
MotionEvent.ACTION_MOVE -> {
val distanceX = abs(motionTouchEventX - currentX)
val distanceY = abs(motionTouchEventY - currentY)
if (distanceX >= touchTolerance || distanceY >= touchTolerance) {
path.quadTo(
currentX,
currentY,
(motionTouchEventX + currentX) / 2,
(currentY + motionTouchEventY) / 2
)
currentX = motionTouchEventX
currentY = motionTouchEventY
}
invalidate()
}
MotionEvent.ACTION_UP -> {
path.lineTo(currentX, currentY)
extraCanvas?.drawPath(path, paint)
paths.add(Triple(path, drawingColor, strokeDrawWidth))
path = Path()
}
}
return true
}
override fun isSaveEnabled(): Boolean {
return true
}
So no-one answered and after some searching and experiments I get working solution, use it or adapt for your need if you will get in the same situation
( answer is based on the code from my question - so if you miss some dependencies please check it )
Init companion object with request code for gallery action in your ACTIVITY
companion object {
private const val GALLERY_REQUEST_CODE = 102
}
Create a method to pick image from gallery ( you need to receive Uri )
private fun pickFromGallery() {
val intent = Intent(Intent.ACTION_PICK)
intent.type = "image/*"
val imageTypes = arrayOf("image/jpeg", "image/png")
intent.putExtra(Intent.EXTRA_MIME_TYPES, imageTypes)
startActivityForResult(intent, GALLERY_REQUEST_CODE)
}
Than you need to override onActivityResult() method to receive your Uri and send it to the custom view
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == GALLERY_REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK) {
val uri: Uri? = data?.data
if (uri != null) {
val bitmap = MediaStore.Images.Media.getBitmap(this.contentResolver, uri)
canvasCustomView.loadCanvasBackground(bitmap)
}
}
}
}
Now in onDraw() method ( in your Custom View ) you need to use .drawBitmap to set your received Uri aka bitmap as a background to your canvas
override fun onDraw(canvas: Canvas?) {
if (bitmapBackground != null) {
canvas?.drawBitmap(bitmapBackground!!, 0f, 0f, paint)
}