Scale Image Viewer

repository·main·Indexed 42 days ago

https://github.com/jvziyaoyao/scale

A Jetpack Compose Multiplatform library for image viewing. Scale provides components for smooth gesture animations, image lists (pagers), and pop-up previews with transition effects. It includes specialized support for loading extremely large images via the sampling-decoder module and offers high-level components like ImageViewer, ImagePager, and ImagePreviewer, as well as low-level tools like ZoomableView.

Tokens
20.6K
Snippets
63
Records
73
Agent score
53%

What's inside Scale

  1. How transition animations work with TransformItemView

    main

    To achieve a smooth transition (similar to WeChat Moments) where the image expands from its original position, use TransformItemView in your list and PreviewerState.enterTransform(index) to open it.

    Critical Requirement: The key provided to TransformItemView must match the key returned by the getKey lambda in rememberPreviewerState.

    If you have the same key appearing in multiple places on the same screen, you must provide a unique ItemStateMap to each PreviewerState via transformItemStateMap and wrap the corresponding UI in a CompositionLocalProvider(LocalTransformItemStateMap provides ...) to avoid incorrect animation positioning.

    // Setup state with key mapping
    val state = rememberPreviewerState(
        pageCount = { images.size },
        getKey = { index -> images[index].first } // Returns the unique key
    )
    
    // In your list/grid
    images.forEachIndexed { index, image ->
        TransformItemView(
            key = image.first, // Must match getKey result
            transformState = state,
            itemState = rememberTransformItemState(intrinsicSize = painter.intrinsicSize),
            // ...
        ) { /* content */ }
    }
    
    // Trigger transition
    scope.launch { state.enterTransform(index) }
  2. Handle gestures and state in ImagePager

    main

    Gestures

    ImagePager uses the same gesture system as ZoomablePager. You can access gesture callbacks via PagerGestureScope. See ZoomablePager PagerGestureScope for details.

    State Control

    ImagePager is controlled via ZoomablePagerState. Use this state to programmatically manage the pager's position and zoom level. See ZoomablePager ZoomablePagerState for details.

  3. Basic usage of ImagePreviewer

    main

    Use ImagePreviewer to implement an image preview component similar to WeChat Moments. You need to prepare an image list, declare a PreviewerState using rememberPreviewerState, and provide an imageLoader that returns a Pair containing the image painter and its intrinsic size.

    // Prepare an image list
    val images = remember {
        mutableStateListOf(
            "https://t7.baidu.com/it/u=1595072465,3644073269&fm=193&f=GIF",
            "https://t7.baidu.com/it/u=4198287529,2774471735&fm=193&f=GIF",
        )
    }
    // Declare a PreviewerState
    val state = rememberPreviewerState(pageCount = { images.size }) { images[it] }
    // Create an ImagePreviewer
    ImagePreviewer(
        state = state,
        imageLoader = {
            val painter = rememberAsyncImagePainter(model = images[page])
            Pair(painter, painter.intrinsicSize)
        }
    )
    
    // Open
    state.open()
    // Close
    state.close()
  4. Use ZoomableView to zoom any Composable

    main

    ZoomableView is the core component of the library. It allows you to perform zoom (pinch-to-zoom) and pan operations on any arbitrary Composable content.

    Requirements for correct display:

    1. Provide ZoomableViewState: You must provide a state via rememberZoomableState and inform it of the content's intrinsic size using contentSize. Without this, the component will not display correctly.
    2. Use Modifier.fillMaxSize(): The content inside the ZoomableView lambda must use Modifier.fillMaxSize() to ensure it fills the entire layer provided by the component.
    val painter = painterResource(id = R.drawable.light_02)
    val state = rememberZoomableState(contentSize = painter.intrinsicSize)
    ZoomableView(state = state) {
        Image(
            modifier = Modifier.fillMaxSize(), // Mandatory: must fill the layer
            painter = painter,
            contentDescription = null,
        )
    }
  5. Use ZoomablePager for zoomable horizontal lists

    main

    ZoomablePager is built on top of ZoomableView and Jetpack Compose Pager. It provides support for horizontal list interfaces and simplifies gesture handling and ZoomableView state management.

    To use it, you must wrap your content in a ZoomablePolicy composable. ZoomablePolicy requires the intrinsicSize of the content being displayed, and the content inside the ZoomablePolicy lambda must use Modifier.fillMaxSize().

    Basic Usage Example

    // Prepare an image list
    val images = remember {
        mutableStateListOf(R.drawable.light_01, R.drawable.light_02)
    }
    // Create a PagerState
    val pagerState = rememberZoomablePagerState { images.size }
    // Pager component
    ZoomablePager(state = pagerState) { page ->
        val painter = painterResource(id = images[page])
        // Mandatory Composable function
        ZoomablePolicy(intrinsicSize = painter.intrinsicSize) { _ ->
            Image(
                modifier = Modifier.fillMaxSize(),
                painter = painter,
                contentDescription = null
            )
        }
    }
    val images = remember {
        mutableStateListOf(R.drawable.light_01, R.drawable.light_02)
    }
    val pagerState = rememberZoomablePagerState { images.size }
    ZoomablePager(state = pagerState) { page ->
        val painter = painterResource(id = images[page])
        ZoomablePolicy(intrinsicSize = painter.intrinsicSize) { _ ->
            Image(
                modifier = Modifier.fillMaxSize(),
                painter = painter,
                contentDescription = null
            )
        }
    }
  6. Load and display huge images with SamplingDecoder

    main

    To support extremely large images, add the sampling-decoder dependency. The ImageViewer will only treat the image as a 'huge image' if the model type is a SamplingDecoder. You must use rememberSamplingDecoder and provide a ModelProcessor.

    // Add dependency: implementation("com.jvziyaoyao.scale:sampling-decoder:$version")
    
    val bytes = remember { mutableStateOf<ByteArray?>(null) }
    LaunchedEffect(Unit) { bytes.value = Res.readBytes("files/a350.jpg") }
    val (samplingDecoder) = rememberSamplingDecoder(bytes.value)
    if (samplingDecoder != null) {
        val state = rememberZoomableState(
            contentSize = samplingDecoder.intrinsicSize
        )
        ImageViewer(
            model = samplingDecoder,
            state = state,
            processor = ModelProcessor(samplingProcessorPair),
        )
    }
  7. Basic usage of ImageViewer

    main

    Use ImageViewer to display images with built-in zoom and pan capabilities. You must provide the intrinsic size of the image via rememberZoomableState(contentSize = ...) for the component to display correctly.

    Note: Providing the image's intrinsic size is mandatory.

    val painter = painterResource(id = R.drawable.light_02)
    val state = rememberZoomableState(contentSize = painter.intrinsicSize)
    ImageViewer(model = painter, state = state)
  8. Install Scale via MavenCentral

    main

    Scale is available on mavenCentral(). You can include the core image viewer and the optional sampling decoder for large image support.

    // Use MavenCentral repository
    repositories {
        mavenCentral()
    }
    
    val version = "1.1.1-beta.3"
    // Image viewer library
    implementation("com.jvziyaoyao.scale:image-viewer:$version")
    // Large image support (optional)
    implementation("com.jvziyaoyao.scale:sampling-decoder:$version")