mirror of
https://github.com/fankes/pagecurl-multiplatform.git
synced 2025-09-06 18:55:28 +08:00
Add comments
This commit is contained in:
@@ -10,24 +10,53 @@ import androidx.compose.ui.unit.IntSize
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.wewox.pagecurl.ExperimentalPageCurlApi
|
import eu.wewox.pagecurl.ExperimentalPageCurlApi
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The configuration for PageCurl.
|
||||||
|
*
|
||||||
|
* @property curl Configures how page curl looks like.
|
||||||
|
* @property interaction Configures interactions with a page curl. Such as if drag or tap to go on next or previous
|
||||||
|
* page is allowed, etc.
|
||||||
|
*/
|
||||||
@ExperimentalPageCurlApi
|
@ExperimentalPageCurlApi
|
||||||
public data class PageCurlConfig(
|
public data class PageCurlConfig(
|
||||||
val curl: CurlConfig = CurlConfig(),
|
val curl: CurlConfig = CurlConfig(),
|
||||||
val interaction: InteractionConfig = InteractionConfig(),
|
val interaction: InteractionConfig = InteractionConfig(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures how page curl looks like.
|
||||||
|
*
|
||||||
|
* @property backPage Configures how back-page looks like (color, content alpha).
|
||||||
|
* @property shadow Configures how page shadow looks like (color, alpha, radius).
|
||||||
|
*/
|
||||||
@ExperimentalPageCurlApi
|
@ExperimentalPageCurlApi
|
||||||
public data class CurlConfig(
|
public data class CurlConfig(
|
||||||
val backPage: BackPageConfig = BackPageConfig(),
|
val backPage: BackPageConfig = BackPageConfig(),
|
||||||
val shadow: ShadowConfig = ShadowConfig(),
|
val shadow: ShadowConfig = ShadowConfig(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures how back-page of the page curl looks like.
|
||||||
|
*
|
||||||
|
* @property color Color of the back-page. In majority of use-cases it should be set to the content background color.
|
||||||
|
* @property contentAlpha The alpha which defines how content is "seen through" the back-page. From 0 (nothing is
|
||||||
|
* visible) to 1 (everything is visible).
|
||||||
|
*/
|
||||||
@ExperimentalPageCurlApi
|
@ExperimentalPageCurlApi
|
||||||
public data class BackPageConfig(
|
public data class BackPageConfig(
|
||||||
val color: Color = Color.White,
|
val color: Color = Color.White,
|
||||||
val contentAlpha: Float = 0.1f,
|
val contentAlpha: Float = 0.1f,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures how page shadow looks like.
|
||||||
|
*
|
||||||
|
* @property color The color of the shadow. In majority of use-cases it should be set to the inverted color to the
|
||||||
|
* content background color. Should be a solid color, see [alpha] to adjust opacity.
|
||||||
|
* @property alpha The alpha of the [color].
|
||||||
|
* @property radius Defines how big the shadow is.
|
||||||
|
* @property offset Defines how shadow is shifted from the page. A little shift may add more realism.
|
||||||
|
*/
|
||||||
@ExperimentalPageCurlApi
|
@ExperimentalPageCurlApi
|
||||||
public data class ShadowConfig(
|
public data class ShadowConfig(
|
||||||
val color: Color = Color.Black,
|
val color: Color = Color.Black,
|
||||||
@@ -36,16 +65,39 @@ public data class ShadowConfig(
|
|||||||
val offset: DpOffset = DpOffset((-5).dp, 0.dp),
|
val offset: DpOffset = DpOffset((-5).dp, 0.dp),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures interactions with a page curl.
|
||||||
|
*
|
||||||
|
* @property drag Configures drag interactions.
|
||||||
|
* @property tap Configures tap interactions.
|
||||||
|
*/
|
||||||
@ExperimentalPageCurlApi
|
@ExperimentalPageCurlApi
|
||||||
public data class InteractionConfig(
|
public data class InteractionConfig(
|
||||||
val drag: Drag = Drag(),
|
val drag: Drag = Drag(),
|
||||||
val tap: Tap = Tap(),
|
val tap: Tap = Tap(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures drag interactions.
|
||||||
|
*
|
||||||
|
* @property forward Configures forward drag interaction.
|
||||||
|
* @property backward Configures backward drag interaction.
|
||||||
|
*/
|
||||||
@ExperimentalPageCurlApi
|
@ExperimentalPageCurlApi
|
||||||
public data class Drag(
|
public data class Drag(
|
||||||
val forward: Interaction = Interaction(true, rightHalf(), leftHalf()),
|
val forward: Interaction = Interaction(true, rightHalf(), leftHalf()),
|
||||||
val backward: Interaction = Interaction(true, forward.end, forward.start),
|
val backward: Interaction = Interaction(true, forward.end, forward.start),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The drag interaction setting.
|
||||||
|
*
|
||||||
|
* @property enabled True if this interaction is enabled or not.
|
||||||
|
* @property start Defines a rectangle where interaction should start. The rectangle coordinates are relative
|
||||||
|
* (from 0 to 1) and then scaled to the PageCurl bounds.
|
||||||
|
* @property end Defines a rectangle where interaction should end. The rectangle coordinates are relative
|
||||||
|
* (from 0 to 1) and then scaled to the PageCurl bounds.
|
||||||
|
*/
|
||||||
@ExperimentalPageCurlApi
|
@ExperimentalPageCurlApi
|
||||||
public data class Interaction(
|
public data class Interaction(
|
||||||
val enabled: Boolean,
|
val enabled: Boolean,
|
||||||
@@ -54,18 +106,41 @@ public data class InteractionConfig(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures tap interactions.
|
||||||
|
*
|
||||||
|
* @property forward Configures forward tap interaction.
|
||||||
|
* @property backward Configures backward tap interaction.
|
||||||
|
* @property custom The custom tap interaction. Could be provided to implement custom taps in the PageCurl, e.g. to
|
||||||
|
* capture taps in the center, etc.
|
||||||
|
*/
|
||||||
@ExperimentalPageCurlApi
|
@ExperimentalPageCurlApi
|
||||||
public data class Tap(
|
public data class Tap(
|
||||||
val forward: Interaction = Interaction(true, rightHalf()),
|
val forward: Interaction = Interaction(true, rightHalf()),
|
||||||
val backward: Interaction = Interaction(true, leftHalf()),
|
val backward: Interaction = Interaction(true, leftHalf()),
|
||||||
val custom: CustomInteraction = CustomInteraction(false)
|
val custom: CustomInteraction = CustomInteraction(false)
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The tap interaction setting.
|
||||||
|
*
|
||||||
|
* @property enabled True if this interaction is enabled or not.
|
||||||
|
* @property target Defines a rectangle where interaction captured. The rectangle coordinates are relative
|
||||||
|
* (from 0 to 1) and then scaled to the PageCurl bounds.
|
||||||
|
*/
|
||||||
@ExperimentalPageCurlApi
|
@ExperimentalPageCurlApi
|
||||||
public data class Interaction(
|
public data class Interaction(
|
||||||
val enabled: Boolean,
|
val enabled: Boolean,
|
||||||
val target: Rect = Rect.Zero,
|
val target: Rect = Rect.Zero,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The custom tap interaction setting.
|
||||||
|
*
|
||||||
|
* @property enabled True if this interaction is enabled or not.
|
||||||
|
* @property onTap The lambda to invoke to check if tap is handled by custom tap or not. Receives the density
|
||||||
|
* scope, the PageCurl size and tap position. Returns true if tap is handled and false otherwise.
|
||||||
|
*/
|
||||||
@ExperimentalPageCurlApi
|
@ExperimentalPageCurlApi
|
||||||
public data class CustomInteraction(
|
public data class CustomInteraction(
|
||||||
val enabled: Boolean,
|
val enabled: Boolean,
|
||||||
@@ -74,6 +149,15 @@ public data class InteractionConfig(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The utility function to create a new copy of the [InteractionConfig] with different enabled states of each
|
||||||
|
* interactions.
|
||||||
|
*
|
||||||
|
* @param dragForwardEnabled True to enable forward drag interaction.
|
||||||
|
* @param dragBackwardEnabled True to enable backward drag interaction.
|
||||||
|
* @param tapForwardEnabled True to enable forward tap interaction.
|
||||||
|
* @param tapBackwardEnabled True to enable backward tap interaction.
|
||||||
|
*/
|
||||||
@ExperimentalPageCurlApi
|
@ExperimentalPageCurlApi
|
||||||
public fun InteractionConfig.copy(
|
public fun InteractionConfig.copy(
|
||||||
dragForwardEnabled: Boolean = drag.forward.enabled,
|
dragForwardEnabled: Boolean = drag.forward.enabled,
|
||||||
@@ -91,6 +175,12 @@ public fun InteractionConfig.copy(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The left half of the PageCurl.
|
||||||
|
*/
|
||||||
private fun leftHalf(): Rect = Rect(0.0f, 0.0f, 0.5f, 1.0f)
|
private fun leftHalf(): Rect = Rect(0.0f, 0.0f, 0.5f, 1.0f)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The right half of the PageCurl.
|
||||||
|
*/
|
||||||
private fun rightHalf(): Rect = Rect(0.5f, 0.0f, 1.0f, 1.0f)
|
private fun rightHalf(): Rect = Rect(0.5f, 0.0f, 1.0f, 1.0f)
|
||||||
|
@@ -5,6 +5,7 @@ import android.graphics.Canvas
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.CacheDrawScope
|
import androidx.compose.ui.draw.CacheDrawScope
|
||||||
|
import androidx.compose.ui.draw.DrawResult
|
||||||
import androidx.compose.ui.draw.drawWithCache
|
import androidx.compose.ui.draw.drawWithCache
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.geometry.toRect
|
import androidx.compose.ui.geometry.toRect
|
||||||
@@ -33,19 +34,20 @@ internal fun Modifier.drawCurl(
|
|||||||
posA: Offset,
|
posA: Offset,
|
||||||
posB: Offset,
|
posB: Offset,
|
||||||
): Modifier = drawWithCache {
|
): Modifier = drawWithCache {
|
||||||
fun drawOnlyContent() =
|
// Fast-check if curl is in left most position (gesture is fully completed)
|
||||||
onDrawWithContent {
|
// In such case do not bother and draw nothing
|
||||||
drawContent()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (posA == size.toRect().topLeft && posB == size.toRect().bottomLeft) {
|
if (posA == size.toRect().topLeft && posB == size.toRect().bottomLeft) {
|
||||||
return@drawWithCache onDrawWithContent { }
|
return@drawWithCache drawNothing()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fast-check if curl is in right most position (gesture is not yet started)
|
||||||
|
// In such case do not bother and draw the full content
|
||||||
if (posA == size.toRect().topRight && posB == size.toRect().bottomRight) {
|
if (posA == size.toRect().topRight && posB == size.toRect().bottomRight) {
|
||||||
return@drawWithCache drawOnlyContent()
|
return@drawWithCache drawOnlyContent()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find the intersection of the curl line ([posA, posB]) and top and bottom sides, so that we may clip and mirror
|
||||||
|
// content correctly
|
||||||
val topIntersection = lineLineIntersection(
|
val topIntersection = lineLineIntersection(
|
||||||
Offset(0f, 0f), Offset(size.width, 0f),
|
Offset(0f, 0f), Offset(size.width, 0f),
|
||||||
posA, posB
|
posA, posB
|
||||||
@@ -54,14 +56,20 @@ internal fun Modifier.drawCurl(
|
|||||||
Offset(0f, size.height), Offset(size.width, size.height),
|
Offset(0f, size.height), Offset(size.width, size.height),
|
||||||
posA, posB
|
posA, posB
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Should not really happen, but in case there is not intersection (curl line is horizontal), just draw the full
|
||||||
|
// content instead
|
||||||
if (topIntersection == null || bottomIntersection == null) {
|
if (topIntersection == null || bottomIntersection == null) {
|
||||||
return@drawWithCache drawOnlyContent()
|
return@drawWithCache drawOnlyContent()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Limit x coordinates of both intersections to be at least 0, so that page do not look like teared from the book
|
||||||
val topCurlOffset = Offset(max(0f, topIntersection.x), topIntersection.y)
|
val topCurlOffset = Offset(max(0f, topIntersection.x), topIntersection.y)
|
||||||
val bottomCurlOffset = Offset(max(0f, bottomIntersection.x), bottomIntersection.y)
|
val bottomCurlOffset = Offset(max(0f, bottomIntersection.x), bottomIntersection.y)
|
||||||
|
|
||||||
|
// That is the easy part, prepare a lambda to draw the content clipped by the curl line
|
||||||
val drawClippedContent = prepareClippedContent(topCurlOffset, bottomCurlOffset)
|
val drawClippedContent = prepareClippedContent(topCurlOffset, bottomCurlOffset)
|
||||||
|
// That is the tricky part, prepare a lambda to draw the back-page with the shadow
|
||||||
val drawCurl = prepareCurl(config, topCurlOffset, bottomCurlOffset)
|
val drawCurl = prepareCurl(config, topCurlOffset, bottomCurlOffset)
|
||||||
|
|
||||||
onDrawWithContent {
|
onDrawWithContent {
|
||||||
@@ -70,16 +78,34 @@ internal fun Modifier.drawCurl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The simple method to draw the whole unmodified content.
|
||||||
|
*/
|
||||||
|
private fun CacheDrawScope.drawOnlyContent(): DrawResult =
|
||||||
|
onDrawWithContent {
|
||||||
|
drawContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The simple method to draw nothing.
|
||||||
|
*/
|
||||||
|
private fun CacheDrawScope.drawNothing(): DrawResult =
|
||||||
|
onDrawWithContent {
|
||||||
|
/* Empty */
|
||||||
|
}
|
||||||
|
|
||||||
@ExperimentalPageCurlApi
|
@ExperimentalPageCurlApi
|
||||||
private fun CacheDrawScope.prepareClippedContent(
|
private fun CacheDrawScope.prepareClippedContent(
|
||||||
topCurlOffset: Offset,
|
topCurlOffset: Offset,
|
||||||
bottomCurlOffset: Offset,
|
bottomCurlOffset: Offset,
|
||||||
): ContentDrawScope.() -> Unit {
|
): ContentDrawScope.() -> Unit {
|
||||||
|
// Make a quadrilateral from the left side to the intersection points
|
||||||
val path = Path()
|
val path = Path()
|
||||||
path.lineTo(topCurlOffset.x, topCurlOffset.y)
|
path.lineTo(topCurlOffset.x, topCurlOffset.y)
|
||||||
path.lineTo(bottomCurlOffset.x, bottomCurlOffset.y)
|
path.lineTo(bottomCurlOffset.x, bottomCurlOffset.y)
|
||||||
path.lineTo(0f, size.height)
|
path.lineTo(0f, size.height)
|
||||||
return result@{
|
return result@{
|
||||||
|
// Draw a content clipped by the constructed path
|
||||||
clipPath(path) {
|
clipPath(path) {
|
||||||
this@result.drawContent()
|
this@result.drawContent()
|
||||||
}
|
}
|
||||||
@@ -92,8 +118,13 @@ private fun CacheDrawScope.prepareCurl(
|
|||||||
topCurlOffset: Offset,
|
topCurlOffset: Offset,
|
||||||
bottomCurlOffset: Offset,
|
bottomCurlOffset: Offset,
|
||||||
): ContentDrawScope.() -> Unit {
|
): ContentDrawScope.() -> Unit {
|
||||||
|
// Build a quadrilateral of the part of the page which should be mirrored as the back-page
|
||||||
|
// In all cases polygon should have 4 points, even when back-page is only a small "corner" (with 3 points) due to
|
||||||
|
// the shadow rendering, otherwise it will create a visual artifact when switching between 3 and 4 points polygon
|
||||||
val polygon = Polygon(
|
val polygon = Polygon(
|
||||||
sequence {
|
sequence {
|
||||||
|
// Find the intersection of the curl line and right side
|
||||||
|
// If intersection is found adds to the polygon points list
|
||||||
suspend fun SequenceScope<Offset>.yieldEndSideInterception() {
|
suspend fun SequenceScope<Offset>.yieldEndSideInterception() {
|
||||||
val offset = lineLineIntersection(
|
val offset = lineLineIntersection(
|
||||||
topCurlOffset, bottomCurlOffset,
|
topCurlOffset, bottomCurlOffset,
|
||||||
@@ -102,12 +133,18 @@ private fun CacheDrawScope.prepareCurl(
|
|||||||
yield(offset)
|
yield(offset)
|
||||||
yield(offset)
|
yield(offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In case top intersection lays in the bounds of the page curl, take 2 points from the top side, otherwise
|
||||||
|
// take the interception with a right side
|
||||||
if (topCurlOffset.x < size.width) {
|
if (topCurlOffset.x < size.width) {
|
||||||
yield(topCurlOffset)
|
yield(topCurlOffset)
|
||||||
yield(Offset(size.width, topCurlOffset.y))
|
yield(Offset(size.width, topCurlOffset.y))
|
||||||
} else {
|
} else {
|
||||||
yieldEndSideInterception()
|
yieldEndSideInterception()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In case bottom intersection lays in the bounds of the page curl, take 2 points from the bottom side,
|
||||||
|
// otherwise take the interception with a right side
|
||||||
if (bottomCurlOffset.x < size.width) {
|
if (bottomCurlOffset.x < size.width) {
|
||||||
yield(Offset(size.width, size.height))
|
yield(Offset(size.width, size.height))
|
||||||
yield(bottomCurlOffset)
|
yield(bottomCurlOffset)
|
||||||
@@ -117,17 +154,25 @@ private fun CacheDrawScope.prepareCurl(
|
|||||||
}.toList()
|
}.toList()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Calculate the angle in radians between X axis and the curl line, this is used to rotate mirrored content to the
|
||||||
|
// right position of the curled back-page
|
||||||
val lineVector = topCurlOffset - bottomCurlOffset
|
val lineVector = topCurlOffset - bottomCurlOffset
|
||||||
val angle = Math.PI.toFloat() + atan2(-lineVector.y, lineVector.x) * 2
|
val angle = Math.PI.toFloat() - atan2(lineVector.y, lineVector.x) * 2
|
||||||
|
|
||||||
|
// Prepare a lambda to draw the shadow of the back-page
|
||||||
val drawShadow = prepareShadow(config, polygon, angle)
|
val drawShadow = prepareShadow(config, polygon, angle)
|
||||||
|
|
||||||
return result@{
|
return result@{
|
||||||
withTransform({
|
withTransform({
|
||||||
|
// Mirror in X axis the drawing as back-page should be mirrored
|
||||||
scale(-1f, 1f, pivot = bottomCurlOffset)
|
scale(-1f, 1f, pivot = bottomCurlOffset)
|
||||||
|
// Rotate the drawing according to the curl line
|
||||||
rotateRad(angle, pivot = bottomCurlOffset)
|
rotateRad(angle, pivot = bottomCurlOffset)
|
||||||
}) {
|
}) {
|
||||||
|
// Draw shadow first
|
||||||
this@result.drawShadow()
|
this@result.drawShadow()
|
||||||
|
|
||||||
|
// And finally draw the back-page with an overlay with alpha
|
||||||
clipPath(polygon.toPath()) {
|
clipPath(polygon.toPath()) {
|
||||||
this@result.drawContent()
|
this@result.drawContent()
|
||||||
|
|
||||||
@@ -146,15 +191,19 @@ private fun CacheDrawScope.prepareShadow(
|
|||||||
): ContentDrawScope.() -> Unit {
|
): ContentDrawScope.() -> Unit {
|
||||||
val shadow = config.shadow
|
val shadow = config.shadow
|
||||||
|
|
||||||
|
// Quick exit if no shadow is requested
|
||||||
if (shadow.alpha == 0f || shadow.radius == 0.dp) {
|
if (shadow.alpha == 0f || shadow.radius == 0.dp) {
|
||||||
return { /* No shadow is requested */ }
|
return { /* No shadow is requested */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prepare shadow parameters
|
||||||
val radius = shadow.radius.toPx()
|
val radius = shadow.radius.toPx()
|
||||||
val shadowColor = shadow.color.copy(alpha = shadow.alpha).toArgb()
|
val shadowColor = shadow.color.copy(alpha = shadow.alpha).toArgb()
|
||||||
val transparent = shadow.color.copy(alpha = 0f).toArgb()
|
val transparent = shadow.color.copy(alpha = 0f).toArgb()
|
||||||
val shadowOffset = Offset(-shadow.offset.x.toPx(), shadow.offset.y.toPx())
|
val shadowOffset = Offset(-shadow.offset.x.toPx(), shadow.offset.y.toPx())
|
||||||
.rotate(2 * Math.PI.toFloat() - angle)
|
.rotate(2 * Math.PI.toFloat() - angle)
|
||||||
|
|
||||||
|
// Prepare shadow paint with a shadow layer
|
||||||
val paint = Paint().apply {
|
val paint = Paint().apply {
|
||||||
val frameworkPaint = asFrameworkPaint()
|
val frameworkPaint = asFrameworkPaint()
|
||||||
frameworkPaint.color = transparent
|
frameworkPaint.color = transparent
|
||||||
@@ -166,6 +215,8 @@ private fun CacheDrawScope.prepareShadow(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hardware acceleration supports setShadowLayer() only on API 28 and above, thus to support previous API versions
|
||||||
|
// draw a shadow to the bitmap instead
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
prepareShadowApi28(radius, paint, polygon)
|
prepareShadowApi28(radius, paint, polygon)
|
||||||
} else {
|
} else {
|
||||||
@@ -193,6 +244,7 @@ private fun CacheDrawScope.prepareShadowImage(
|
|||||||
paint: Paint,
|
paint: Paint,
|
||||||
polygon: Polygon,
|
polygon: Polygon,
|
||||||
): ContentDrawScope.() -> Unit {
|
): ContentDrawScope.() -> Unit {
|
||||||
|
// Increase the size a little bit so that shadow is not clipped
|
||||||
val bitmap = Bitmap.createBitmap(
|
val bitmap = Bitmap.createBitmap(
|
||||||
(size.width + radius * 4).toInt(),
|
(size.width + radius * 4).toInt(),
|
||||||
(size.height + radius * 4).toInt(),
|
(size.height + radius * 4).toInt(),
|
||||||
@@ -201,6 +253,7 @@ private fun CacheDrawScope.prepareShadowImage(
|
|||||||
Canvas(bitmap).apply {
|
Canvas(bitmap).apply {
|
||||||
drawPath(
|
drawPath(
|
||||||
polygon
|
polygon
|
||||||
|
// As bitmap size is increased we should translate the polygon so that shadow remains in center
|
||||||
.translate(Offset(2 * radius, 2 * radius))
|
.translate(Offset(2 * radius, 2 * radius))
|
||||||
.offset(radius).toPath()
|
.offset(radius).toPath()
|
||||||
.asAndroidPath(),
|
.asAndroidPath(),
|
||||||
@@ -210,6 +263,7 @@ private fun CacheDrawScope.prepareShadowImage(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
drawIntoCanvas {
|
drawIntoCanvas {
|
||||||
|
// As bitmap size is increased we should shift the drawing so that shadow remains in center
|
||||||
it.nativeCanvas.drawBitmap(bitmap, -2 * radius, -2 * radius, null)
|
it.nativeCanvas.drawBitmap(bitmap, -2 * radius, -2 * radius, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -77,6 +77,7 @@ internal fun Modifier.curlGesture(
|
|||||||
return@pointerInput
|
return@pointerInput
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use velocity tracker to support flings
|
||||||
val velocityTracker = VelocityTracker()
|
val velocityTracker = VelocityTracker()
|
||||||
val startRect by lazy { direction.start.multiply(size) }
|
val startRect by lazy { direction.start.multiply(size) }
|
||||||
val endRect by lazy { direction.end.multiply(size) }
|
val endRect by lazy { direction.end.multiply(size) }
|
||||||
@@ -87,6 +88,7 @@ internal fun Modifier.curlGesture(
|
|||||||
return@awaitPointerEventScope
|
return@awaitPointerEventScope
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Change X position to be always on the right side for more convenient gesture tracking
|
||||||
val dragStart = down.position.copy(x = size.width.toFloat())
|
val dragStart = down.position.copy(x = size.width.toFloat())
|
||||||
|
|
||||||
onStart()
|
onStart()
|
||||||
|
@@ -10,6 +10,14 @@ import androidx.compose.ui.Modifier
|
|||||||
import eu.wewox.pagecurl.ExperimentalPageCurlApi
|
import eu.wewox.pagecurl.ExperimentalPageCurlApi
|
||||||
import eu.wewox.pagecurl.config.PageCurlConfig
|
import eu.wewox.pagecurl.config.PageCurlConfig
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the pages which may be rotated by drag or tap gestures.
|
||||||
|
*
|
||||||
|
* @param state The state of the PageCurl. Use this to programmatically change the current page or observe changes.
|
||||||
|
* @param modifier The modifier for this composable.
|
||||||
|
* @param config The configuration for PageCurl. Configures how page curl looks like and interacts.
|
||||||
|
* @param content The content lambda to provide the page composable. Receives the page number.
|
||||||
|
*/
|
||||||
@ExperimentalPageCurlApi
|
@ExperimentalPageCurlApi
|
||||||
@Composable
|
@Composable
|
||||||
public fun PageCurl(
|
public fun PageCurl(
|
||||||
|
@@ -22,6 +22,13 @@ import kotlinx.coroutines.coroutineScope
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remembers the [PageCurlState].
|
||||||
|
*
|
||||||
|
* @param max The max number of pages.
|
||||||
|
* @param initialCurrent The initial current page.
|
||||||
|
* @return The remembered [PageCurlState].
|
||||||
|
*/
|
||||||
@ExperimentalPageCurlApi
|
@ExperimentalPageCurlApi
|
||||||
@Composable
|
@Composable
|
||||||
public fun rememberPageCurlState(
|
public fun rememberPageCurlState(
|
||||||
@@ -46,14 +53,27 @@ public fun rememberPageCurlState(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The state of the PageCurl.
|
||||||
|
*
|
||||||
|
* @property max The max number of pages.
|
||||||
|
* @param initialCurrent The initial current page.
|
||||||
|
*/
|
||||||
@ExperimentalPageCurlApi
|
@ExperimentalPageCurlApi
|
||||||
public class PageCurlState(
|
public class PageCurlState(
|
||||||
public val max: Int,
|
public val max: Int,
|
||||||
initialCurrent: Int = 0,
|
initialCurrent: Int = 0,
|
||||||
) {
|
) {
|
||||||
|
/**
|
||||||
|
* The observable current page.
|
||||||
|
*/
|
||||||
public var current: Int by mutableStateOf(initialCurrent)
|
public var current: Int by mutableStateOf(initialCurrent)
|
||||||
internal set
|
internal set
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The observable progress as page is rotated.
|
||||||
|
* When going forward it changes from 0 to 1, when going backward it is going from 0 to -1.
|
||||||
|
*/
|
||||||
public val progress: Float get() = internalState?.progress ?: 0f
|
public val progress: Float get() = internalState?.progress ?: 0f
|
||||||
|
|
||||||
internal var internalState: InternalState? by mutableStateOf(null)
|
internal var internalState: InternalState? by mutableStateOf(null)
|
||||||
@@ -75,11 +95,21 @@ public class PageCurlState(
|
|||||||
internalState = InternalState(constraints, left, right, forward, backward)
|
internalState = InternalState(constraints, left, right, forward, backward)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantly snaps the state to the given page.
|
||||||
|
*
|
||||||
|
* @param value The page to snap to.
|
||||||
|
*/
|
||||||
public suspend fun snapTo(value: Int) {
|
public suspend fun snapTo(value: Int) {
|
||||||
current = value.coerceIn(0, max - 1)
|
current = value.coerceIn(0, max - 1)
|
||||||
internalState?.reset()
|
internalState?.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Go forward with an animation.
|
||||||
|
*
|
||||||
|
* @param block The animation block to animate a change.
|
||||||
|
*/
|
||||||
public suspend fun next(block: suspend Animatable<Edge, AnimationVector4D>.(Size) -> Unit = DefaultNext) {
|
public suspend fun next(block: suspend Animatable<Edge, AnimationVector4D>.(Size) -> Unit = DefaultNext) {
|
||||||
internalState?.animateTo(
|
internalState?.animateTo(
|
||||||
target = { current + 1 },
|
target = { current + 1 },
|
||||||
@@ -87,6 +117,11 @@ public class PageCurlState(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Go backward with an animation.
|
||||||
|
*
|
||||||
|
* @param block The animation block to animate a change.
|
||||||
|
*/
|
||||||
public suspend fun prev(block: suspend Animatable<Edge, AnimationVector4D>.(Size) -> Unit = DefaultPrev) {
|
public suspend fun prev(block: suspend Animatable<Edge, AnimationVector4D>.(Size) -> Unit = DefaultPrev) {
|
||||||
internalState?.animateTo(
|
internalState?.animateTo(
|
||||||
target = { current - 1 },
|
target = { current - 1 },
|
||||||
@@ -146,6 +181,9 @@ public class PageCurlState(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The wrapper to represent a line with 2 points: [top] and [bottom].
|
||||||
|
*/
|
||||||
public data class Edge(val top: Offset, val bottom: Offset) {
|
public data class Edge(val top: Offset, val bottom: Offset) {
|
||||||
|
|
||||||
internal val centerX: Float = (top.x + bottom.x) * 0.5f
|
internal val centerX: Float = (top.x + bottom.x) * 0.5f
|
||||||
|
Reference in New Issue
Block a user