Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: thêm custom thư viện thành công #106

Merged
merged 6 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@ android {
dependencies {
implementation(libs.purchases)
implementation(libs.purchases.ui)
implementation(libs.easycrop)
implementation(libs.accompanist.permissions)
implementation(platform(libs.firebase.bom))
implementation(libs.firebase.analytics)
Expand All @@ -103,6 +102,7 @@ dependencies {
implementation(libs.play.services.ads)

implementation(projects.composeCardstack)
implementation(projects.easycrop)
implementation(libs.lottie.compose)

// Compose
Expand Down
1 change: 1 addition & 0 deletions easycrop/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
54 changes: 54 additions & 0 deletions easycrop/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
}
android {
namespace = "com.mr0xf00.easycrop"
compileSdk = libs.versions.compileSdk.get().toInt()
compileSdk = libs.versions.compileSdk.get().toInt()
nqmgaming marked this conversation as resolved.
Show resolved Hide resolved

defaultConfig {
minSdk = libs.versions.minSdk.get().toInt()
}

testOptions {
targetSdk = libs.versions.targetSdk.get().toInt()
}

lint {
targetSdk = libs.versions.targetSdk.get().toInt()
}

buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = libs.versions.jvmTarget.get()
}
buildFeatures {
compose = true
}
}

dependencies {
implementation(platform(libs.androidx.compose.bom))
implementation(libs.bundles.compose)
// Unit Test
testImplementation(libs.bundles.testing)
// Android Test
androidTestImplementation(libs.bundles.android.testing)
androidTestImplementation(platform(libs.androidx.compose.bom))
// Debug Test
debugImplementation(libs.bundles.debugging)
}
90 changes: 90 additions & 0 deletions easycrop/src/androidTest/java/com/mr0xf00/easycrop/ResultTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.mr0xf00.easycrop

import android.graphics.Bitmap
import android.graphics.BitmapFactory
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.toPixelMap
import androidx.compose.ui.unit.IntSize
import androidx.test.platform.app.InstrumentationRegistry
import com.mr0xf00.easycrop.images.ImageStream
import com.mr0xf00.easycrop.images.ImageStreamSrc
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert
import org.junit.Test
import org.junit.Before

@OptIn(ExperimentalCoroutinesApi::class)
class ResultTest {

private lateinit var state: CropState
private val full = imageStream("dog.jpg")

@Before
fun createState() = runTest {
val src = ImageStreamSrc(full)
checkNotNull(src)
state = CropState(src)
}

@Test
fun image_is_unchanged_when_using_a_full_region_and_no_transform() = runTest {
val expected = full.openImage()
val actual = state.createResult(null)
assertEqual(expected, actual)
}

@Test
fun correct_result_when_applying_transforms() = runTest {
state.rotLeft()
state.flipHorizontal()
val expected = imageStream("dog_rl_fh.png").openImage()
val actual = state.createResult(null)
assertEqual(expected, actual)
}

@Test
fun correct_result_when_resizing_region_and_applying_transforms() = runTest {
state.rotLeft()
state.flipHorizontal()
state.region = Rect(Offset(294f, 86f), Size(182f, 143f))
val expected = imageStream("dog_rl_fh_294_86_182_143.png").openImage()
val actual = state.createResult(null)?.apply { save() }
assertEqual(expected, actual)
}

private fun imageStream(name: String): ImageStream {
return ImageStream { javaClass.classLoader!!.getResourceAsStream(name) }
}
nqmgaming marked this conversation as resolved.
Show resolved Hide resolved
}

private fun ImageStream.openImage(): ImageBitmap {
return BitmapFactory.decodeStream(openStream(), null, null)?.asImageBitmap()
?: error("Image $this cannot be opened")
}

private fun assertEqual(expected: ImageBitmap, actual: ImageBitmap?) {
checkNotNull(actual)
Assert.assertEquals(
IntSize(expected.width, expected.height),
IntSize(actual.width, actual.height)
)
Assert.assertArrayEquals(
expected.toPixelMap().buffer,
actual.toPixelMap().buffer
)
}

private fun ImageBitmap.save() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
context.filesDir.resolve("result.png").outputStream().use { stream ->
asAndroidBitmap().compress(
Bitmap.CompressFormat.PNG, 100, stream
)
}
}
nqmgaming marked this conversation as resolved.
Show resolved Hide resolved
Binary file added easycrop/src/androidTest/resources/dog.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added easycrop/src/androidTest/resources/dog_rl_fh.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions easycrop/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

</manifest>
65 changes: 65 additions & 0 deletions easycrop/src/main/java/com/mr0xf00/easycrop/CropShapes.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.mr0xf00.easycrop

import androidx.compose.runtime.Stable
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.RoundRect
import androidx.compose.ui.graphics.Path
import com.mr0xf00.easycrop.utils.polygonPath

/**
* A Shape used to clip the resulting image.
* Implementations should provide a meaningful equals method,
* such as (A == B) => A.asPath(R) == B.asPath(R)
*/
@Stable
fun interface CropShape {
fun asPath(rect: Rect): Path
}

@Stable
val RectCropShape = CropShape { rect -> Path().apply { addRect(rect) } }

@Stable
val CircleCropShape = CropShape { rect -> Path().apply { addOval(rect) } }

@Stable
val TriangleCropShape = CropShape { rect ->
Path().apply {
moveTo(rect.left, rect.bottom)
lineTo(rect.center.x, rect.top)
lineTo(rect.right, rect.bottom)
close()
}
}

val StarCropShape = CropShape { rect ->
polygonPath(
tx = rect.left, ty = rect.top,
sx = rect.width / 32, sy = rect.height / 32,
points = floatArrayOf(
31.95f, 12.418856f,
20.63289f, 11.223692f,
16f, 0.83228856f,
11.367113f, 11.223692f,
0.05000003f, 12.418856f,
8.503064f, 20.03748f,
6.1431603f, 31.167711f,
16f, 25.48308f,
25.85684f, 31.167711f,
23.496937f, 20.03748f
)
)
}

data class RoundRectCropShape(private val cornersPercent: Int) : CropShape {
override fun asPath(rect: Rect): Path {
val radius = CornerRadius(rect.minDimension * cornersPercent / 100f)
return Path().apply { addRoundRect(RoundRect(rect = rect, radius)) }
}
}
nqmgaming marked this conversation as resolved.
Show resolved Hide resolved

val DefaultCropShapes = listOf(
RectCropShape, CircleCropShape, RoundRectCropShape(15),
StarCropShape, TriangleCropShape
)
126 changes: 126 additions & 0 deletions easycrop/src/main/java/com/mr0xf00/easycrop/CropState.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package com.mr0xf00.easycrop

import androidx.compose.runtime.*
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.geometry.toRect
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.toIntRect
import androidx.compose.ui.unit.toSize
import com.mr0xf00.easycrop.utils.*
import com.mr0xf00.easycrop.utils.constrainOffset
import com.mr0xf00.easycrop.utils.eq
import com.mr0xf00.easycrop.utils.next90
import com.mr0xf00.easycrop.utils.prev90
import com.mr0xf00.easycrop.images.ImageSrc

/** State for the current image being cropped */
@Stable
interface CropState {
val src: ImageSrc
var transform: ImgTransform
var region: Rect
var aspectLock: Boolean
var shape: CropShape
val accepted: Boolean
fun done(accept: Boolean)
fun reset()
}

internal fun CropState(
src: ImageSrc,
onDone: () -> Unit = {},
): CropState = object : CropState {
val defaultTransform: ImgTransform = ImgTransform.Identity
val defaultShape: CropShape = RectCropShape
val defaultAspectLock: Boolean = false
override val src: ImageSrc get() = src
private var _transform: ImgTransform by mutableStateOf(defaultTransform)
override var transform: ImgTransform
get() = _transform
set(value) {
onTransformUpdated(transform, value)
_transform = value
}

val defaultRegion = src.size.toSize().toRect()

private var _region by mutableStateOf(defaultRegion)
override var region
get() = _region
set(value) {
// _region = value
_region = updateRegion(
old = _region, new = value,
bounds = imgRect, aspectLock = aspectLock
)
}

val imgRect by derivedStateOf { getTransformedImageRect(transform, src.size) }

override var shape: CropShape by mutableStateOf(defaultShape)
override var aspectLock by mutableStateOf(defaultAspectLock)

private fun onTransformUpdated(old: ImgTransform, new: ImgTransform) {
val unTransform = old.asMatrix(src.size).apply { invert() }
_region = new.asMatrix(src.size).map(unTransform.map(region))
}

override fun reset() {
transform = defaultTransform
shape = defaultShape
_region = defaultRegion
aspectLock = defaultAspectLock
}

override var accepted: Boolean by mutableStateOf(false)

override fun done(accept: Boolean) {
accepted = accept
onDone()
}
}

internal fun getTransformedImageRect(transform: ImgTransform, size: IntSize): Rect {
val dstMat = transform.asMatrix(size)
return dstMat.map(size.toIntRect().toRect())
}

internal fun CropState.rotLeft() {
transform = transform.copy(angleDeg = transform.angleDeg.prev90())
}

internal fun CropState.rotRight() {
transform = transform.copy(angleDeg = transform.angleDeg.next90())
}

internal fun CropState.flipHorizontal() {
if ((transform.angleDeg / 90) % 2 == 0) flipX() else flipY()
}

internal fun CropState.flipVertical() {
if ((transform.angleDeg / 90) % 2 == 0) flipY() else flipX()
}

internal fun CropState.flipX() {
transform = transform.copy(scale = transform.scale.copy(x = -1 * transform.scale.x))
}

internal fun CropState.flipY() {
transform = transform.copy(scale = transform.scale.copy(y = -1 * transform.scale.y))
}

internal fun updateRegion(old: Rect, new: Rect, bounds: Rect, aspectLock: Boolean): Rect {
val offsetOnly = old.width.eq(new.width) && old.height.eq(new.height)
return if (offsetOnly) new.constrainOffset(bounds)
else {
val result = when {
aspectLock -> new.keepAspect(old).scaleToFit(bounds, old)
else -> new.constrainResize(bounds)
}
return when {
result.isEmpty -> result.setSize(old, Size(1f, 1f)).constrainOffset(bounds)
else -> result
}
}
}
9 changes: 9 additions & 0 deletions easycrop/src/main/java/com/mr0xf00/easycrop/Cropper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.mr0xf00.easycrop








Loading
Loading