All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.com.skydoves.landscapist.placeholder.shimmer.Placeholder.kt Maven / Gradle / Ivy

Go to download

A pluggable, highly optimized Jetpack Compose image loading library that fetches and displays network images with Glide, Coil, and Fresco.

There is a newer version: 2.3.8
Show newest version
/*
 * Designed and developed by 2020-2023 skydoves (Jaewoong Eum)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.skydoves.landscapist.placeholder.shimmer

import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.Transition
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.updateTransition
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.geometry.toRect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.drawOutline
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.node.Ref
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.LayoutDirection

/**
 * Originated from https://github.com/google/accompanist/blob/main/placeholder/src/main/java/com/google/accompanist/placeholder/Placeholder.kt
 *
 * All rights reserved to Google LLC.
 *
 * Draws some skeleton UI which is typically used whilst content is 'loading'.
 *
 * A version of this modifier which uses appropriate values for Material themed apps is available
 * in the 'Placeholder Material' library.
 *
 * You can provide a [PlaceholderHighlight] which runs an highlight animation on the placeholder.
 * The [shimmer] and [fade] implementations are provided for easy usage.
 *
 * A cross-fade transition will be applied to the content and placeholder UI when the [visible]
 * value changes. The transition can be customized via the [contentFadeTransitionSpec] and
 * [placeholderFadeTransitionSpec] parameters.
 *
 * You can find more information on the pattern at the Material Theming
 * [Placeholder UI](https://material.io/design/communication/launch-screen.html#placeholder-ui)
 * guidelines.
 *
 * @sample com.google.accompanist.sample.placeholder.DocSample_Foundation_Placeholder
 *
 * @param visible whether the placeholder should be visible or not.
 * @param color the color used to draw the placeholder UI.
 * @param shape desired shape of the placeholder. Defaults to [RectangleShape].
 * @param highlight optional highlight animation.
 * @param placeholderFadeTransitionSpec The transition spec to use when fading the placeholder
 * on/off screen. The boolean parameter defined for the transition is [visible].
 * @param contentFadeTransitionSpec The transition spec to use when fading the content
 * on/off screen. The boolean parameter defined for the transition is [visible].
 */
internal fun Modifier.placeholder(
  visible: Boolean,
  color: Color,
  shape: Shape = RectangleShape,
  highlight: PlaceholderHighlight? = null,
  placeholderFadeTransitionSpec:
  @Composable Transition.Segment.() -> FiniteAnimationSpec = { spring() },
  contentFadeTransitionSpec:
  @Composable Transition.Segment.() -> FiniteAnimationSpec = { spring() },
): Modifier = composed(
  inspectorInfo = debugInspectorInfo {
    name = "placeholder"
    value = visible
    properties["visible"] = visible
    properties["color"] = color
    properties["highlight"] = highlight
    properties["shape"] = shape
  },
) {
  // Values used for caching purposes
  val lastSize = remember { Ref() }
  val lastLayoutDirection = remember { Ref() }
  val lastOutline = remember { Ref() }

  // The current highlight animation progress
  var highlightProgress: Float by remember { mutableStateOf(0f) }

  // This is our crossfade transition
  val transitionState = remember { MutableTransitionState(visible) }.apply {
    targetState = visible
  }
  val transition = updateTransition(transitionState, "placeholder_crossfade")

  val placeholderAlpha by transition.animateFloat(
    transitionSpec = placeholderFadeTransitionSpec,
    label = "placeholder_fade",
    targetValueByState = { placeholderVisible -> if (placeholderVisible) 1f else 0f },
  )
  val contentAlpha by transition.animateFloat(
    transitionSpec = contentFadeTransitionSpec,
    label = "content_fade",
    targetValueByState = { placeholderVisible -> if (placeholderVisible) 0f else 1f },
  )

  // Run the optional animation spec and update the progress if the placeholder is visible
  val animationSpec = highlight?.animationSpec
  if (animationSpec != null && (visible || placeholderAlpha >= 0.01f)) {
    val infiniteTransition = rememberInfiniteTransition()
    highlightProgress = infiniteTransition.animateFloat(
      initialValue = 0f,
      targetValue = 1f,
      animationSpec = animationSpec,
    ).value
  }

  val paint = remember { Paint() }
  remember(color, shape, highlight) {
    drawWithContent {
      // Draw the composable content first
      if (contentAlpha in 0.01f..0.99f) {
        // If the content alpha is between 1% and 99%, draw it in a layer with
        // the alpha applied
        paint.alpha = contentAlpha
        withLayer(paint) {
          with(this@drawWithContent) {
            drawContent()
          }
        }
      } else if (contentAlpha >= 0.99f) {
        // If the content alpha is > 99%, draw it with no alpha
        drawContent()
      }

      if (placeholderAlpha in 0.01f..0.99f) {
        // If the placeholder alpha is between 1% and 99%, draw it in a layer with
        // the alpha applied
        paint.alpha = placeholderAlpha
        withLayer(paint) {
          lastOutline.value = drawPlaceholder(
            shape = shape,
            color = color,
            highlight = highlight,
            progress = highlightProgress,
            lastOutline = lastOutline.value,
            lastLayoutDirection = lastLayoutDirection.value,
            lastSize = lastSize.value,
          )
        }
      } else if (placeholderAlpha >= 0.99f) {
        // If the placeholder alpha is > 99%, draw it with no alpha
        lastOutline.value = drawPlaceholder(
          shape = shape,
          color = color,
          highlight = highlight,
          progress = highlightProgress,
          lastOutline = lastOutline.value,
          lastLayoutDirection = lastLayoutDirection.value,
          lastSize = lastSize.value,
        )
      }

      // Keep track of the last size & layout direction
      lastSize.value = size
      lastLayoutDirection.value = layoutDirection
    }
  }
}

private fun DrawScope.drawPlaceholder(
  shape: Shape,
  color: Color,
  highlight: PlaceholderHighlight?,
  progress: Float,
  lastOutline: Outline?,
  lastLayoutDirection: LayoutDirection?,
  lastSize: Size?,
): Outline? {
  // shortcut to avoid Outline calculation and allocation
  if (shape === RectangleShape) {
    // Draw the initial background color
    drawRect(color = color)

    if (highlight != null) {
      drawRect(
        brush = highlight.brush(progress, size),
        alpha = highlight.alpha(progress),
      )
    }
    // We didn't create an outline so return null
    return null
  }

  // Otherwise we need to create an outline from the shape
  val outline = lastOutline.takeIf {
    size == lastSize && layoutDirection == lastLayoutDirection
  } ?: shape.createOutline(size, layoutDirection, this)

  // Draw the placeholder color
  drawOutline(outline = outline, color = color)

  if (highlight != null) {
    drawOutline(
      outline = outline,
      brush = highlight.brush(progress, size),
      alpha = highlight.alpha(progress),
    )
  }

  // Return the outline we used
  return outline
}

private inline fun DrawScope.withLayer(
  paint: Paint,
  drawBlock: DrawScope.() -> Unit,
) = drawIntoCanvas { canvas ->
  canvas.saveLayer(size.toRect(), paint)
  drawBlock()
  canvas.restore()
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy