Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,14 @@
.externalNativeBuild
.cxx
local.properties

# Kotlin incremental compilation cache
.kotlin/

# Module-level build output (root /build above only covers the top-level dir)
**/build/

# GPG keyring files — never commit signing keys
*.gpg
*.kbx
secring.gpg
31 changes: 28 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,31 @@
# Changelog

## Unreleased

### imagepicker
- Fluent builder API: `ImagePicker.with(context, caller).source().crop().onResult().onError().launch()`
- Gallery picking via `ActivityResultContracts.GetContent`
- Camera capture via `ActivityResultContracts.TakePicture` with automatic EXIF orientation correction
- `MediaSource.Gallery`, `MediaSource.Camera`, `MediaSource.Both` (Both falls back to Gallery)
- Sealed `ImagePickerResult`: `Success`, `SuccessWithBitmap`, `Cancelled`, `Error`
- Sealed `ImagePickerException`: `PermissionDenied`, `AppNotFound`, `FileCreationFailed`, `InvalidUri`, `DecodingFailed`, `FileDeletionFailed`, `IntentFailed`, `Unknown`
- Automatic CAMERA permission request before camera launch
- `CropImageLauncher` — chains gallery/camera result into `CropperActivity` when `crop(true)`
- `ImagePickerFileProvider` — isolated FileProvider subclass; authority `{applicationId}.imagepicker.provider`
- `<queries>` manifest entry for camera intent visibility on Android 11+
- All `registerForActivityResult` calls happen at construction time (before `onStart`)

### imagecropper
- `CropperView` — custom View with matrix-based image fit, touch-drag/resize crop rect
- `CropOverlayDrawer` — dimmed overlay, border, rule-of-thirds grid, L-shaped corner handles
- `CropTouchHandler` — detects MOVE / corner / edge areas; enforces `MIN_CROP_SIZE`
- `CropperSavedState` — full Parcelable save/restore of crop rect across rotation
- `CropperActivity` — standalone Activity: receives URI via `EXTRA_INPUT_URI`, returns cropped JPEG URI via `EXTRA_OUTPUT_URI` using `FileProvider` (`{applicationId}.imagecropper.provider`)
- `getCroppedImage()` — returns cropped `Bitmap` from current rect, clamped to image bounds

### sample-app
- Compose UI: empty state → gallery pick → crop → result display
- Error and exception feedback via Toast

## 0.1.0
- Initial MediaKit OSS setup
- Added README
- Prepared repository structure for publishing
- Initial repository structure and OSS setup
76 changes: 73 additions & 3 deletions ImageCropper/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
`maven-publish`
signing
}

android {
Expand Down Expand Up @@ -30,14 +32,82 @@ android {
kotlinOptions {
jvmTarget = "11"
}

publishing {
singleVariant("release") {
withSourcesJar()
withJavadocJar()
}
}
}

dependencies {

api(project(":imagepicker"))
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.activity.ktx)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
}
}

afterEvaluate {
publishing {
publications {
create<MavenPublication>("release") {
from(components["release"])
groupId = project.findProperty("GROUP_ID") as String
artifactId = "imagecropper"
version = project.findProperty("VERSION_NAME") as String

pom {
name.set("MediaKit ImageCropper")
description.set("Touch-driven image cropping for Android. Custom CropperView with rule-of-thirds grid, corner handles, and state restoration. Integrates with MediaKit ImagePicker or standalone.")
url.set("https://github.com/AkshayAshokCode/MediaKit-android")
licenses {
license {
name.set("MIT License")
url.set("https://opensource.org/licenses/MIT")
}
}
developers {
developer {
id.set("akshayashokcode")
name.set("Akshay Ashok")
email.set("akshayashokan1054@gmail.com")
}
}
scm {
connection.set("scm:git:github.com/AkshayAshokCode/MediaKit-android.git")
developerConnection.set("scm:git:ssh://github.com/AkshayAshokCode/MediaKit-android.git")
url.set("https://github.com/AkshayAshokCode/MediaKit-android/tree/main")
}
}
}
}

repositories {
maven {
name = "sonatype"
url = uri(
if ((project.findProperty("VERSION_NAME") as String).endsWith("SNAPSHOT"))
"https://s01.oss.sonatype.org/content/repositories/snapshots/"
else
"https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/"
)
credentials {
username = project.findProperty("OSSRH_USERNAME") as String? ?: ""
password = project.findProperty("OSSRH_PASSWORD") as String? ?: ""
}
}
}
}

signing {
val signingKeyId = project.findProperty("SIGNING_KEY_ID") as String?
val signingKey = project.findProperty("SIGNING_KEY") as String?
val signingPassword = project.findProperty("SIGNING_PASSWORD") as String?
useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword)
sign(publishing.publications["release"])
}
}
8 changes: 8 additions & 0 deletions ImageCropper/consumer-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Keep CropperActivity — launched by intent from MediaKitCropProvider
-keep class com.akshayashokcode.imagecropper.CropperActivity { *; }

# Keep MediaKitCropProvider so consumers can instantiate it by name or reflection
-keep class com.akshayashokcode.imagecropper.MediaKitCropProvider { *; }

# Keep CropperView for consumers embedding it directly in XML layouts
-keep class com.akshayashokcode.imagecropper.CropperView { *; }
16 changes: 15 additions & 1 deletion ImageCropper/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".CropperActivity"
android:exported="false"/>

</manifest>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.imagecropper.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/imagecropper_file_paths"/>
</provider>
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.akshayashokcode.imagecropper

import android.content.Intent
import android.graphics.BitmapFactory
import android.os.Bundle
import android.widget.Button
import androidx.activity.ComponentActivity
import androidx.core.content.FileProvider
import androidx.core.net.toUri
import java.io.File
import java.io.FileOutputStream

class CropperActivity : ComponentActivity() {

companion object {
const val EXTRA_INPUT_URI = "extra_input_uri"
const val EXTRA_OUTPUT_URI = "extra_output_uri"
}

private lateinit var cropperView: CropperView

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setContentView(R.layout.activity_cropper)

cropperView = findViewById(R.id.cropperView)

val cropButton = findViewById<Button>(R.id.buttonCrop)

val uriString = intent.getStringExtra(EXTRA_INPUT_URI)

if (uriString == null) {
finish()
return
}

val inputUri = uriString.toUri()

val bitmap = contentResolver.openInputStream(inputUri)?.use { BitmapFactory.decodeStream(it) }
if (bitmap == null) {
setResult(RESULT_CANCELED)
finish()
return
}
cropperView.setImageBitmap(bitmap)

cropButton.setOnClickListener {
val croppedBitmap = cropperView.getCroppedImage()

if (croppedBitmap == null) {
setResult(RESULT_CANCELED)
finish()
return@setOnClickListener
}

val outputFile = File(cacheDir, "cropped_${System.currentTimeMillis()}.jpg")

FileOutputStream(outputFile).use { outputStream ->
croppedBitmap.compress(android.graphics.Bitmap.CompressFormat.JPEG, 100, outputStream)
}

val outputUri = FileProvider.getUriForFile(
this,
"${packageName}.imagecropper.provider",
outputFile
)

setResult(
RESULT_OK,
Intent().apply {
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
putExtra(EXTRA_OUTPUT_URI, outputUri.toString())
}
)

finish()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.akshayashokcode.imagecropper.internal

import android.graphics.*

class CropOverlayDrawer {
internal class CropOverlayDrawer {

val bitmapPaint = Paint(Paint.ANTI_ALIAS_FLAG)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package com.akshayashokcode.imagecropper.internal
import android.graphics.RectF
import kotlin.math.abs

class CropTouchHandler(private val cropRect: RectF) {
internal class CropTouchHandler(private val cropRect: RectF) {

enum class Area { NONE, MOVE, TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.akshayashokcode.imagecropper.internal

import androidx.core.graphics.toColorInt

object CropConstants {
internal object CropConstants {
const val TOUCH_THRESHOLD = 40f
const val MIN_CROP_SIZE = 200f
const val CORNER_STROKE_WIDTH = 10f
Expand Down
20 changes: 20 additions & 0 deletions ImageCropper/src/main/res/layout/activity_cropper.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>

<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<com.akshayashokcode.imagecropper.CropperView
android:id="@+id/cropperView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>

<Button
android:id="@+id/buttonCrop"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Crop"/>
</LinearLayout>
Loading
Loading