diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..fb7f4a8 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml new file mode 100644 index 0000000..058aee2 --- /dev/null +++ b/.idea/deploymentTargetDropDown.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..2370474 --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 941ce03..ca65a49 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -3,11 +3,11 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' android { - compileSdkVersion 28 + compileSdkVersion 31 defaultConfig { applicationId "com.divyanshu.androiddraw" minSdkVersion 21 - targetSdkVersion 28 + targetSdkVersion 31 versionCode 3 versionName "1.0.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -31,19 +31,19 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" //AndroidX - implementation 'androidx.appcompat:appcompat:1.0.2' - implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.appcompat:appcompat:1.4.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.2' implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.cardview:cardview:1.0.0' - implementation 'com.google.android.material:material:1.0.0' + implementation 'com.google.android.material:material:1.4.0' //Annotation - implementation 'com.github.bumptech.glide:glide:4.8.0' - annotationProcessor 'com.github.bumptech.glide:compiler:4.7.1' + implementation 'com.github.bumptech.glide:glide:4.12.0' + annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0' //Test - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.2.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test:runner:1.4.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e41b6c1..3afd872 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,7 +11,8 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> - + diff --git a/build.gradle b/build.gradle index e0bf29a..61ad121 100644 --- a/build.gradle +++ b/build.gradle @@ -1,13 +1,14 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.5.21' + ext.compose_version = '1.0.5' + ext.kotlin_version = '1.5.31' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.2.2' + classpath 'com.android.tools.build:gradle:7.0.4' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong @@ -19,6 +20,7 @@ allprojects { repositories { google() jcenter() + maven { url 'https://jitpack.io' } } } diff --git a/draw/build.gradle b/draw/build.gradle index 45b345f..58bce81 100644 --- a/draw/build.gradle +++ b/draw/build.gradle @@ -1,12 +1,10 @@ apply plugin: 'com.android.library' android { - compileSdkVersion 28 + compileSdkVersion 31 defaultConfig { minSdkVersion 19 - targetSdkVersion 28 - versionCode 1 - versionName "1.0" + targetSdkVersion 31 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -23,17 +21,17 @@ dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') //AndroidX - implementation 'androidx.appcompat:appcompat:1.0.2' - implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.appcompat:appcompat:1.4.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.2' - implementation 'com.google.android.material:material:1.0.0' + implementation 'com.google.android.material:material:1.4.0' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" //Test - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.2.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test:runner:1.4.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' } apply plugin: 'kotlin-android' diff --git a/draw/src/main/AndroidManifest.xml b/draw/src/main/AndroidManifest.xml index 19e1db7..a8587d2 100644 --- a/draw/src/main/AndroidManifest.xml +++ b/draw/src/main/AndroidManifest.xml @@ -5,7 +5,8 @@ + android:theme="@style/Theme.AppCompat.NoActionBar" + android:exported="true"/> \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 4d9ca16..0f80bbf 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/jpc/.gitignore b/jpc/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/jpc/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/jpc/README.md b/jpc/README.md new file mode 100644 index 0000000..10d937d --- /dev/null +++ b/jpc/README.md @@ -0,0 +1,16 @@ +### Purpose: + +This is a sample app intended to show a way of using DrawView in Jetpack Compose + +### How to run: + +Choose the ‘jpc’ instead of ‘app’ to run the program + +### Notes from the contributer: + +* The app doesn’t load images asynchronously. Implementation of asynchronous loading is recommended +* The app expects users to allow access to storage. Handling denial is necessary in real app +* In the ListScreen, saved images are shown in grid. Taps on each item won’t cause anything. Implement onClick functionality if needed +* Coloring and sizing etc hard-coded + +Thanks divyanshub024 for creating such a useful library. \ No newline at end of file diff --git a/jpc/build.gradle b/jpc/build.gradle new file mode 100644 index 0000000..9645b64 --- /dev/null +++ b/jpc/build.gradle @@ -0,0 +1,70 @@ +plugins { + id 'com.android.application' + id 'kotlin-android' +} + +android { + compileSdkVersion 31 + + defaultConfig { + applicationId "com.divyanshu.androiddraw.jpc" + minSdkVersion 21 + targetSdkVersion 31 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary true + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + useIR = true + } + buildFeatures { + compose true + } + composeOptions { + kotlinCompilerExtensionVersion compose_version + kotlinCompilerVersion '1.5.21' + } + packagingOptions { + resources { + excludes += '/META-INF/{AL2.0,LGPL2.1}' + } + } +} + +dependencies { + + implementation 'androidx.core:core-ktx:1.7.0' + implementation 'androidx.appcompat:appcompat:1.4.0' + implementation 'com.google.android.material:material:1.4.0' + implementation "androidx.compose.ui:ui:$compose_version" + implementation "androidx.compose.material:material:$compose_version" + implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0' + implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.4.0' + implementation 'androidx.activity:activity-compose:1.4.0' + implementation "androidx.navigation:navigation-compose:2.4.0-beta02" + + implementation 'com.github.divyanshub024:AndroidDraw:v0.1' + + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" + debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" +} \ No newline at end of file diff --git a/jpc/proguard-rules.pro b/jpc/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/jpc/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/jpc/src/androidTest/java/com/divyanshu/androiddraw/jpc/ExampleInstrumentedTest.kt b/jpc/src/androidTest/java/com/divyanshu/androiddraw/jpc/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..ab81d62 --- /dev/null +++ b/jpc/src/androidTest/java/com/divyanshu/androiddraw/jpc/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.divyanshu.androiddraw.jpc + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.divyanshu.androiddraw.jpc", appContext.packageName) + } +} \ No newline at end of file diff --git a/jpc/src/main/AndroidManifest.xml b/jpc/src/main/AndroidManifest.xml new file mode 100644 index 0000000..848c0c9 --- /dev/null +++ b/jpc/src/main/AndroidManifest.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/jpc/src/main/java/com/divyanshu/androiddraw/jpc/DrawScreen.kt b/jpc/src/main/java/com/divyanshu/androiddraw/jpc/DrawScreen.kt new file mode 100644 index 0000000..151c2b2 --- /dev/null +++ b/jpc/src/main/java/com/divyanshu/androiddraw/jpc/DrawScreen.kt @@ -0,0 +1,386 @@ +package com.divyanshu.androiddraw.jpc + +import android.util.Xml +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.core.animateDp +import androidx.compose.animation.core.updateTransition +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Close +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.navigation.NavController +import org.xmlpull.v1.XmlPullParser +import com.divyanshu.draw.widget.DrawView +import java.util.* + +@ExperimentalAnimationApi +@Composable +fun DrawScreen( + navController: NavController +) { + val context = LocalContext.current + + // States in which a particular tool is selected + val showDrawTools = remember { mutableStateOf(false) } // Needed for ColorPalette, StrokeWidth, and Opacity + val isEraserSelected = remember { mutableStateOf(false) } + val isColorPaletteSelected = remember { mutableStateOf(false) } + val isStrokeWidthSelected = remember { mutableStateOf(false) } + val isOpacitySelected = remember { mutableStateOf(false) } + val isUndoSelected = remember { mutableStateOf(false) } + val isRedoSelected = remember { mutableStateOf(false) } + + // Currently-selected color, stroke width, and opacity + val pathColor = remember { mutableStateOf(Color.Black) } + val strokeWidth = remember { mutableStateOf(5f) } + val opacity = remember { mutableStateOf(100f) } + + // Used to animate the ToolBar going up and down based on the visibility of DrawTools + val transition = updateTransition(showDrawTools.value) + val elevation by transition.animateDp { isSelected -> + if (isSelected) 40.dp else 0.dp + } + + // The color of the eraser. It has to be the same as the background color + val backgroundColor = MaterialTheme.colors.background + + // For saving + val showSaveDialog = remember { mutableStateOf(false) } + val resultBitmap = remember { mutableStateOf(Utils.createEmptyBitmap(1, 1)) } + + Box( + modifier = Modifier.fillMaxSize() + ) { + // Get the DrawView from xml + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { + val parser: XmlPullParser = it.resources.getXml(R.xml.draw_view) + try { + parser.next() + parser.nextTag() + } catch (e: Exception) { + e.printStackTrace() + } + val attrs = Xml.asAttributeSet(parser) + DrawView(it, attrs) + } + ) { drawView -> + drawView.setColor(if (isEraserSelected.value) backgroundColor.toArgb() else pathColor.value.toArgb()) + drawView.setStrokeWidth(strokeWidth.value) + drawView.setAlpha(if (isEraserSelected.value) 100 else opacity.value.toInt()) + + if (isUndoSelected.value) { + drawView.undo() + isUndoSelected.value = false + } + if (isRedoSelected.value) { + drawView.redo() + isRedoSelected.value = false + } + if (showSaveDialog.value) { + resultBitmap.value = drawView.getBitmap() + } + } + // Close button + IconButton( + modifier = Modifier + .align(Alignment.TopStart) + .size(50.dp) + .padding(4.dp), + onClick = { navController.navigateUp() } + ) { + Icon(imageVector = Icons.Default.Close, tint = Color.Gray, contentDescription = "Close") + } + // Save button + IconButton( + modifier = Modifier + .align(Alignment.TopEnd) + .size(50.dp) + .padding(4.dp) + .clip(CircleShape) + .background(Color.Black), + onClick = { + showSaveDialog.value = true + } + ) { + Icon(imageVector = Icons.Default.Check, tint = Color.White, contentDescription = "Save") + } + // Toolbar at the bottom + Row( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + .padding(0.dp, 0.dp, 0.dp, elevation), + horizontalArrangement = Arrangement.Center, + ) { + IconButton( + onClick = { + showDrawTools.value = false + isEraserSelected.value = !isEraserSelected.value + } + ) { + Icon( + painter = painterResource(id = R.drawable.ic_eraser_black_24dp), + tint = if(isEraserSelected.value) Color.Black else Color.Gray, + contentDescription = "Eraser" + ) + } + IconButton( + onClick = { + showDrawTools.value = true + isStrokeWidthSelected.value = true + isColorPaletteSelected.value = false + isOpacitySelected.value = false + } + ) { + Icon( + painter = painterResource(id = R.drawable.ic_adjust_black_24dp), + tint = Color.Gray, + contentDescription = "Stroke Width" + ) + } + IconButton( + onClick = { + showDrawTools.value = true + isStrokeWidthSelected.value = false + isColorPaletteSelected.value = true + isOpacitySelected.value = false + } + ) { + Icon( + painter = painterResource(id = R.drawable.ic_color_lens_black_24dp), + tint = Color.Gray, + contentDescription = "Color" + ) + } + IconButton( + onClick = { + showDrawTools.value = true + isStrokeWidthSelected.value = false + isColorPaletteSelected.value = false + isOpacitySelected.value = true + } + ) { + Icon( + painter = painterResource(id = R.drawable.ic_opacity_black_24dp), + tint = Color.Gray, + contentDescription = "Opacity" + ) + } + IconButton( + onClick = { + showDrawTools.value = false + isUndoSelected.value = true + } + ) { + Icon( + painter = painterResource(id = R.drawable.ic_undo_black_24dp), + tint = Color.Gray, + contentDescription = "Undo" + ) + } + IconButton( + onClick = { + showDrawTools.value = false + isRedoSelected.value = true + } + ) { + Icon( + painter = painterResource(id = R.drawable.ic_redo_black_24dp), + tint = Color.Gray, + contentDescription = "Redo" + ) + } + } + // Box that comes up animated from the bottom. Only for ColorPalette, StrokeWidth, and Opacity + AnimatedVisibility( + modifier = Modifier.align(Alignment.BottomCenter), + visible = showDrawTools.value + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(50.dp) + .padding(15.dp, 2.dp) + ) { + when { + isColorPaletteSelected.value -> { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .size(if (pathColor.value == Color.Black) 38.dp else 32.dp) + .clip(CircleShape) + .background(Color.Black) + .clickable { pathColor.value = Color.Black } + ) + Box( + modifier = Modifier + .size(if (pathColor.value == Color.Red) 38.dp else 32.dp) + .clip(CircleShape) + .background(Color.Red) + .clickable { pathColor.value = Color.Red } + ) + Box( + modifier = Modifier + .size(if (pathColor.value == Color.Yellow) 38.dp else 32.dp) + .clip(CircleShape) + .background(Color.Yellow) + .clickable { pathColor.value = Color.Yellow } + ) + Box( + modifier = Modifier + .size(if (pathColor.value == Color.Green) 38.dp else 32.dp) + .clip(CircleShape) + .background(Color.Green) + .clickable { pathColor.value = Color.Green } + ) + Box( + modifier = Modifier + .size(if (pathColor.value == Color.Blue) 38.dp else 32.dp) + .clip(CircleShape) + .background(Color.Blue) + .clickable { pathColor.value = Color.Blue } + ) + Box( + modifier = Modifier + .size(if (pathColor.value == Color.Gray) 38.dp else 32.dp) + .clip(CircleShape) + .background(Color.Gray) + .clickable { pathColor.value = Color.Gray } + ) + Box( + modifier = Modifier + .size(if (pathColor.value == Color.Magenta) 38.dp else 32.dp) + .clip(CircleShape) + .background(Color.Magenta) + .clickable { pathColor.value = Color.Magenta } + ) + } + } + isStrokeWidthSelected.value -> { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Slider( + modifier = Modifier + .padding(5.dp, 0.dp) + .weight(1f), + value = strokeWidth.value, + onValueChange = { strokeWidth.value = it }, + valueRange = 0f..100f, + steps = 100, + colors = SliderDefaults.colors( + thumbColor = MaterialTheme.colors.secondary, + activeTrackColor = MaterialTheme.colors.secondary + ) + ) + Box( + modifier = Modifier + .size(40.dp) + .padding(2.dp) + ) { + Box( + modifier = Modifier + .size(36.dp * strokeWidth.value / 100f) + .clip(CircleShape) + .background(pathColor.value) + ) + } + } + } + isOpacitySelected.value -> { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Slider( + modifier = Modifier + .padding(5.dp, 0.dp) + .weight(1f), + value = opacity.value, + onValueChange = { opacity.value = it }, + valueRange = 0f..100f, + steps = 100, + colors = SliderDefaults.colors( + thumbColor = MaterialTheme.colors.secondary, + activeTrackColor = MaterialTheme.colors.secondary + ) + ) + Box( + modifier = Modifier + .size(40.dp) + .padding(2.dp) + .clip(CircleShape) + .background( + // alpha has to be within the range: 0f..1f + pathColor.value.copy(alpha = opacity.value / 100f) + ) + ) + } + } + } + } + } + } + + if (showSaveDialog.value) { + val fileName = remember { mutableStateOf(UUID.randomUUID().toString()) } + AlertDialog( + onDismissRequest = { showSaveDialog.value = false }, + title = { + Text( + text = "Save Drawing", + fontWeight = FontWeight.Bold + ) + }, + text = { + OutlinedTextField( + value = fileName.value, + maxLines = 1, + onValueChange = { fileName.value = it } + ) + }, + dismissButton = { + OutlinedButton( + onClick = { showSaveDialog.value = false } + ) { + Text(text = "Cancel") + } + }, + confirmButton = { + OutlinedButton( + onClick = { + Utils.saveImage(context, resultBitmap.value, fileName.value) + navController.navigateUp() + } + ) { + Text(text = "OK") + } + } + ) + } +} \ No newline at end of file diff --git a/jpc/src/main/java/com/divyanshu/androiddraw/jpc/ListScreen.kt b/jpc/src/main/java/com/divyanshu/androiddraw/jpc/ListScreen.kt new file mode 100644 index 0000000..e5d1908 --- /dev/null +++ b/jpc/src/main/java/com/divyanshu/androiddraw/jpc/ListScreen.kt @@ -0,0 +1,151 @@ +package com.divyanshu.androiddraw.jpc + +import android.Manifest +import android.content.pm.PackageManager +import android.graphics.ImageDecoder +import android.os.Build +import android.provider.MediaStore +import android.util.Log +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.GridCells +import androidx.compose.foundation.lazy.LazyVerticalGrid +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat +import androidx.navigation.NavController + +@ExperimentalFoundationApi +@Composable +fun ListScreen( + navController: NavController +) { + val context = LocalContext.current + val scaffoldState = rememberScaffoldState() + val gridListState = rememberLazyListState() + + val imagePathList = remember { mutableStateOf(Utils.getImagePathList(context)) } + + val launcher = rememberLauncherForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted: Boolean -> + if (isGranted) { + Log.d("ListScreen","PERMISSION GRANTED") + } else { + Log.d("ListScreen","PERMISSION DENIED") + } + } + + LaunchedEffect(Unit) { + when (PackageManager.PERMISSION_GRANTED) { + ContextCompat.checkSelfPermission( + context, Manifest.permission.WRITE_EXTERNAL_STORAGE + ) -> { + Log.d("ListScreen","Has WRITE_EXTERNAL_STORAGE permission") + } + else -> { + // Asking for permission + launcher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) + } + } + + imagePathList.value = Utils.getImagePathList(context) + } + + Scaffold( + floatingActionButton = { + FloatingActionButton( + onClick = { + navController.navigate("draw") + }, + backgroundColor = MaterialTheme.colors.primary + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = "Add Note" + ) + } + }, + scaffoldState = scaffoldState + ) { + Column(modifier = Modifier.fillMaxSize()) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(48.dp) + .background(Color.Black) + ) { + Text( + modifier = Modifier + .align(Alignment.CenterStart) + .padding(10.dp, 0.dp), + text = "Android Draw", + fontWeight = FontWeight.Bold, + color = Color.White + ) + } + LazyVerticalGrid( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + cells = GridCells.Fixed(count = 2), + state = gridListState, + contentPadding = PaddingValues( + start = 6.dp, + top = 8.dp, + end = 6.dp, + bottom = 8.dp + ), + content = { + items(imagePathList.value.size) { index -> + val imagePath = imagePathList.value[index] + + val bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + val source = ImageDecoder.createSource(context.contentResolver, imagePath) + ImageDecoder.decodeBitmap(source) + } + else { + MediaStore.Images.Media.getBitmap(context.contentResolver, imagePath) + } + + bitmap?.let { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(1.dp), + shape = RoundedCornerShape(15.dp), + border = BorderStroke(width = 1.dp, color = Color.Gray) + ) { + Image( + modifier = Modifier + .fillMaxWidth() + .clickable { + // todo: do something upon click + }, + bitmap = bitmap.asImageBitmap(), + contentDescription = "Image" + ) + } + } + } + } + ) + } + } +} \ No newline at end of file diff --git a/jpc/src/main/java/com/divyanshu/androiddraw/jpc/MainActivity.kt b/jpc/src/main/java/com/divyanshu/androiddraw/jpc/MainActivity.kt new file mode 100644 index 0000000..530ae6b --- /dev/null +++ b/jpc/src/main/java/com/divyanshu/androiddraw/jpc/MainActivity.kt @@ -0,0 +1,43 @@ +package com.divyanshu.androiddraw.jpc + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.core.content.ContextCompat +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import com.divyanshu.androiddraw.jpc.ui.theme.AndroidDrawTheme + +@ExperimentalFoundationApi +@ExperimentalAnimationApi +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + this.window.statusBarColor = ContextCompat.getColor(this,R.color.black) + + AndroidDrawTheme { + Surface(color = MaterialTheme.colors.background) { + val navController = rememberNavController() + + NavHost( + navController = navController, + startDestination = "list" + ) { + composable(route = "list") { + ListScreen(navController = navController) + } + composable(route = "draw") { + DrawScreen(navController = navController) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/jpc/src/main/java/com/divyanshu/androiddraw/jpc/Utils.kt b/jpc/src/main/java/com/divyanshu/androiddraw/jpc/Utils.kt new file mode 100644 index 0000000..eaf615e --- /dev/null +++ b/jpc/src/main/java/com/divyanshu/androiddraw/jpc/Utils.kt @@ -0,0 +1,98 @@ +package com.divyanshu.androiddraw.jpc + +import android.content.ContentUris +import android.content.ContentValues +import android.content.Context +import android.graphics.Bitmap +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.MediaStore +import android.util.Log +import androidx.core.net.toUri +import java.util.* +import java.io.File +import java.io.FileOutputStream + +val imageDir = "${Environment.DIRECTORY_PICTURES}/Android Draw/" + +object Utils { + + fun createEmptyBitmap(w: Int, h: Int): Bitmap { + val conf = Bitmap.Config.ARGB_8888 + return Bitmap.createBitmap(w, h, conf) + } + + fun saveImage(context: Context, bitmap: Bitmap, fileName: String) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val resolver = context.contentResolver + val contentValues = ContentValues().apply { + put(MediaStore.MediaColumns.DISPLAY_NAME, fileName) + put(MediaStore.MediaColumns.MIME_TYPE, "image/png") + put(MediaStore.MediaColumns.RELATIVE_PATH, imageDir) + } + + val uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) + + uri?.let { + resolver.openOutputStream(it).use { outputStream -> + bitmap.compress(Bitmap.CompressFormat.PNG,100, outputStream) + } + } + } + else { + val path = Environment.getExternalStoragePublicDirectory(imageDir) + Log.e("path",path.toString()) + val file = File(path, "$fileName.png") + path.mkdirs() + file.createNewFile() + val outputStream = FileOutputStream(file) + bitmap.compress(Bitmap.CompressFormat.PNG,100, outputStream) + outputStream.flush() + outputStream.close() + } + } + + fun getImagePathList(context: Context): List { + val resultList = ArrayList() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val externalUri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL) + val projection = arrayOf(MediaStore.Images.Media._ID) + + context.contentResolver.query( + externalUri, + projection, + null, + null, + MediaStore.Images.Media.DATE_TAKEN + )?.use { cursor -> + val idColumn: Int = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID) + + while (cursor.moveToNext()) { + val uri = ContentUris.withAppendedId( + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + cursor.getLong(idColumn) + ) + resultList.add(uri) + } + } + } + else { + val path = Environment.getExternalStoragePublicDirectory(imageDir) + path.mkdirs() + val imageList = path.listFiles() + + if (imageList.isNullOrEmpty()) + return emptyList() + + for (imagePath in imageList) { + val uri = imagePath.toUri() + resultList.add(uri) + } + } + + return resultList + } + +} \ No newline at end of file diff --git a/jpc/src/main/java/com/divyanshu/androiddraw/jpc/ui/theme/Color.kt b/jpc/src/main/java/com/divyanshu/androiddraw/jpc/ui/theme/Color.kt new file mode 100644 index 0000000..34c9aaf --- /dev/null +++ b/jpc/src/main/java/com/divyanshu/androiddraw/jpc/ui/theme/Color.kt @@ -0,0 +1,8 @@ +package com.divyanshu.androiddraw.jpc.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple200 = Color(0xFFBB86FC) +val Purple500 = Color(0xFF6200EE) +val Purple700 = Color(0xFF3700B3) +val Teal200 = Color(0xFF03DAC5) \ No newline at end of file diff --git a/jpc/src/main/java/com/divyanshu/androiddraw/jpc/ui/theme/Shape.kt b/jpc/src/main/java/com/divyanshu/androiddraw/jpc/ui/theme/Shape.kt new file mode 100644 index 0000000..daa1baf --- /dev/null +++ b/jpc/src/main/java/com/divyanshu/androiddraw/jpc/ui/theme/Shape.kt @@ -0,0 +1,11 @@ +package com.divyanshu.androiddraw.jpc.ui.theme + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Shapes +import androidx.compose.ui.unit.dp + +val Shapes = Shapes( + small = RoundedCornerShape(4.dp), + medium = RoundedCornerShape(4.dp), + large = RoundedCornerShape(0.dp) +) \ No newline at end of file diff --git a/jpc/src/main/java/com/divyanshu/androiddraw/jpc/ui/theme/Theme.kt b/jpc/src/main/java/com/divyanshu/androiddraw/jpc/ui/theme/Theme.kt new file mode 100644 index 0000000..eab0507 --- /dev/null +++ b/jpc/src/main/java/com/divyanshu/androiddraw/jpc/ui/theme/Theme.kt @@ -0,0 +1,48 @@ +package com.divyanshu.androiddraw.jpc.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material.MaterialTheme +import androidx.compose.material.darkColors +import androidx.compose.material.lightColors +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +private val DarkColorPalette = darkColors( + primary = Color.Black, + primaryVariant = Color.Black, + secondary = Teal200 +) + +private val LightColorPalette = lightColors( + primary = Color.Black, + primaryVariant = Color.Black, + secondary = Teal200 + + /* Other default colors to override + background = Color.White, + surface = Color.White, + onPrimary = Color.White, + onSecondary = Color.Black, + onBackground = Color.Black, + onSurface = Color.Black, + */ +) + +@Composable +fun AndroidDrawTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable() () -> Unit +) { + val colors = if (darkTheme) { + DarkColorPalette + } else { + LightColorPalette + } + + MaterialTheme( + colors = colors, + typography = Typography, + shapes = Shapes, + content = content + ) +} \ No newline at end of file diff --git a/jpc/src/main/java/com/divyanshu/androiddraw/jpc/ui/theme/Type.kt b/jpc/src/main/java/com/divyanshu/androiddraw/jpc/ui/theme/Type.kt new file mode 100644 index 0000000..8b9cfcf --- /dev/null +++ b/jpc/src/main/java/com/divyanshu/androiddraw/jpc/ui/theme/Type.kt @@ -0,0 +1,28 @@ +package com.divyanshu.androiddraw.jpc.ui.theme + +import androidx.compose.material.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + body1 = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp + ) + /* Other default text styles to override + button = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.W500, + fontSize = 14.sp + ), + caption = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 12.sp + ) + */ +) \ No newline at end of file diff --git a/jpc/src/main/res/drawable-v24/ic_adjust_black_24dp.xml b/jpc/src/main/res/drawable-v24/ic_adjust_black_24dp.xml new file mode 100644 index 0000000..92e1aa9 --- /dev/null +++ b/jpc/src/main/res/drawable-v24/ic_adjust_black_24dp.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/jpc/src/main/res/drawable-v24/ic_close_black_24dp.xml b/jpc/src/main/res/drawable-v24/ic_close_black_24dp.xml new file mode 100644 index 0000000..ede4b71 --- /dev/null +++ b/jpc/src/main/res/drawable-v24/ic_close_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/jpc/src/main/res/drawable-v24/ic_color_lens_black_24dp.xml b/jpc/src/main/res/drawable-v24/ic_color_lens_black_24dp.xml new file mode 100644 index 0000000..497dad0 --- /dev/null +++ b/jpc/src/main/res/drawable-v24/ic_color_lens_black_24dp.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/jpc/src/main/res/drawable-v24/ic_done_black_24dp.xml b/jpc/src/main/res/drawable-v24/ic_done_black_24dp.xml new file mode 100644 index 0000000..7affe9b --- /dev/null +++ b/jpc/src/main/res/drawable-v24/ic_done_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/jpc/src/main/res/drawable-v24/ic_eraser_black_24dp.xml b/jpc/src/main/res/drawable-v24/ic_eraser_black_24dp.xml new file mode 100644 index 0000000..1283eaa --- /dev/null +++ b/jpc/src/main/res/drawable-v24/ic_eraser_black_24dp.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/jpc/src/main/res/drawable-v24/ic_launcher_foreground.xml b/jpc/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/jpc/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/jpc/src/main/res/drawable-v24/ic_opacity_black_24dp.xml b/jpc/src/main/res/drawable-v24/ic_opacity_black_24dp.xml new file mode 100644 index 0000000..95da153 --- /dev/null +++ b/jpc/src/main/res/drawable-v24/ic_opacity_black_24dp.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/jpc/src/main/res/drawable-v24/ic_redo_black_24dp.xml b/jpc/src/main/res/drawable-v24/ic_redo_black_24dp.xml new file mode 100644 index 0000000..2a4e353 --- /dev/null +++ b/jpc/src/main/res/drawable-v24/ic_redo_black_24dp.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/jpc/src/main/res/drawable-v24/ic_undo_black_24dp.xml b/jpc/src/main/res/drawable-v24/ic_undo_black_24dp.xml new file mode 100644 index 0000000..eb6fa05 --- /dev/null +++ b/jpc/src/main/res/drawable-v24/ic_undo_black_24dp.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/jpc/src/main/res/drawable/ic_launcher_background.xml b/jpc/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/jpc/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jpc/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/jpc/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/jpc/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/jpc/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/jpc/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/jpc/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/jpc/src/main/res/mipmap-hdpi/ic_launcher.png b/jpc/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..a901e5a Binary files /dev/null and b/jpc/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/jpc/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/jpc/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..55df7f2 Binary files /dev/null and b/jpc/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/jpc/src/main/res/mipmap-hdpi/ic_launcher_round.png b/jpc/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..a901e5a Binary files /dev/null and b/jpc/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/jpc/src/main/res/mipmap-mdpi/ic_launcher.png b/jpc/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..7aee16d Binary files /dev/null and b/jpc/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/jpc/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/jpc/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..5eda1bf Binary files /dev/null and b/jpc/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/jpc/src/main/res/mipmap-mdpi/ic_launcher_round.png b/jpc/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..7aee16d Binary files /dev/null and b/jpc/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/jpc/src/main/res/mipmap-xhdpi/ic_launcher.png b/jpc/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..d270f29 Binary files /dev/null and b/jpc/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/jpc/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/jpc/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..f752758 Binary files /dev/null and b/jpc/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/jpc/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/jpc/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..d270f29 Binary files /dev/null and b/jpc/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/jpc/src/main/res/mipmap-xxhdpi/ic_launcher.png b/jpc/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..772635e Binary files /dev/null and b/jpc/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/jpc/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/jpc/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..8912ff5 Binary files /dev/null and b/jpc/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/jpc/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/jpc/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..772635e Binary files /dev/null and b/jpc/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/jpc/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/jpc/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..63b4fad Binary files /dev/null and b/jpc/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/jpc/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/jpc/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..2053e92 Binary files /dev/null and b/jpc/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/jpc/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/jpc/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..63b4fad Binary files /dev/null and b/jpc/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/jpc/src/main/res/values-night/themes.xml b/jpc/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..b6431a8 --- /dev/null +++ b/jpc/src/main/res/values-night/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/jpc/src/main/res/values/colors.xml b/jpc/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/jpc/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/jpc/src/main/res/values/ic_launcher_background.xml b/jpc/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..beab31f --- /dev/null +++ b/jpc/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #000000 + \ No newline at end of file diff --git a/jpc/src/main/res/values/strings.xml b/jpc/src/main/res/values/strings.xml new file mode 100644 index 0000000..51519de --- /dev/null +++ b/jpc/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + jpc + \ No newline at end of file diff --git a/jpc/src/main/res/values/themes.xml b/jpc/src/main/res/values/themes.xml new file mode 100644 index 0000000..e9aeb83 --- /dev/null +++ b/jpc/src/main/res/values/themes.xml @@ -0,0 +1,25 @@ + + + + + + +