Skip to content

Instantly share code, notes, and snippets.

@sphrak
Forked from surajsau/ParallaxScreen.kt
Created May 28, 2022 11:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sphrak/19c007ec82d660efa6e0790342d004de to your computer and use it in GitHub Desktop.
Save sphrak/19c007ec82d660efa6e0790342d004de to your computer and use it in GitHub Desktop.
Parallax effect with Jetpack Compose
@Composable
fun ParallaxScreen(modifier: Modifier = Modifier) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
var data by remember { mutableStateOf<SensorData?>(null) }
DisposableEffect(Unit) {
val dataManager = SensorDataManager(context)
dataManager.init()
val job = scope.launch {
dataManager.data
.receiveAsFlow()
.onEach { data = it }
.collect()
}
onDispose {
dataManager.cancel()
job.cancel()
}
}
Box(modifier = modifier) {
ParallaxView(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.Center),
multiPlier = 20,
data = data
)
}
}
// For changing offset values, it is always preferred to use .offset { } instead of .offset()
// as offset {..} is implemented to avoid recomposition during the offset changes
@Composable
fun ParallaxView(
modifier: Modifier = Modifier,
depthMultiplier: Int = 20,
data: SensorData?
) {
val roll by derivedStateOf { (data?.roll ?: 0f) * depthMultiplier }
val pitch by derivedStateOf { (data?.pitch ?: 0f) * depthMultiplier }
Box(modifier = modifier) {
// Glow Shadow
// Has quicker offset change and in opposite direction to the Image Card
Image(
painter = painterResource(id = drawable.beach),
modifier = Modifier
.offset {
IntOffset(
x = -(roll * 1.5).dp.roundToPx(),
y = (pitch * 2).dp.roundToPx()
)
}
.width(256.dp)
.height(356.dp)
.align(Alignment.Center)
.blur(radius = 24.dp, edgeTreatment = BlurredEdgeTreatment.Unbounded),
contentDescription = null,
contentScale = ContentScale.FillHeight,
)
// Edge (used to give depth to card when tilted)
// Has slightly slower offset change than Image Card
Box(
modifier = Modifier
.offset {
IntOffset(
x = (roll * 0.9).dp.roundToPx(),
y = -(pitch * 0.9).dp.roundToPx()
)
}
.width(300.dp)
.height(400.dp)
.align(Alignment.Center)
.background(
color = Color.White.copy(alpha = 0.3f),
shape = RoundedCornerShape(16.dp)
),
)
// Image Card
// The image inside has a parallax shift in the opposite direction
Image(
painter = painterResource(id = drawable.beach),
modifier = Modifier
.offset {
IntOffset(
x = roll.dp.roundToPx(),
y = -pitch.dp.roundToPx()
)
}
.width(300.dp)
.height(400.dp)
.align(Alignment.Center)
.clip(RoundedCornerShape(16.dp)),
contentDescription = null,
contentScale = ContentScale.FillHeight,
alignment = BiasAlignment(
horizontalBias = (roll * 0.005).toFloat(),
verticalBias = 0f,
)
)
}
}
class SensorDataManager (context: Context): SensorEventListener {
private val sensorManager by lazy {
context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
}
fun init() {
Log.d("SensorDataManager", "init")
val accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY)
val magnetometer = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)
sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_UI)
sensorManager.registerListener(this, magnetometer, SensorManager.SENSOR_DELAY_UI)
}
private var gravity: FloatArray? = null
private var geomagnetic: FloatArray? = null
val data: Channel<SensorData> = Channel(Channel.UNLIMITED)
override fun onSensorChanged(event: SensorEvent?) {
if (event?.sensor?.type == Sensor.TYPE_GRAVITY)
gravity = event.values
if (event?.sensor?.type == Sensor.TYPE_MAGNETIC_FIELD)
geomagnetic = event.values
if (gravity != null && geomagnetic != null) {
var r = FloatArray(9)
var i = FloatArray(9)
if (SensorManager.getRotationMatrix(r, i, gravity, geomagnetic)) {
var orientation = FloatArray(3)
SensorManager.getOrientation(r, orientation)
data.trySend(
SensorData(
roll = orientation[2],
pitch = orientation[1]
)
)
}
}
}
fun cancel() {
Log.d("SensorDataManager", "cancel")
sensorManager.unregisterListener(this)
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
}
data class SensorData(
val roll: Float,
val pitch: Float
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment