@ -4,81 +4,88 @@ import androidx.compose.runtime.getValue
@@ -4,81 +4,88 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.geometry.isSpecified
class ScalableState ( ) {
var imageSize by mutableStateOf ( IntSize ( 0 , 0 ) )
var boxSize by mutableStateOf ( IntSize ( 1 , 1 ) )
var offset by mutableStateOf ( IntOffset . Zero )
/ * *
* Encapsulate all transformations about showing some target ( an image , relative to its center )
* scaled and shifted in some area ( a window , relative to its center )
* /
class ScalableState {
val scaleLimits = 0.2f .. 10f
/ * *
* Offset of the camera before scaling ( an offset in pixels in the area coordinate system )
* /
var offset by mutableStateOf ( Offset . Zero )
private set
var scale by mutableStateOf ( 1f )
}
private set
val ScalableState . visiblePart
get ( ) : IntRect {
val boxRatio = boxSize . width . toFloat ( ) / boxSize . height
val imageRatio = imageSize . width . toFloat ( ) / imageSize . height . toFloat ( )
private var areaSize : Size = Size . Unspecified
private var targetSize : Size = Size . Zero
val size : IntSize =
if ( boxRatio > imageRatio ) {
val height = imageSize . height / scale
val targetWidth = height * boxRatio
IntSize ( minOf ( imageSize . width , targetWidth . toInt ( ) ) , height . toInt ( ) )
} else {
val width = imageSize . width / scale
val targetHeight = width / boxRatio
IntSize ( width . toInt ( ) , minOf ( imageSize . height , targetHeight . toInt ( ) ) )
}
private var offsetXLimits = Float . NEGATIVE _INFINITY .. Float . POSITIVE _INFINITY
private var offsetYLimits = Float . NEGATIVE _INFINITY .. Float . POSITIVE _INFINITY
return IntRect ( offset = offset , size = size )
/ * *
* Limit the target center position , so :
* - if the size of the target is less than area ,
* the center of the target is bound to the center of the area
* - if the size of the target is greater , then limit the center of it ,
* so the target will be always in the area
* /
fun limitTargetInsideArea (
areaSize : Size ,
targetSize : Size ,
) {
this . areaSize = areaSize
this . targetSize = targetSize
applyLimits ( )
}
fun ScalableState . changeBoxSize ( size : IntSize ) {
boxSize = size
updateOffsetLimits ( )
}
fun ScalableState . setScale ( scale : Float ) {
this . scale = scale
}
fun ScalableState . addScale ( diff : Float ) {
scale = if ( scale + diff > MAX _SCALE ) {
MAX _SCALE
} else if ( scale + diff < MIN _SCALE ) {
MIN _SCALE
} else {
scale + diff
private fun applyLimits ( ) {
if ( targetSize . isSpecified && areaSize . isSpecified ) {
offsetXLimits = centerLimits ( targetSize . width * scale , areaSize . width )
offsetYLimits = centerLimits ( targetSize . height * scale , areaSize . height )
offset = Offset (
offset . x . coerceIn ( offsetXLimits ) ,
offset . y . coerceIn ( offsetYLimits ) ,
)
} else {
offsetXLimits = Float . NEGATIVE _INFINITY .. Float . POSITIVE _INFINITY
offsetYLimits = Float . NEGATIVE _INFINITY .. Float . POSITIVE _INFINITY
}
}
updateOffsetLimits ( )
}
fun ScalableState . addDragAmount ( diff : Offset ) {
offset -= IntOffset ( ( diff . x + 1 ) . toInt ( ) , ( diff . y + 1 ) . toInt ( ) )
updateOffsetLimits ( )
}
fun ScalableState . updateImageSize ( width : Int , height : Int ) {
imageSize = IntSize ( width , height )
updateOffsetLimits ( )
}
private fun ScalableState . updateOffsetLimits ( ) {
if ( offset . x + visiblePart . width > imageSize . width ) {
changeOffset ( x = imageSize . width - visiblePart . width )
}
if ( offset . y + visiblePart . height > imageSize . height ) {
changeOffset ( y = imageSize . height - visiblePart . height )
private fun centerLimits ( imageSize : Float , areaSize : Float ) : ClosedFloatingPointRange < Float > {
val areaCenter = areaSize / 2
val imageCenter = imageSize / 2
val extra = ( imageCenter - areaCenter ) . coerceAtLeast ( 0f )
return - extra / 2. . extra / 2
}
if ( offset . x < 0 ) {
changeOffset ( x = 0 )
fun addPan ( pan : Offset ) {
offset += pan
applyLimits ( )
}
if ( offset . y < 0 ) {
changeOffset ( y = 0 )
/ * *
* @param focus on which point the camera is focused in the area coordinate system .
* After we apply the new scale , the camera should be focused on the same point in
* the target coordinate system .
* /
fun addScale ( scaleMultiplier : Float , focus : Offset = Offset . Zero ) {
setScale ( scale * scaleMultiplier , focus )
}
}
private fun ScalableState . changeOffset ( x : Int = offset . x , y : Int = offset . y ) {
offset = IntOffset ( x , y )
fun setScale ( scale : Float , focus : Offset = Offset . Zero ) {
val newScale = scale . coerceIn ( scaleLimits )
val focusInTargetSystem = ( focus - offset ) / this . scale
// calculate newOffset from this equation:
// focusInTargetSystem = (focus - newOffset) / newScale
offset = focus - focusInTargetSystem * newScale
this . scale = newScale
applyLimits ( )
}
}