├── .gitignore ├── .idea └── encodings.xml ├── .travis.yml ├── LICENSE ├── README.md ├── app ├── build.gradle ├── proguard-rules.txt └── src │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-web.png │ ├── java │ └── com │ │ └── tomclaw │ │ └── drawa │ │ ├── core │ │ ├── Config.kt │ │ ├── DrawaGlideModule.java │ │ └── Journal.kt │ │ ├── di │ │ ├── AppComponent.kt │ │ └── AppModule.kt │ │ ├── draw │ │ ├── BitmapDrawHost.kt │ │ ├── BitmapHost.kt │ │ ├── DrawActivity.kt │ │ ├── DrawHost.kt │ │ ├── DrawHostHolder.kt │ │ ├── DrawInteractor.kt │ │ ├── DrawPresenter.kt │ │ ├── DrawResourceProvider.kt │ │ ├── DrawView.kt │ │ ├── Event.kt │ │ ├── History.kt │ │ ├── ImageProvider.kt │ │ ├── ToolProvider.kt │ │ ├── ToolsView.kt │ │ ├── di │ │ │ ├── DrawComponent.kt │ │ │ ├── DrawModule.kt │ │ │ └── ToolsModule.kt │ │ ├── tools │ │ │ ├── Brush.kt │ │ │ ├── Eraser.kt │ │ │ ├── Fill.kt │ │ │ ├── Fluffy.kt │ │ │ ├── Marker.kt │ │ │ ├── Pencil.kt │ │ │ ├── Size.kt │ │ │ └── Tool.kt │ │ └── view │ │ │ ├── DrawingListener.kt │ │ │ ├── DrawingView.kt │ │ │ ├── PaletteView.kt │ │ │ └── TouchEvent.kt │ │ ├── dto │ │ ├── Record.kt │ │ └── Size.kt │ │ ├── info │ │ ├── InfoActivity.kt │ │ ├── InfoPresenter.kt │ │ ├── InfoResourceProvider.kt │ │ ├── InfoView.kt │ │ └── di │ │ │ ├── InfoComponent.kt │ │ │ └── InfoModule.kt │ │ ├── main │ │ └── App.kt │ │ ├── play │ │ ├── EventsDrawable.kt │ │ ├── EventsProvider.kt │ │ ├── EventsRenderer.kt │ │ ├── PlayActivity.kt │ │ ├── PlayInteractor.kt │ │ ├── PlayPresenter.kt │ │ ├── PlayView.kt │ │ └── di │ │ │ ├── PlayComponent.kt │ │ │ └── PlayModule.kt │ │ ├── share │ │ ├── DetachedDrawHost.kt │ │ ├── ShareActivity.kt │ │ ├── ShareAdapter.kt │ │ ├── ShareInteractor.kt │ │ ├── ShareItem.kt │ │ ├── ShareItemHolder.kt │ │ ├── SharePlugin.kt │ │ ├── SharePresenter.kt │ │ ├── ShareResult.kt │ │ ├── ShareView.kt │ │ ├── di │ │ │ ├── ShareComponent.kt │ │ │ └── ShareModule.kt │ │ └── plugin │ │ │ ├── AnimSharePlugin.kt │ │ │ ├── StaticSharePlugin.kt │ │ │ └── VideoSharePlugin.kt │ │ ├── stock │ │ ├── RecordConverter.kt │ │ ├── StockActivity.kt │ │ ├── StockAdapter.kt │ │ ├── StockInteractor.kt │ │ ├── StockItem.kt │ │ ├── StockItemHolder.kt │ │ ├── StockPresenter.kt │ │ ├── StockView.kt │ │ └── di │ │ │ ├── StockComponent.kt │ │ │ └── StockModule.kt │ │ └── util │ │ ├── AspectRatioImageView.kt │ │ ├── CircleProgressView.kt │ │ ├── DataProvider.kt │ │ ├── Logger.kt │ │ ├── MetricsProvider.kt │ │ ├── PerActivity.kt │ │ ├── QueueLinearFloodFiller.kt │ │ ├── RecordNotFoundException.kt │ │ ├── Records.kt │ │ ├── RectF.kt │ │ ├── SchedulersFactory.kt │ │ ├── StreamDecoder.kt │ │ ├── StreamDrawable.kt │ │ ├── StreamRenderer.kt │ │ ├── Streams.kt │ │ ├── Views.kt │ │ └── ZoomableImageView.kt │ └── res │ ├── drawable-xhdpi │ └── doodle.png │ ├── drawable-xxxhdpi │ └── doodle.png │ ├── drawable │ ├── animation.xml │ ├── background_doodle.xml │ ├── brush.xml │ ├── circle.xml │ ├── delete.xml │ ├── duplicate.xml │ ├── eraser.xml │ ├── format_color_fill.xml │ ├── ic_launcher_foreground.xml │ ├── image.xml │ ├── info.xml │ ├── lead_pencil.xml │ ├── logo.xml │ ├── marker.xml │ ├── play.xml │ ├── plus.xml │ ├── replay.xml │ ├── shadow_toolbar.xml │ ├── shadow_top.xml │ ├── share_variant.xml │ ├── size_l.xml │ ├── size_m.xml │ ├── size_s.xml │ ├── size_xl.xml │ ├── size_xxl.xml │ ├── spray.xml │ ├── undo_variant.xml │ └── videocam.xml │ ├── layout │ ├── choosers_view.xml │ ├── controls_view.xml │ ├── draw.xml │ ├── info.xml │ ├── play.xml │ ├── progress_view.xml │ ├── shadow_toolbar.xml │ ├── share.xml │ ├── share_item_view.xml │ ├── stock.xml │ ├── stock_item_view.xml │ └── tools_view.xml │ ├── menu │ ├── draw.xml │ ├── play.xml │ └── stock.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── values │ ├── attrs.xml │ ├── colors.xml │ ├── dimens.xml │ ├── ic_launcher_background.xml │ ├── strings.xml │ └── styles.xml │ └── xml │ └── filepaths.xml ├── build.gradle ├── circle.yml ├── gif-encoder ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── tomclaw │ └── drawa │ └── gif │ ├── GifEncoder.java │ ├── LzwEncoder.java │ └── NeuQuant.java ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── graphics ├── doodle.sketch ├── draw.png ├── icon-2.sketch ├── icon.sketch ├── main.png ├── palette.png └── share.png ├── import-summary.txt ├── settings.gradle └── tomclaw.keystore /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.zip 3 | *.apk 4 | *.iml 5 | 6 | # IDE folders # 7 | gen/ 8 | bin/ 9 | out/ 10 | proguard_logs/ 11 | projectFilesBackup/ 12 | .idea/* 13 | !.idea/codeStyleSettings.xml 14 | !.idea/encodings.xml 15 | !.idea/inspectionProfiles/ 16 | 17 | # Gradle files 18 | .gradle/ 19 | *build/ 20 | 21 | # Local configuration file (sdk path, etc) 22 | local.properties 23 | 24 | # OS-specific files 25 | .DS_Store 26 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | jdk: oraclejdk8 3 | 4 | android: 5 | components: 6 | - tools 7 | - tools 8 | - platform-tools 9 | - build-tools-25.0.3 10 | - android-25 11 | - extra 12 | - extra-android-support 13 | - extra-android-m2repository 14 | - extra-google-m2repository 15 | 16 | script: 17 | - ./gradlew build dependencies || true 18 | before_install: 19 | - chmod +x gradlew 20 | branches: 21 | only: 22 | - master 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Drawa 2 | DRAWa - animation drawing application for Android 3 | 4 | ![Screenshot](graphics/main.png "Main Screen") 5 | ![Screenshot](graphics/draw.png "Draw Screen") 6 | 7 | ![Screenshot](graphics/palette.png "Palette") 8 | ![Screenshot](graphics/share.png "Share Screen") 9 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | id 'kotlin-kapt' 5 | id 'kotlin-parcelize' 6 | } 7 | 8 | android { 9 | signingConfigs { 10 | release { 11 | if (project.hasProperty("storeFile")) storeFile file("$rootDir/" + project.storeFile) 12 | if (project.hasProperty("storePassword")) storePassword project.storePassword 13 | if (project.hasProperty("keyAlias")) keyAlias project.keyAlias 14 | if (project.hasProperty("keyPassword")) keyPassword project.keyPassword 15 | } 16 | } 17 | 18 | compileSdk 34 19 | 20 | defaultConfig { 21 | applicationId "com.tomclaw.drawa" 22 | minSdkVersion 21 23 | targetSdkVersion 34 24 | versionCode project.hasProperty("versionCode") ? Integer.parseInt(project.versionCode) : 1 25 | versionName "1.0" 26 | multiDexEnabled true 27 | } 28 | 29 | buildTypes { 30 | release { 31 | minifyEnabled true 32 | shrinkResources true 33 | signingConfig signingConfigs.release 34 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' 35 | } 36 | } 37 | compileOptions { 38 | sourceCompatibility JavaVersion.VERSION_17 39 | targetCompatibility JavaVersion.VERSION_17 40 | } 41 | namespace 'com.tomclaw.drawa' 42 | } 43 | 44 | dependencies { 45 | implementation project(path: ':gif-encoder') 46 | implementation 'com.github.solkin:disk-lru-cache:1.5' 47 | implementation 'org.jcodec:jcodec:0.2.5' 48 | implementation 'org.jcodec:jcodec-android:0.2.5' 49 | implementation 'androidx.appcompat:appcompat:1.7.0' 50 | implementation 'androidx.cardview:cardview:1.0.0' 51 | implementation 'androidx.recyclerview:recyclerview:1.3.2' 52 | implementation 'com.google.android.material:material:1.12.0' 53 | implementation 'com.github.bumptech.glide:glide:4.16.0' 54 | implementation 'io.reactivex.rxjava2:rxjava:2.2.10' 55 | implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' 56 | implementation 'com.jakewharton.rxrelay2:rxrelay:2.1.1' 57 | implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0' 58 | implementation 'com.google.dagger:dagger:2.50' 59 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 60 | implementation 'androidx.multidex:multidex:2.0.1' 61 | kapt 'com.google.dagger:dagger-compiler:2.50' 62 | kapt 'com.google.dagger:dagger-android-processor:2.50' 63 | kapt 'com.github.bumptech.glide:compiler:4.16.0' 64 | kapt 'org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.5.0' 65 | } 66 | -------------------------------------------------------------------------------- /app/proguard-rules.txt: -------------------------------------------------------------------------------- 1 | # Glide 2 | -keep public class * implements com.bumptech.glide.module.GlideModule 3 | -keep public class * extends com.bumptech.glide.module.AppGlideModule 4 | -keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** { 5 | **[] $VALUES; 6 | public *; 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 15 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 39 | 40 | 45 | 46 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solkin/drawa-android/a156fa2a3745a9167ae13d750dd4102adf754de2/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/core/Config.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.core 2 | 3 | const val LOG_TAG: String = "Drawa" 4 | 5 | const val BITMAP_WIDTH = 720 6 | const val BITMAP_HEIGHT = 720 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/core/DrawaGlideModule.java: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.core; 2 | 3 | import com.bumptech.glide.annotation.GlideModule; 4 | import com.bumptech.glide.module.AppGlideModule; 5 | 6 | @GlideModule 7 | public final class DrawaGlideModule extends AppGlideModule { 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/di/AppComponent.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.di 2 | 3 | import com.tomclaw.drawa.draw.di.DrawComponent 4 | import com.tomclaw.drawa.draw.di.DrawModule 5 | import com.tomclaw.drawa.info.di.InfoComponent 6 | import com.tomclaw.drawa.info.di.InfoModule 7 | import com.tomclaw.drawa.play.di.PlayComponent 8 | import com.tomclaw.drawa.play.di.PlayModule 9 | import com.tomclaw.drawa.share.di.ShareComponent 10 | import com.tomclaw.drawa.share.di.ShareModule 11 | import com.tomclaw.drawa.stock.di.StockComponent 12 | import com.tomclaw.drawa.stock.di.StockModule 13 | import dagger.Component 14 | import javax.inject.Singleton 15 | 16 | @Singleton 17 | @Component(modules = [AppModule::class]) 18 | interface AppComponent { 19 | 20 | fun stockComponent(module: StockModule): StockComponent 21 | 22 | fun drawComponent(module: DrawModule): DrawComponent 23 | 24 | fun shareComponent(module: ShareModule): ShareComponent 25 | 26 | fun infoComponent(module: InfoModule): InfoComponent 27 | 28 | fun playComponent(module: PlayModule): PlayComponent 29 | 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/di/AppModule.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.di 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import com.tomclaw.cache.DiskLruCache 6 | import com.tomclaw.drawa.core.Journal 7 | import com.tomclaw.drawa.core.JournalImpl 8 | import com.tomclaw.drawa.draw.ImageProvider 9 | import com.tomclaw.drawa.draw.ImageProviderImpl 10 | import com.tomclaw.drawa.util.Logger 11 | import com.tomclaw.drawa.util.LoggerImpl 12 | import com.tomclaw.drawa.util.MetricsProvider 13 | import com.tomclaw.drawa.util.MetricsProviderImpl 14 | import com.tomclaw.drawa.util.SchedulersFactory 15 | import com.tomclaw.drawa.util.SchedulersFactoryImpl 16 | import dagger.Module 17 | import dagger.Provides 18 | import java.io.File 19 | import javax.inject.Singleton 20 | 21 | @Module 22 | class AppModule(private val app: Application) { 23 | 24 | @Provides 25 | @Singleton 26 | internal fun provideContext(): Context = app 27 | 28 | @Provides 29 | @Singleton 30 | internal fun provideSchedulersFactory(): SchedulersFactory = SchedulersFactoryImpl() 31 | 32 | @Provides 33 | @Singleton 34 | internal fun provideJournal(filesDir: File): Journal { 35 | val journalFile = File(filesDir, "journal.dat") 36 | return JournalImpl(journalFile) 37 | } 38 | 39 | @Provides 40 | @Singleton 41 | fun provideImageProvider(filesDir: File, journal: Journal): ImageProvider { 42 | return ImageProviderImpl(filesDir, journal) 43 | } 44 | 45 | @Provides 46 | @Singleton 47 | fun provideFilesDir(): File = app.filesDir 48 | 49 | @Provides 50 | @Singleton 51 | internal fun provideLogger(): Logger = LoggerImpl() 52 | 53 | @Provides 54 | @Singleton 55 | fun provideMetricsProvider(): MetricsProvider { 56 | return MetricsProviderImpl(app) 57 | } 58 | 59 | @Provides 60 | @Singleton 61 | fun provideLruCache(): DiskLruCache { 62 | val cacheDir = File(app.cacheDir, "share") 63 | return DiskLruCache.create(cacheDir, LRU_CACHE_SIZE) 64 | } 65 | 66 | } 67 | 68 | const val LRU_CACHE_SIZE = 25L * 1024 * 1024 69 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/draw/BitmapDrawHost.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.draw 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.Canvas 5 | import android.graphics.Color 6 | import android.graphics.Paint 7 | import android.graphics.Rect 8 | import com.tomclaw.drawa.core.BITMAP_HEIGHT 9 | import com.tomclaw.drawa.core.BITMAP_WIDTH 10 | 11 | class BitmapDrawHost(width: Int = BITMAP_WIDTH, height: Int = BITMAP_HEIGHT) : BitmapHost { 12 | 13 | private val hiddenBitmap: Bitmap = Bitmap.createBitmap( 14 | width, 15 | height, 16 | Bitmap.Config.ARGB_8888 17 | ) 18 | 19 | override val normalBitmap: Bitmap = Bitmap.createBitmap( 20 | width, 21 | height, 22 | Bitmap.Config.ARGB_8888 23 | ) 24 | 25 | private val hiddenCanvas: Canvas = Canvas(hiddenBitmap) 26 | private val normalCanvas: Canvas = Canvas(normalBitmap) 27 | 28 | override val canvas: Canvas 29 | get() = if (hidden) hiddenCanvas else normalCanvas 30 | override val bitmap: Bitmap 31 | get() = if (hidden) hiddenBitmap else normalBitmap 32 | 33 | override val src: Rect = Rect(0, 0, normalBitmap.width, normalBitmap.height) 34 | 35 | override val paint: Paint = Paint().apply { 36 | isAntiAlias = true 37 | isDither = true 38 | isFilterBitmap = true 39 | } 40 | 41 | override var hidden = false 42 | set(value) { 43 | if (!value) { 44 | normalBitmap.eraseColor(Color.TRANSPARENT) 45 | normalCanvas.drawBitmap(hiddenBitmap, src, src, paint) 46 | } 47 | field = value 48 | } 49 | 50 | init { 51 | clearBitmap() 52 | } 53 | 54 | override fun applyBitmap(bitmap: Bitmap) { 55 | val dst = this.src 56 | val src = Rect(0, 0, bitmap.width, bitmap.height) 57 | canvas.drawBitmap(bitmap, src, dst, paint) 58 | } 59 | 60 | override fun clearBitmap() { 61 | bitmap.eraseColor(Color.TRANSPARENT) 62 | } 63 | 64 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/draw/BitmapHost.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.draw 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.Canvas 5 | import android.graphics.Paint 6 | import android.graphics.Rect 7 | 8 | interface BitmapHost { 9 | 10 | val paint: Paint 11 | 12 | val bitmap: Bitmap 13 | 14 | val normalBitmap: Bitmap 15 | 16 | val src: Rect 17 | 18 | val canvas: Canvas 19 | 20 | var hidden: Boolean 21 | 22 | fun applyBitmap(bitmap: Bitmap) 23 | 24 | fun clearBitmap() 25 | 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/draw/DrawActivity.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.draw 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import androidx.appcompat.app.AppCompatActivity 7 | import com.tomclaw.drawa.R 8 | import com.tomclaw.drawa.draw.di.DrawModule 9 | import com.tomclaw.drawa.main.getComponent 10 | import com.tomclaw.drawa.play.createPlayActivityIntent 11 | import com.tomclaw.drawa.share.createShareActivityIntent 12 | import com.tomclaw.drawa.util.MetricsProvider 13 | import javax.inject.Inject 14 | 15 | class DrawActivity : AppCompatActivity(), DrawPresenter.DrawRouter { 16 | 17 | @Inject 18 | lateinit var presenter: DrawPresenter 19 | 20 | @Inject 21 | lateinit var metricsProvider: MetricsProvider 22 | 23 | override fun onCreate(savedInstanceState: Bundle?) { 24 | val recordId = intent.getRecordId() 25 | val presenterState = savedInstanceState?.getBundle(KEY_PRESENTER_STATE) 26 | val drawHostHolder = DrawHostHolder() 27 | application.getComponent() 28 | .drawComponent( 29 | DrawModule( 30 | resources = resources, 31 | recordId = recordId, 32 | drawHostHolder = drawHostHolder, 33 | presenterState = presenterState 34 | ) 35 | ) 36 | .inject(activity = this) 37 | 38 | super.onCreate(savedInstanceState) 39 | setContentView(R.layout.draw) 40 | 41 | val view = DrawViewImpl(window.decorView, drawHostHolder, metricsProvider) 42 | 43 | presenter.attachView(view) 44 | } 45 | 46 | override fun onStart() { 47 | super.onStart() 48 | presenter.attachRouter(this) 49 | } 50 | 51 | override fun onStop() { 52 | presenter.detachRouter() 53 | super.onStop() 54 | } 55 | 56 | override fun onDestroy() { 57 | presenter.detachView() 58 | super.onDestroy() 59 | } 60 | 61 | override fun onSaveInstanceState(outState: Bundle) { 62 | super.onSaveInstanceState(outState) 63 | outState.putBundle(KEY_PRESENTER_STATE, presenter.saveState()) 64 | } 65 | 66 | override fun onBackPressed() { 67 | super.onBackPressed() 68 | presenter.onBackPressed() 69 | } 70 | 71 | override fun showShareScreen() { 72 | val intent = createShareActivityIntent( 73 | context = this, 74 | recordId = intent.getRecordId() 75 | ) 76 | startActivity(intent) 77 | } 78 | 79 | override fun showPlayScreen() { 80 | val intent = createPlayActivityIntent( 81 | context = this, 82 | recordId = intent.getRecordId() 83 | ) 84 | startActivity(intent) 85 | } 86 | 87 | override fun leaveScreen() { 88 | setResult(RESULT_OK) 89 | finish() 90 | } 91 | 92 | private fun Intent.getRecordId() = this.getIntExtra(EXTRA_RECORD_ID, RECORD_ID_INVALID).apply { 93 | if (this == RECORD_ID_INVALID) { 94 | throw IllegalArgumentException("record id must be specified") 95 | } 96 | } 97 | 98 | } 99 | 100 | fun createDrawActivityIntent( 101 | context: Context, 102 | recordId: Int 103 | ): Intent = Intent(context, DrawActivity::class.java) 104 | .putExtra(EXTRA_RECORD_ID, recordId) 105 | 106 | private const val KEY_PRESENTER_STATE = "presenter_state" 107 | 108 | private const val EXTRA_RECORD_ID = "record_id" 109 | 110 | private const val RECORD_ID_INVALID = -1 -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/draw/DrawHost.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.draw 2 | 3 | interface DrawHost : BitmapHost { 4 | 5 | fun invalidate() 6 | 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/draw/DrawHostHolder.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.draw 2 | 3 | class DrawHostHolder { 4 | 5 | lateinit var drawHost: DrawHost 6 | 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/draw/DrawInteractor.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.draw 2 | 3 | import com.tomclaw.drawa.core.Journal 4 | import com.tomclaw.drawa.dto.Record 5 | import com.tomclaw.drawa.util.SchedulersFactory 6 | import io.reactivex.Observable 7 | import io.reactivex.Single 8 | 9 | interface DrawInteractor { 10 | 11 | fun loadHistory(): Observable 12 | 13 | fun saveHistory(): Observable 14 | 15 | fun undo(): Observable 16 | 17 | fun duplicate(): Observable 18 | 19 | fun delete(): Observable 20 | 21 | } 22 | 23 | class DrawInteractorImpl(private val recordId: Int, 24 | private val imageProvider: ImageProvider, 25 | private val journal: Journal, 26 | private val history: History, 27 | private val drawHostHolder: DrawHostHolder, 28 | private val schedulers: SchedulersFactory) : DrawInteractor { 29 | 30 | private var isDeleted = false 31 | 32 | override fun loadHistory(): Observable { 33 | return resolve({ 34 | history.load() 35 | .flatMap { imageProvider.readImage(recordId) } 36 | .map { bitmap -> 37 | drawHostHolder.drawHost.applyBitmap(bitmap) 38 | bitmap.recycle() 39 | } 40 | .toObservable() 41 | .subscribeOn(schedulers.io()) 42 | }, { 43 | Observable.just(Unit) 44 | }) 45 | } 46 | 47 | override fun saveHistory(): Observable { 48 | return resolve({ 49 | history.save() 50 | .flatMap { 51 | imageProvider.saveImage( 52 | recordId, 53 | drawHostHolder.drawHost.bitmap 54 | ) 55 | } 56 | .map { } 57 | .toObservable() 58 | .subscribeOn(schedulers.io()) 59 | }, { 60 | Observable.just(Unit) 61 | }) 62 | } 63 | 64 | override fun undo(): Observable { 65 | return resolve({ 66 | Single 67 | .create { emitter -> 68 | history.undo() 69 | emitter.onSuccess(Unit) 70 | } 71 | .toObservable() 72 | }, { 73 | Observable.just(Unit) 74 | }).subscribeOn(schedulers.single()) 75 | } 76 | 77 | override fun duplicate(): Observable = Single 78 | .create { emitter -> 79 | val record = journal.create() 80 | journal.add(record) 81 | emitter.onSuccess(record) 82 | } 83 | .flatMap { record -> 84 | history.duplicate(record.id).map { record } 85 | } 86 | .flatMap { record -> 87 | imageProvider.duplicateImage( 88 | sourceRecordId = recordId, 89 | targetRecordId = record.id 90 | ) 91 | } 92 | .flatMap { journal.save() } 93 | .toObservable() 94 | .subscribeOn(schedulers.io()) 95 | 96 | override fun delete(): Observable { 97 | isDeleted = true 98 | return history.delete() 99 | .flatMap { journal.delete(id = recordId) } 100 | .toObservable() 101 | .subscribeOn(schedulers.io()) 102 | } 103 | 104 | private fun resolve(notDeleted: () -> T, deleted: () -> T): T { 105 | return if (isDeleted) deleted.invoke() else notDeleted.invoke() 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/draw/DrawResourceProvider.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.draw 2 | 3 | import android.content.res.Resources 4 | import com.tomclaw.drawa.R 5 | 6 | interface DrawResourceProvider { 7 | 8 | val defaultColor: Int 9 | 10 | } 11 | 12 | class DrawResourceProviderImpl(val resources: Resources) : DrawResourceProvider { 13 | 14 | override val defaultColor = resources.getColor(R.color.color10) 15 | 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/draw/DrawView.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.draw 2 | 3 | import android.view.View 4 | import android.widget.ViewFlipper 5 | import androidx.appcompat.widget.Toolbar 6 | import com.jakewharton.rxrelay2.PublishRelay 7 | import com.tomclaw.drawa.R 8 | import com.tomclaw.drawa.draw.tools.Tool 9 | import com.tomclaw.drawa.draw.view.DrawingListener 10 | import com.tomclaw.drawa.draw.view.DrawingView 11 | import com.tomclaw.drawa.draw.view.TouchEvent 12 | import com.tomclaw.drawa.util.MetricsProvider 13 | import com.tomclaw.drawa.util.hideWithAlphaAnimation 14 | import com.tomclaw.drawa.util.showWithAlphaAnimation 15 | import io.reactivex.Observable 16 | 17 | interface DrawView : ToolsView { 18 | 19 | fun setDrawingListener(listener: DrawingListener) 20 | 21 | fun acceptTool(tool: Tool) 22 | 23 | fun showProgress() 24 | 25 | fun showOverlayProgress() 26 | 27 | fun showContent() 28 | 29 | fun touchEvents(): Observable 30 | 31 | fun drawEvents(): Observable 32 | 33 | fun navigationClicks(): Observable 34 | 35 | fun undoClicks(): Observable 36 | 37 | fun playClicks(): Observable 38 | 39 | fun shareClicks(): Observable 40 | 41 | fun duplicateClicks(): Observable 42 | 43 | fun deleteClicks(): Observable 44 | 45 | } 46 | 47 | class DrawViewImpl( 48 | view: View, 49 | drawHostHolder: DrawHostHolder, 50 | private val metricsProvider: MetricsProvider 51 | ) : DrawView, ToolsView by ToolsViewImpl(view) { 52 | 53 | private val toolbar: Toolbar = view.findViewById(R.id.toolbar) 54 | private val drawingView: DrawingView = view.findViewById(R.id.drawing_view) 55 | private val overlayProgress: View = view.findViewById(R.id.overlay_progress) 56 | private val flipper: ViewFlipper = view.findViewById(R.id.flipper) 57 | private val undoButton: View = view.findViewById(R.id.undo_button) 58 | 59 | private val touchRelay = PublishRelay.create() 60 | private val drawRelay = PublishRelay.create() 61 | private val navigationRelay = PublishRelay.create() 62 | private val undoRelay = PublishRelay.create() 63 | private val playRelay = PublishRelay.create() 64 | private val shareRelay = PublishRelay.create() 65 | private val duplicateRelay = PublishRelay.create() 66 | private val deleteRelay = PublishRelay.create() 67 | 68 | init { 69 | drawHostHolder.drawHost = drawingView 70 | toolbar.setTitle(R.string.draw) 71 | toolbar.setNavigationOnClickListener { 72 | navigationRelay.accept(Unit) 73 | } 74 | toolbar.inflateMenu(R.menu.draw) 75 | toolbar.setOnMenuItemClickListener { item -> 76 | when (item.itemId) { 77 | R.id.menu_share -> shareRelay.accept(Unit) 78 | R.id.menu_play -> playRelay.accept(Unit) 79 | R.id.menu_duplicate -> duplicateRelay.accept(Unit) 80 | R.id.menu_delete -> deleteRelay.accept(Unit) 81 | } 82 | true 83 | } 84 | undoButton.setOnClickListener { undoRelay.accept(Unit) } 85 | drawingView.drawingListener = object : DrawingListener { 86 | override fun onTouchEvent(event: TouchEvent) { 87 | touchRelay.accept(event) 88 | } 89 | 90 | override fun onDraw() { 91 | drawRelay.accept(Unit) 92 | } 93 | } 94 | } 95 | 96 | override fun setDrawingListener(listener: DrawingListener) { 97 | drawingView.drawingListener = listener 98 | } 99 | 100 | override fun acceptTool(tool: Tool) { 101 | tool.initialize(drawingView, metricsProvider) 102 | } 103 | 104 | override fun showProgress() { 105 | flipper.displayedChild = 0 106 | } 107 | 108 | override fun showOverlayProgress() { 109 | overlayProgress.showWithAlphaAnimation(animateFully = true) 110 | } 111 | 112 | override fun showContent() { 113 | flipper.displayedChild = 1 114 | overlayProgress.hideWithAlphaAnimation(animateFully = false) 115 | } 116 | 117 | override fun touchEvents(): Observable = touchRelay 118 | 119 | override fun drawEvents(): Observable = drawRelay 120 | 121 | override fun navigationClicks(): Observable = navigationRelay 122 | 123 | override fun undoClicks(): Observable = undoRelay 124 | 125 | override fun playClicks(): Observable = playRelay 126 | 127 | override fun shareClicks(): Observable = shareRelay 128 | 129 | override fun duplicateClicks(): Observable = duplicateRelay 130 | 131 | override fun deleteClicks(): Observable = deleteRelay 132 | 133 | } 134 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/draw/Event.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.draw 2 | 3 | data class Event( 4 | val index: Int, 5 | val toolType: Int, 6 | val color: Int, 7 | val size: Int, 8 | val x: Int, 9 | val y: Int, 10 | val action: Int 11 | ) 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/draw/ImageProvider.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.draw 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.BitmapFactory 5 | import com.tomclaw.drawa.core.Journal 6 | import com.tomclaw.drawa.dto.Record 7 | import com.tomclaw.drawa.util.imageFile 8 | import com.tomclaw.drawa.util.safeClose 9 | import io.reactivex.Single 10 | import java.io.File 11 | import java.io.FileInputStream 12 | import java.io.FileOutputStream 13 | import java.io.InputStream 14 | import java.io.OutputStream 15 | 16 | interface ImageProvider { 17 | 18 | fun readImage(recordId: Int): Single 19 | 20 | fun saveImage(recordId: Int, bitmap: Bitmap): Single 21 | 22 | fun duplicateImage(sourceRecordId: Int, targetRecordId: Int): Single 23 | 24 | } 25 | 26 | class ImageProviderImpl( 27 | private val filesDir: File, 28 | private val journal: Journal 29 | ) : ImageProvider { 30 | 31 | override fun readImage(recordId: Int): Single = Single.create { emitter -> 32 | var stream: InputStream? = null 33 | try { 34 | val imageFile = journal.get(recordId).imageFile(filesDir) 35 | stream = FileInputStream(imageFile) 36 | emitter.onSuccess(BitmapFactory.decodeStream(stream)) 37 | } catch (ex: Throwable) { 38 | emitter.onError(ex) 39 | } finally { 40 | stream.safeClose() 41 | } 42 | } 43 | 44 | override fun saveImage(recordId: Int, bitmap: Bitmap): Single = Single 45 | .create { 46 | journal.get(recordId) 47 | .imageFile(filesDir) 48 | .delete() 49 | it.onSuccess(Unit) 50 | } 51 | .flatMap { journal.touch(recordId) } 52 | .map { record -> 53 | var stream: OutputStream? = null 54 | try { 55 | val imageFile = record.imageFile(filesDir) 56 | stream = FileOutputStream(imageFile) 57 | bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream) 58 | } finally { 59 | stream.safeClose() 60 | } 61 | record 62 | } 63 | 64 | override fun duplicateImage(sourceRecordId: Int, targetRecordId: Int): Single = Single 65 | .create { 66 | journal 67 | .get(sourceRecordId) 68 | .imageFile(filesDir) 69 | .copyTo( 70 | target = journal.get(targetRecordId).imageFile(filesDir), 71 | overwrite = true 72 | ) 73 | it.onSuccess(Unit) 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/draw/ToolProvider.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.draw 2 | 3 | import com.tomclaw.drawa.draw.tools.Tool 4 | 5 | interface ToolProvider { 6 | 7 | fun getTool(type: Int): Tool 8 | 9 | fun listTools(): List 10 | 11 | } 12 | 13 | class ToolProviderImpl(toolSet: Set) : ToolProvider { 14 | 15 | private val tools = HashMap() 16 | 17 | init { 18 | toolSet.forEach { tool -> 19 | tools[tool.type] = tool 20 | } 21 | } 22 | 23 | override fun getTool(type: Int): Tool = tools[type] 24 | ?: throw IllegalArgumentException("No tool found for type $type") 25 | 26 | override fun listTools(): List = ArrayList(tools.values) 27 | 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/draw/di/DrawComponent.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.draw.di 2 | 3 | import com.tomclaw.drawa.draw.DrawActivity 4 | import com.tomclaw.drawa.util.PerActivity 5 | import dagger.Subcomponent 6 | 7 | @PerActivity 8 | @Subcomponent(modules = [DrawModule::class, ToolsModule::class]) 9 | interface DrawComponent { 10 | 11 | fun inject(activity: DrawActivity) 12 | 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/draw/di/DrawModule.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.draw.di 2 | 3 | import android.content.res.Resources 4 | import android.os.Bundle 5 | import com.tomclaw.drawa.core.Journal 6 | import com.tomclaw.drawa.draw.DrawHostHolder 7 | import com.tomclaw.drawa.draw.DrawInteractor 8 | import com.tomclaw.drawa.draw.DrawInteractorImpl 9 | import com.tomclaw.drawa.draw.DrawPresenter 10 | import com.tomclaw.drawa.draw.DrawPresenterImpl 11 | import com.tomclaw.drawa.draw.DrawResourceProvider 12 | import com.tomclaw.drawa.draw.DrawResourceProviderImpl 13 | import com.tomclaw.drawa.draw.History 14 | import com.tomclaw.drawa.draw.HistoryImpl 15 | import com.tomclaw.drawa.draw.ImageProvider 16 | import com.tomclaw.drawa.draw.ToolProvider 17 | import com.tomclaw.drawa.util.Logger 18 | import com.tomclaw.drawa.util.PerActivity 19 | import com.tomclaw.drawa.util.SchedulersFactory 20 | import dagger.Module 21 | import dagger.Provides 22 | import java.io.File 23 | 24 | @Module 25 | class DrawModule( 26 | private val resources: Resources, 27 | private val recordId: Int, 28 | private val drawHostHolder: DrawHostHolder, 29 | private val presenterState: Bundle? 30 | ) { 31 | 32 | @Provides 33 | @PerActivity 34 | fun provideDrawPresenter( 35 | interactor: DrawInteractor, 36 | toolProvider: ToolProvider, 37 | history: History, 38 | resourceProvider: DrawResourceProvider, 39 | schedulers: SchedulersFactory 40 | ): DrawPresenter = DrawPresenterImpl( 41 | interactor, 42 | schedulers, 43 | toolProvider, 44 | history, 45 | drawHostHolder, 46 | resourceProvider, 47 | presenterState 48 | ) 49 | 50 | @Provides 51 | @PerActivity 52 | fun provideDrawInteractor( 53 | history: History, 54 | journal: Journal, 55 | imageProvider: ImageProvider, 56 | schedulers: SchedulersFactory 57 | ): DrawInteractor = DrawInteractorImpl( 58 | recordId, 59 | imageProvider, 60 | journal, 61 | history, 62 | drawHostHolder, 63 | schedulers 64 | ) 65 | 66 | @Provides 67 | @PerActivity 68 | fun provideHistory(filesDir: File, logger: Logger): History { 69 | return HistoryImpl(recordId, filesDir, logger) 70 | } 71 | 72 | @Provides 73 | @PerActivity 74 | fun provideDrawResourceProvider(): DrawResourceProvider { 75 | return DrawResourceProviderImpl(resources) 76 | } 77 | 78 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/draw/di/ToolsModule.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.draw.di 2 | 3 | import com.tomclaw.drawa.draw.ToolProvider 4 | import com.tomclaw.drawa.draw.ToolProviderImpl 5 | import com.tomclaw.drawa.draw.tools.Brush 6 | import com.tomclaw.drawa.draw.tools.Eraser 7 | import com.tomclaw.drawa.draw.tools.Fill 8 | import com.tomclaw.drawa.draw.tools.Fluffy 9 | import com.tomclaw.drawa.draw.tools.Marker 10 | import com.tomclaw.drawa.draw.tools.Pencil 11 | import com.tomclaw.drawa.draw.tools.Tool 12 | import com.tomclaw.drawa.util.PerActivity 13 | import dagger.Module 14 | import dagger.Provides 15 | import dagger.multibindings.IntoSet 16 | 17 | @Module 18 | class ToolsModule { 19 | 20 | @Provides 21 | @PerActivity 22 | fun provideToolProvider(toolSet: Set<@JvmSuppressWildcards Tool>): ToolProvider { 23 | return ToolProviderImpl(toolSet) 24 | } 25 | 26 | @IntoSet 27 | @Provides 28 | @PerActivity 29 | fun providePencil(): Tool = Pencil() 30 | 31 | @IntoSet 32 | @Provides 33 | @PerActivity 34 | fun provideBrush(): Tool = Brush() 35 | 36 | @IntoSet 37 | @Provides 38 | @PerActivity 39 | fun provideMarker(): Tool = Marker() 40 | 41 | @IntoSet 42 | @Provides 43 | @PerActivity 44 | fun provideFluffy(): Tool = Fluffy() 45 | 46 | @IntoSet 47 | @Provides 48 | @PerActivity 49 | fun provideFill(): Tool = Fill() 50 | 51 | @IntoSet 52 | @Provides 53 | @PerActivity 54 | fun provideEraser(): Tool = Eraser() 55 | 56 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/draw/tools/Brush.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.draw.tools 2 | 3 | import android.graphics.Paint 4 | import android.graphics.Path 5 | import kotlin.math.abs 6 | import kotlin.math.sqrt 7 | 8 | class Brush : Tool() { 9 | 10 | private var startX: Int = 0 11 | private var startY: Int = 0 12 | private var prevX: Int = 0 13 | private var prevY: Int = 0 14 | private var path = Path() 15 | 16 | private var startStrokeSize: Float = 0.0f 17 | 18 | override val alpha = 0xff 19 | override val type = TYPE_BRUSH 20 | 21 | override fun initPaint() = Paint().apply { 22 | isAntiAlias = true 23 | isDither = true 24 | style = Paint.Style.STROKE 25 | strokeJoin = Paint.Join.ROUND 26 | strokeCap = Paint.Cap.ROUND 27 | } 28 | 29 | override fun onTouchDown(x: Int, y: Int) { 30 | resetRadius() 31 | startStrokeSize = strokeSize 32 | 33 | startX = x 34 | startY = y 35 | 36 | path.moveTo(x.toFloat(), y.toFloat()) 37 | path.lineTo(x.toFloat(), y.toFloat()) 38 | 39 | prevX = x 40 | prevY = y 41 | 42 | drawPath(path) 43 | } 44 | 45 | override fun onTouchMove(x: Int, y: Int) { 46 | if (path.isEmpty) { 47 | path.moveTo(prevX.toFloat(), prevY.toFloat()) 48 | } 49 | path.quadTo( 50 | prevX.toFloat(), 51 | prevY.toFloat(), 52 | ((x + prevX).toFloat() / 2), 53 | ((y + prevY).toFloat() / 2) 54 | ) 55 | 56 | val deltaX = abs(x - prevX) 57 | val deltaY = abs(y - prevY) 58 | val length = sqrt((deltaX * deltaX + deltaY * deltaY).toDouble()) 59 | val sizeStep = defaultRadius / 20 60 | var size = strokeSize 61 | if (length * 5 < defaultRadius) { 62 | size += sizeStep 63 | 64 | path.reset() 65 | path.moveTo(prevX.toFloat(), prevY.toFloat()) 66 | path.lineTo(x.toFloat(), y.toFloat()) 67 | } else { 68 | size -= sizeStep 69 | } 70 | if (size > startStrokeSize / SIZE_MULTIPLIER && size < startStrokeSize * SIZE_MULTIPLIER) { 71 | strokeSize = size 72 | } 73 | 74 | prevX = x 75 | prevY = y 76 | 77 | drawPath(path) 78 | } 79 | 80 | override fun onTouchUp(x: Int, y: Int) { 81 | if (path.isEmpty) { 82 | path.moveTo(prevX.toFloat(), prevY.toFloat()) 83 | } 84 | if (x == startX && y == startY) { 85 | path.lineTo(x + 0.1f, y.toFloat()) 86 | } else { 87 | path.lineTo(x.toFloat(), y.toFloat()) 88 | } 89 | 90 | drawPath(path) 91 | 92 | prevX = 0 93 | prevY = 0 94 | 95 | path.reset() 96 | } 97 | 98 | override fun onDraw() {} 99 | 100 | } 101 | 102 | private const val SIZE_MULTIPLIER = 2f 103 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/draw/tools/Eraser.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.draw.tools 2 | 3 | import android.graphics.Color 4 | import android.graphics.MaskFilter 5 | import android.graphics.Paint 6 | import android.graphics.Path 7 | import android.graphics.PathEffect 8 | import android.graphics.PorterDuff 9 | import android.graphics.PorterDuffXfermode 10 | 11 | class Eraser : Tool() { 12 | 13 | private var startX: Int = 0 14 | private var startY: Int = 0 15 | private var prevX: Int = 0 16 | private var prevY: Int = 0 17 | private var path = Path() 18 | 19 | override var color = ERASER_COLOR 20 | override val alpha = 0xff 21 | override val type = TYPE_ERASER 22 | 23 | override fun initPaint() = Paint().apply { 24 | isAntiAlias = true 25 | isDither = true 26 | style = Paint.Style.STROKE 27 | strokeJoin = Paint.Join.ROUND 28 | strokeCap = Paint.Cap.ROUND 29 | color = ERASER_COLOR 30 | xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) 31 | } 32 | 33 | override fun onTouchDown(x: Int, y: Int) { 34 | resetRadius() 35 | 36 | startX = x 37 | startY = y 38 | 39 | path.moveTo(x.toFloat(), y.toFloat()) 40 | path.lineTo(x.toFloat(), y.toFloat()) 41 | 42 | prevX = x 43 | prevY = y 44 | 45 | drawPath(path) 46 | } 47 | 48 | override fun onTouchMove(x: Int, y: Int) { 49 | if (path.isEmpty) { 50 | path.moveTo(prevX.toFloat(), prevY.toFloat()) 51 | } 52 | if (x == startX && y == startY) { 53 | path.lineTo(x + 0.1f, y.toFloat()) 54 | } else { 55 | path.quadTo( 56 | prevX.toFloat(), 57 | prevY.toFloat(), 58 | ((x + prevX) / 2).toFloat(), 59 | ((y + prevY) / 2).toFloat() 60 | ) 61 | } 62 | 63 | prevX = x 64 | prevY = y 65 | 66 | drawPath(path) 67 | } 68 | 69 | override fun onTouchUp(x: Int, y: Int) { 70 | if (path.isEmpty) { 71 | path.moveTo(prevX.toFloat(), prevY.toFloat()) 72 | } 73 | path.quadTo(prevX.toFloat(), prevY.toFloat(), x.toFloat(), y.toFloat()) 74 | 75 | path.reset() 76 | 77 | drawPath(path) 78 | 79 | prevX = 0 80 | prevY = 0 81 | } 82 | 83 | override fun onDraw() {} 84 | 85 | } 86 | 87 | const val ERASER_COLOR = Color.TRANSPARENT 88 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/draw/tools/Fill.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.draw.tools 2 | 3 | import android.graphics.Paint 4 | import com.tomclaw.drawa.util.QueueLinearFloodFiller 5 | 6 | class Fill : Tool() { 7 | 8 | override val alpha = 0xff 9 | override val type = TYPE_FILL 10 | 11 | override fun initPaint() = Paint().apply { 12 | isAntiAlias = true 13 | isDither = true 14 | style = Paint.Style.STROKE 15 | strokeJoin = Paint.Join.ROUND 16 | strokeCap = Paint.Cap.ROUND 17 | } 18 | 19 | override fun onTouchDown(x: Int, y: Int) { 20 | val color = color 21 | val pixel = bitmap.getPixel(x, y) 22 | QueueLinearFloodFiller(bitmap, pixel, color).run { 23 | setTolerance(COLOR_DELTA) 24 | floodFill(x, y) 25 | } 26 | } 27 | 28 | override fun onTouchUp(x: Int, y: Int) {} 29 | 30 | override fun onDraw() {} 31 | 32 | override fun onTouchMove(x: Int, y: Int) {} 33 | 34 | } 35 | 36 | private const val COLOR_DELTA = 0x32 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/draw/tools/Fluffy.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.draw.tools 2 | 3 | import android.graphics.DiscretePathEffect 4 | import android.graphics.Paint 5 | import android.graphics.Path 6 | import java.util.Random 7 | 8 | class Fluffy : Tool() { 9 | 10 | private var startX: Int = 0 11 | private var startY: Int = 0 12 | private var prevX: Int = 0 13 | private var prevY: Int = 0 14 | private var path = Path() 15 | private var random = Random() 16 | 17 | override val alpha = 0x20 18 | override val type = TYPE_FLUFFY 19 | 20 | override fun initPaint() = Paint().apply { 21 | isAntiAlias = true 22 | isDither = true 23 | style = Paint.Style.STROKE 24 | strokeJoin = Paint.Join.MITER 25 | strokeCap = Paint.Cap.SQUARE 26 | strokeMiter = 0.2f 27 | pathEffect = DiscretePathEffect(2f, 2f) 28 | } 29 | 30 | override fun onTouchDown(x: Int, y: Int) { 31 | resetRadius() 32 | 33 | startX = x 34 | startY = y 35 | 36 | path.moveTo(x.toFloat(), y.toFloat()) 37 | path.lineTo(x.toFloat(), y.toFloat()) 38 | 39 | prevX = x 40 | prevY = y 41 | 42 | drawPath(path) 43 | } 44 | 45 | override fun onTouchMove(x: Int, y: Int) { 46 | if (path.isEmpty) { 47 | path.moveTo(prevX.toFloat(), prevY.toFloat()) 48 | } 49 | path.lineTo(x.toFloat(), y.toFloat()) 50 | 51 | prevX = x 52 | prevY = y 53 | 54 | drawPath(path) 55 | } 56 | 57 | override fun onTouchUp(x: Int, y: Int) { 58 | if (path.isEmpty) { 59 | path.moveTo(prevX.toFloat(), prevY.toFloat()) 60 | } 61 | if (x == startX && y == startY) { 62 | for (c in 0..2) { 63 | path.lineTo(randomizeCoordinate(x).toFloat(), randomizeCoordinate(y).toFloat()) 64 | drawPath(path) 65 | } 66 | } else { 67 | path.lineTo(x.toFloat(), y.toFloat()) 68 | } 69 | 70 | drawPath(path) 71 | 72 | prevX = 0 73 | prevY = 0 74 | } 75 | 76 | override fun onDraw() { 77 | path.reset() 78 | } 79 | 80 | private fun randomizeCoordinate(value: Int): Int { 81 | return value + random.nextInt(DOT_RADIUS + 1) - DOT_RADIUS / 2 82 | } 83 | 84 | } 85 | 86 | private const val DOT_RADIUS = 6 87 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/draw/tools/Marker.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.draw.tools 2 | 3 | import android.graphics.DashPathEffect 4 | import android.graphics.Paint 5 | import android.graphics.Path 6 | import java.util.Random 7 | 8 | class Marker : Tool() { 9 | 10 | private var startX: Int = 0 11 | private var startY: Int = 0 12 | private var prevX: Int = 0 13 | private var prevY: Int = 0 14 | private var path = Path() 15 | private var random = Random() 16 | 17 | override val alpha = 0x50 18 | override val type = TYPE_MARKER 19 | 20 | override fun initPaint() = Paint().apply { 21 | isAntiAlias = true 22 | isDither = true 23 | style = Paint.Style.STROKE 24 | strokeJoin = Paint.Join.MITER 25 | strokeCap = Paint.Cap.BUTT 26 | pathEffect = DashPathEffect(floatArrayOf(2f, 0f), 0f) 27 | } 28 | 29 | override fun onTouchDown(x: Int, y: Int) { 30 | resetRadius() 31 | 32 | startX = x 33 | startY = y 34 | 35 | path.moveTo(x.toFloat(), y.toFloat()) 36 | path.lineTo(x.toFloat(), y.toFloat()) 37 | 38 | prevX = x 39 | prevY = y 40 | 41 | drawPath(path) 42 | } 43 | 44 | override fun onTouchMove(x: Int, y: Int) { 45 | if (path.isEmpty) { 46 | path.moveTo(prevX.toFloat(), prevY.toFloat()) 47 | } 48 | path.lineTo(x.toFloat(), y.toFloat()) 49 | 50 | prevX = x 51 | prevY = y 52 | 53 | drawPath(path) 54 | } 55 | 56 | override fun onTouchUp(x: Int, y: Int) { 57 | if (path.isEmpty) { 58 | path.moveTo(prevX.toFloat(), prevY.toFloat()) 59 | } 60 | if (x == startX && y == startY) { 61 | for (c in 0..2) { 62 | path.lineTo(randomizeCoordinate(x).toFloat(), randomizeCoordinate(y).toFloat()) 63 | drawPath(path) 64 | } 65 | } else { 66 | path.lineTo(x.toFloat(), y.toFloat()) 67 | } 68 | 69 | drawPath(path) 70 | 71 | prevX = 0 72 | prevY = 0 73 | } 74 | 75 | override fun onDraw() = path.reset() 76 | 77 | private fun randomizeCoordinate(value: Int) = 78 | value + random.nextInt(DOT_RADIUS + 1) - DOT_RADIUS / 2 79 | 80 | } 81 | 82 | private const val DOT_RADIUS = 4 83 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/draw/tools/Pencil.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.draw.tools 2 | 3 | import android.graphics.Paint 4 | import android.graphics.Path 5 | 6 | class Pencil : Tool() { 7 | 8 | private var startX: Int = 0 9 | private var startY: Int = 0 10 | private var prevX: Int = 0 11 | private var prevY: Int = 0 12 | private var path = Path() 13 | 14 | override val alpha = 0xff 15 | override val type = TYPE_PENCIL 16 | 17 | override fun initPaint() = Paint().apply { 18 | isAntiAlias = true 19 | isDither = true 20 | style = Paint.Style.STROKE 21 | strokeJoin = Paint.Join.ROUND 22 | strokeCap = Paint.Cap.ROUND 23 | } 24 | 25 | override fun onTouchDown(x: Int, y: Int) { 26 | resetRadius() 27 | 28 | startX = x 29 | startY = y 30 | 31 | path.moveTo(x.toFloat(), y.toFloat()) 32 | path.lineTo(x.toFloat(), y.toFloat()) 33 | 34 | prevX = x 35 | prevY = y 36 | 37 | drawPath(path) 38 | } 39 | 40 | override fun onTouchMove(x: Int, y: Int) { 41 | if (path.isEmpty) { 42 | path.moveTo(prevX.toFloat(), prevY.toFloat()) 43 | } 44 | if (x == startX && y == startY) { 45 | path.lineTo(x + 0.1f, y.toFloat()) 46 | } else { 47 | path.quadTo( 48 | prevX.toFloat(), 49 | prevY.toFloat(), 50 | ((x + prevX) / 2).toFloat(), 51 | ((y + prevY) / 2).toFloat() 52 | ) 53 | } 54 | 55 | prevX = x 56 | prevY = y 57 | 58 | drawPath(path) 59 | } 60 | 61 | override fun onTouchUp(x: Int, y: Int) { 62 | if (path.isEmpty) { 63 | path.moveTo(prevX.toFloat(), prevY.toFloat()) 64 | } 65 | path.quadTo(prevX.toFloat(), prevY.toFloat(), x.toFloat(), y.toFloat()) 66 | 67 | path.reset() 68 | 69 | drawPath(path) 70 | 71 | prevX = 0 72 | prevY = 0 73 | } 74 | 75 | override fun onDraw() {} 76 | 77 | } 78 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/draw/tools/Size.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.draw.tools 2 | 3 | const val SIZE_S = 8 4 | const val SIZE_M = 16 5 | const val SIZE_L = 24 6 | const val SIZE_XL = 32 7 | const val SIZE_XXL = 40 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/draw/tools/Tool.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.draw.tools 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.Canvas 5 | import android.graphics.Color 6 | import android.graphics.Paint 7 | import android.graphics.Path 8 | import android.graphics.Rect 9 | import com.tomclaw.drawa.draw.DrawHost 10 | import com.tomclaw.drawa.util.MetricsProvider 11 | import kotlin.math.min 12 | 13 | abstract class Tool { 14 | 15 | private lateinit var callback: DrawHost 16 | private lateinit var metricsProvider: MetricsProvider 17 | 18 | lateinit var paint: Paint 19 | private set 20 | 21 | var size: Int = SIZE_M 22 | 23 | abstract val alpha: Int 24 | 25 | open var color: Int 26 | get() { 27 | val color = paint.color 28 | return Color.rgb(Color.red(color), Color.green(color), Color.blue(color)) 29 | } 30 | set(color) { 31 | paint.color = -0x1 32 | paint.color = Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color)) 33 | } 34 | 35 | protected val bitmap: Bitmap 36 | get() = callback.bitmap 37 | 38 | private val canvas: Canvas 39 | get() = callback.canvas 40 | 41 | abstract val type: Int 42 | 43 | var strokeSize: Float 44 | get() = paint.strokeWidth 45 | set(strokeSize) { 46 | paint.strokeWidth = strokeSize 47 | } 48 | 49 | var defaultRadius: Float = 0.0f 50 | 51 | fun initialize(callback: DrawHost, metricsProvider: MetricsProvider) { 52 | this.callback = callback 53 | this.metricsProvider = metricsProvider 54 | this.paint = initPaint() 55 | 56 | defaultRadius = convertSize(SIZE_L) 57 | } 58 | 59 | abstract fun initPaint(): Paint 60 | 61 | abstract fun onTouchDown(x: Int, y: Int) 62 | 63 | abstract fun onTouchMove(x: Int, y: Int) 64 | 65 | abstract fun onTouchUp(x: Int, y: Int) 66 | 67 | abstract fun onDraw() 68 | 69 | fun drawPath(path: Path) { 70 | canvas.drawPath(path, paint) 71 | } 72 | 73 | fun resetRadius() { 74 | strokeSize = convertSize(size) 75 | } 76 | 77 | private fun convertSize(size: Int): Float { 78 | val pixelSize = metricsProvider.convertDpToPixel(dp = size.toFloat()) 79 | return pixelSize * callback.bitmap.width / metricsProvider.getScreenSize().minDimension() 80 | } 81 | 82 | private fun Rect.minDimension(): Int { 83 | return min(width(), height()) 84 | } 85 | 86 | } 87 | 88 | const val TYPE_PENCIL = 1 89 | const val TYPE_BRUSH = 2 90 | const val TYPE_MARKER = 3 91 | const val TYPE_FLUFFY = 4 92 | const val TYPE_FILL = 5 93 | const val TYPE_ERASER = 6 94 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/draw/view/DrawingListener.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.draw.view 2 | 3 | interface DrawingListener { 4 | 5 | fun onTouchEvent(event: TouchEvent) 6 | 7 | fun onDraw() 8 | 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/draw/view/DrawingView.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.draw.view 2 | 3 | import android.content.Context 4 | import android.graphics.Canvas 5 | import android.graphics.Paint 6 | import android.graphics.Rect 7 | import android.util.AttributeSet 8 | import android.view.MotionEvent 9 | import android.view.View 10 | import com.tomclaw.drawa.R 11 | import com.tomclaw.drawa.draw.BitmapDrawHost 12 | import com.tomclaw.drawa.draw.BitmapHost 13 | import com.tomclaw.drawa.draw.DrawHost 14 | import kotlin.math.min 15 | 16 | class DrawingView( 17 | context: Context, 18 | attributeSet: AttributeSet 19 | ) : View(context, attributeSet), BitmapHost by BitmapDrawHost(), DrawHost { 20 | 21 | private var dst: Rect? = null 22 | 23 | override val paint: Paint = Paint().apply { 24 | isAntiAlias = true 25 | isDither = true 26 | isFilterBitmap = true 27 | } 28 | 29 | var drawingListener: DrawingListener? = null 30 | 31 | override fun onDraw(canvas: Canvas) { 32 | if (dst == null) { 33 | dst = Rect(0, 0, width, height) 34 | } 35 | val dst = dst ?: return 36 | drawTransparency(canvas) 37 | canvas.drawBitmap(normalBitmap, src, dst, paint) 38 | 39 | drawingListener?.onDraw() 40 | } 41 | 42 | private fun drawTransparency(canvas: Canvas) { 43 | canvas.drawColor(resources.getColor(R.color.transparent_chess_light)) 44 | paint.color = resources.getColor(R.color.transparent_chess_dark) 45 | 46 | val size = resources.getDimensionPixelSize(R.dimen.transparent_chess_size) 47 | 48 | val colCount = width / size + 1 49 | val rowCount = height / size + 1 50 | 51 | for (vrt in 0 until colCount step 1) { 52 | val start = vrt % 2 53 | for (hrz in start until rowCount step 2) { 54 | val hrzPxl = size * hrz.toFloat() 55 | val vrtPxl = size * vrt.toFloat() 56 | canvas.drawRect(hrzPxl, vrtPxl, hrzPxl + size, vrtPxl + size, paint) 57 | } 58 | } 59 | } 60 | 61 | override fun dispatchTouchEvent(event: MotionEvent): Boolean { 62 | val eventX = (bitmap.width * event.x / width).toInt() 63 | val eventY = (bitmap.height * event.y / height).toInt() 64 | drawingListener?.onTouchEvent(TouchEvent(eventX, eventY, event.action)) 65 | invalidate() 66 | return true 67 | } 68 | 69 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 70 | val size = min(widthMeasureSpec, heightMeasureSpec) 71 | super.onMeasure(size, size) 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/draw/view/TouchEvent.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.draw.view 2 | 3 | data class TouchEvent( 4 | val eventX: Int, 5 | val eventY: Int, 6 | val action: Int 7 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/dto/Record.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.dto 2 | 3 | import android.os.Parcel 4 | import android.os.Parcelable 5 | 6 | class Record( 7 | val id: Int, 8 | val size: Size, 9 | var time: Long = System.currentTimeMillis() 10 | ) : Parcelable { 11 | 12 | override fun writeToParcel(dest: Parcel, flags: Int) = with(dest) { 13 | writeInt(id) 14 | writeParcelable(size, flags) 15 | writeLong(time) 16 | } 17 | 18 | override fun describeContents(): Int = 0 19 | 20 | companion object CREATOR : Parcelable.Creator { 21 | override fun createFromParcel(parcel: Parcel): Record { 22 | val id = parcel.readInt() 23 | val size = parcel.readParcelable(Size::class.java.classLoader)!! 24 | val time = parcel.readLong() 25 | return Record(id, size, time) 26 | } 27 | 28 | override fun newArray(size: Int): Array { 29 | return arrayOfNulls(size) 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/dto/Size.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.dto 2 | 3 | import android.os.Parcel 4 | import android.os.Parcelable 5 | 6 | class Size( 7 | val width: Int, 8 | val height: Int 9 | ) : Parcelable { 10 | 11 | override fun writeToParcel(dest: Parcel, flags: Int) = with(dest) { 12 | writeInt(width) 13 | writeInt(height) 14 | } 15 | 16 | override fun describeContents(): Int = 0 17 | 18 | companion object CREATOR : Parcelable.Creator { 19 | override fun createFromParcel(parcel: Parcel): Size { 20 | val width = parcel.readInt() 21 | val height = parcel.readInt() 22 | return Size(width, height) 23 | } 24 | 25 | override fun newArray(size: Int): Array { 26 | return arrayOfNulls(size) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/info/InfoActivity.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.info 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.content.Intent.ACTION_VIEW 6 | import android.net.Uri 7 | import android.os.Bundle 8 | import androidx.appcompat.app.AppCompatActivity 9 | import com.tomclaw.drawa.R 10 | import com.tomclaw.drawa.info.di.InfoModule 11 | import com.tomclaw.drawa.main.getComponent 12 | import javax.inject.Inject 13 | 14 | class InfoActivity : AppCompatActivity(), InfoPresenter.InfoRouter { 15 | 16 | @Inject 17 | lateinit var presenter: InfoPresenter 18 | 19 | override fun onCreate(savedInstanceState: Bundle?) { 20 | application.getComponent() 21 | .infoComponent(InfoModule(context = this)) 22 | .inject(activity = this) 23 | 24 | super.onCreate(savedInstanceState) 25 | setContentView(R.layout.info) 26 | 27 | val view = InfoViewImpl(window.decorView) 28 | 29 | presenter.attachView(view) 30 | } 31 | 32 | override fun onStart() { 33 | super.onStart() 34 | presenter.attachRouter(router = this) 35 | } 36 | 37 | override fun onStop() { 38 | presenter.detachRouter() 39 | super.onStop() 40 | } 41 | 42 | override fun onDestroy() { 43 | presenter.detachView() 44 | super.onDestroy() 45 | } 46 | 47 | override fun openRate() { 48 | openUriSafe( 49 | uri = MARKET_URI_RATE + packageName, 50 | fallback = WEB_URI_RATE + packageName 51 | ) 52 | } 53 | 54 | override fun openProjects() { 55 | openUriSafe( 56 | uri = MARKET_URI_PROJECTS + VENDOR_ID, 57 | fallback = WEB_URI_PROJECTS + VENDOR_ID 58 | ) 59 | } 60 | 61 | override fun leaveScreen() { 62 | finish() 63 | } 64 | 65 | private fun openUriSafe(uri: String, fallback: String) { 66 | try { 67 | startActivity(Intent(ACTION_VIEW, Uri.parse(uri))) 68 | } catch (ignored: android.content.ActivityNotFoundException) { 69 | startActivity(Intent(ACTION_VIEW, Uri.parse(fallback))) 70 | } 71 | } 72 | 73 | } 74 | 75 | fun createInfoActivityIntent(context: Context): Intent = 76 | Intent(context, InfoActivity::class.java) 77 | 78 | private const val VENDOR_ID = "TomClaw" 79 | private const val MARKET_URI_RATE = "market://details?id=" 80 | private const val MARKET_URI_PROJECTS = "market://search?q=" 81 | private const val WEB_URI_RATE = "https://play.google.com/store/apps/details?id=" 82 | private const val WEB_URI_PROJECTS = "https://play.google.com/store/apps/search?q=" 83 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/info/InfoPresenter.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.info 2 | 3 | import io.reactivex.disposables.CompositeDisposable 4 | import io.reactivex.rxkotlin.plusAssign 5 | 6 | interface InfoPresenter { 7 | 8 | fun attachView(view: InfoView) 9 | 10 | fun detachView() 11 | 12 | fun attachRouter(router: InfoRouter) 13 | 14 | fun detachRouter() 15 | 16 | interface InfoRouter { 17 | 18 | fun openRate() 19 | 20 | fun openProjects() 21 | 22 | fun leaveScreen() 23 | 24 | } 25 | 26 | } 27 | 28 | class InfoPresenterImpl(private val resourceProvider: InfoResourceProvider) : InfoPresenter { 29 | 30 | private var view: InfoView? = null 31 | private var router: InfoPresenter.InfoRouter? = null 32 | 33 | private val subscriptions = CompositeDisposable() 34 | 35 | override fun attachView(view: InfoView) { 36 | this.view = view 37 | 38 | subscriptions += view.navigationClicks().subscribe { router?.leaveScreen() } 39 | subscriptions += view.rateClicks().subscribe { router?.openRate() } 40 | subscriptions += view.projectsClicks().subscribe { router?.openProjects() } 41 | 42 | bindVersion() 43 | } 44 | 45 | private fun bindVersion() { 46 | view?.setVersion(resourceProvider.provideVersion()) 47 | } 48 | 49 | override fun detachView() { 50 | subscriptions.clear() 51 | this.view = null 52 | } 53 | 54 | override fun attachRouter(router: InfoPresenter.InfoRouter) { 55 | this.router = router 56 | } 57 | 58 | override fun detachRouter() { 59 | this.router = null 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/info/InfoResourceProvider.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.info 2 | 3 | import android.content.pm.PackageManager 4 | import android.content.res.Resources 5 | import com.tomclaw.drawa.R 6 | 7 | interface InfoResourceProvider { 8 | 9 | fun provideVersion(): String 10 | 11 | } 12 | 13 | class InfoResourceProviderImpl( 14 | private val packageName: String, 15 | private val packageManager: PackageManager, 16 | private val resources: Resources 17 | ) : InfoResourceProvider { 18 | 19 | override fun provideVersion(): String { 20 | try { 21 | val info = packageManager.getPackageInfo(packageName, 0) 22 | return resources.getString(R.string.app_version, info.versionName, info.versionCode) 23 | } catch (ignored: PackageManager.NameNotFoundException) { 24 | } 25 | return "" 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/info/InfoView.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.info 2 | 3 | import android.view.View 4 | import android.widget.TextView 5 | import androidx.appcompat.widget.Toolbar 6 | import com.jakewharton.rxrelay2.PublishRelay 7 | import com.tomclaw.drawa.R 8 | import io.reactivex.Observable 9 | 10 | interface InfoView { 11 | 12 | fun navigationClicks(): Observable 13 | 14 | fun rateClicks(): Observable 15 | 16 | fun projectsClicks(): Observable 17 | 18 | fun setVersion(version: String) 19 | 20 | } 21 | 22 | class InfoViewImpl(view: View) : InfoView { 23 | 24 | private val toolbar: Toolbar = view.findViewById(R.id.toolbar) 25 | private val rateButton: View = view.findViewById(R.id.rate_button) 26 | private val projectsButton: View = view.findViewById(R.id.projects_button) 27 | private val versionText: TextView = view.findViewById(R.id.app_version) 28 | 29 | private val navigationRelay = PublishRelay.create() 30 | private val rateRelay = PublishRelay.create() 31 | private val projectsRelay = PublishRelay.create() 32 | 33 | init { 34 | toolbar.setTitle(R.string.info) 35 | toolbar.setNavigationOnClickListener { navigationRelay.accept(Unit) } 36 | rateButton.setOnClickListener { rateRelay.accept(Unit) } 37 | projectsButton.setOnClickListener { projectsRelay.accept(Unit) } 38 | } 39 | 40 | override fun navigationClicks(): Observable = navigationRelay 41 | 42 | override fun rateClicks(): Observable = rateRelay 43 | 44 | override fun projectsClicks(): Observable = projectsRelay 45 | 46 | override fun setVersion(version: String) { 47 | versionText.text = version 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/info/di/InfoComponent.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.info.di 2 | 3 | import com.tomclaw.drawa.info.InfoActivity 4 | import com.tomclaw.drawa.util.PerActivity 5 | import dagger.Subcomponent 6 | 7 | @PerActivity 8 | @Subcomponent(modules = [InfoModule::class]) 9 | interface InfoComponent { 10 | 11 | fun inject(activity: InfoActivity) 12 | 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/info/di/InfoModule.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.info.di 2 | 3 | import android.content.Context 4 | import com.tomclaw.drawa.info.InfoPresenter 5 | import com.tomclaw.drawa.info.InfoPresenterImpl 6 | import com.tomclaw.drawa.info.InfoResourceProvider 7 | import com.tomclaw.drawa.info.InfoResourceProviderImpl 8 | import com.tomclaw.drawa.util.PerActivity 9 | import dagger.Module 10 | import dagger.Provides 11 | 12 | @Module 13 | class InfoModule(private val context: Context) { 14 | 15 | @Provides 16 | @PerActivity 17 | fun provideInfoPresenter(resourceProvider: InfoResourceProvider): InfoPresenter { 18 | return InfoPresenterImpl(resourceProvider) 19 | } 20 | 21 | @Provides 22 | @PerActivity 23 | fun provideInfoResourceProvider(): InfoResourceProvider { 24 | return InfoResourceProviderImpl( 25 | context.packageName, 26 | context.packageManager, 27 | context.resources 28 | ) 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/main/App.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.main 2 | 3 | import android.app.Application 4 | import com.tomclaw.drawa.di.AppComponent 5 | import com.tomclaw.drawa.di.AppModule 6 | import com.tomclaw.drawa.di.DaggerAppComponent 7 | 8 | class App : Application() { 9 | 10 | lateinit var component: AppComponent 11 | private set 12 | 13 | override fun onCreate() { 14 | super.onCreate() 15 | component = buildComponent() 16 | } 17 | 18 | private fun buildComponent(): AppComponent { 19 | return DaggerAppComponent.builder() 20 | .appModule(AppModule(this)) 21 | .build() 22 | } 23 | 24 | } 25 | 26 | fun Application.getComponent(): AppComponent { 27 | return (this as App).component 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/play/EventsDrawable.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.play 2 | 3 | import com.tomclaw.drawa.draw.DrawHost 4 | import com.tomclaw.drawa.draw.Event 5 | import com.tomclaw.drawa.util.StreamDrawable 6 | 7 | class EventsDrawable( 8 | drawHost: DrawHost, 9 | decoder: EventsProvider, 10 | renderer: EventsRenderer 11 | ) : StreamDrawable(drawHost.bitmap, drawHost.paint, decoder, renderer) 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/play/EventsProvider.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.play 2 | 3 | import com.tomclaw.drawa.draw.Event 4 | import com.tomclaw.drawa.draw.History 5 | import com.tomclaw.drawa.play.di.PLAY_HEIGHT 6 | import com.tomclaw.drawa.play.di.PLAY_WIDTH 7 | import com.tomclaw.drawa.util.SchedulersFactory 8 | import com.tomclaw.drawa.util.StreamDecoder 9 | import io.reactivex.disposables.CompositeDisposable 10 | import io.reactivex.rxkotlin.plusAssign 11 | 12 | class EventsProvider( 13 | private val history: History, 14 | private val schedulers: SchedulersFactory 15 | ) : StreamDecoder { 16 | 17 | private var events: Iterator? = null 18 | 19 | private val subscriptions = CompositeDisposable() 20 | 21 | override fun getWidth(): Int = PLAY_WIDTH 22 | 23 | override fun getHeight(): Int = PLAY_HEIGHT 24 | 25 | override fun hasFrame(): Boolean = events().hasNext() 26 | 27 | override fun readFrame(): Event = events().next() 28 | 29 | override fun getDelay(): Int = 10 30 | 31 | override fun stop() { 32 | subscriptions.clear() 33 | } 34 | 35 | fun reset() { 36 | loadEvents() 37 | } 38 | 39 | private fun events(): Iterator { 40 | return events ?: loadEvents() 41 | } 42 | 43 | private fun loadEvents(): Iterator { 44 | subscriptions += history.load() 45 | .subscribeOn(schedulers.trampoline()) 46 | .subscribe() 47 | val events = history.getEvents() 48 | this.events = events 49 | return events 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/play/EventsRenderer.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.play 2 | 3 | import android.view.MotionEvent 4 | import com.tomclaw.drawa.core.BITMAP_HEIGHT 5 | import com.tomclaw.drawa.core.BITMAP_WIDTH 6 | import com.tomclaw.drawa.draw.DrawHost 7 | import com.tomclaw.drawa.draw.Event 8 | import com.tomclaw.drawa.draw.ToolProvider 9 | import com.tomclaw.drawa.util.MetricsProvider 10 | import com.tomclaw.drawa.util.StreamRenderer 11 | 12 | class EventsRenderer( 13 | private val toolProvider: ToolProvider, 14 | private val metricsProvider: MetricsProvider, 15 | private val drawHost: DrawHost 16 | ) : StreamRenderer { 17 | 18 | init { 19 | toolProvider.listTools().forEach { it.initialize(drawHost, metricsProvider) } 20 | } 21 | 22 | override fun render(frame: Event) { 23 | processToolEvent(frame) 24 | } 25 | 26 | private fun processToolEvent(event: Event) { 27 | val tool = toolProvider.getTool(event.toolType) 28 | val x = (event.x * drawHost.bitmap.width / BITMAP_WIDTH) 29 | val y = (event.y * drawHost.bitmap.height / BITMAP_HEIGHT) 30 | with(tool) { 31 | when (event.action) { 32 | MotionEvent.ACTION_DOWN -> { 33 | color = event.color 34 | size = event.size 35 | onTouchDown(x, y) 36 | } 37 | MotionEvent.ACTION_MOVE -> onTouchMove(x, y) 38 | MotionEvent.ACTION_UP -> onTouchUp(x, y) 39 | } 40 | onDraw() 41 | } 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/play/PlayActivity.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.play 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import androidx.appcompat.app.AppCompatActivity 7 | import com.tomclaw.drawa.R 8 | import com.tomclaw.drawa.main.getComponent 9 | import com.tomclaw.drawa.play.di.PlayModule 10 | import com.tomclaw.drawa.share.createShareActivityIntent 11 | import javax.inject.Inject 12 | 13 | class PlayActivity : AppCompatActivity(), PlayPresenter.PlayRouter { 14 | 15 | @Inject 16 | lateinit var presenter: PlayPresenter 17 | 18 | override fun onCreate(savedInstanceState: Bundle?) { 19 | val recordId = intent.getRecordId() 20 | application.getComponent() 21 | .playComponent(PlayModule(recordId)) 22 | .inject(activity = this) 23 | 24 | super.onCreate(savedInstanceState) 25 | setContentView(R.layout.play) 26 | 27 | val view = PlayViewImpl(window.decorView) 28 | 29 | presenter.attachView(view) 30 | } 31 | 32 | override fun onStart() { 33 | super.onStart() 34 | presenter.attachRouter(router = this) 35 | } 36 | 37 | override fun onStop() { 38 | presenter.detachRouter() 39 | super.onStop() 40 | } 41 | 42 | override fun onDestroy() { 43 | presenter.detachView() 44 | super.onDestroy() 45 | } 46 | 47 | override fun showShareScreen() { 48 | val intent = createShareActivityIntent( 49 | context = this, 50 | recordId = intent.getRecordId() 51 | ) 52 | startActivity(intent) 53 | } 54 | 55 | override fun leaveScreen() { 56 | finish() 57 | } 58 | 59 | private fun Intent.getRecordId() = getIntExtra(EXTRA_RECORD_ID, RECORD_ID_INVALID).apply { 60 | if (this == RECORD_ID_INVALID) { 61 | throw IllegalArgumentException("record id must be specified") 62 | } 63 | } 64 | 65 | } 66 | 67 | fun createPlayActivityIntent( 68 | context: Context, 69 | recordId: Int 70 | ): Intent = Intent(context, PlayActivity::class.java) 71 | .putExtra(EXTRA_RECORD_ID, recordId) 72 | 73 | private const val EXTRA_RECORD_ID = "record_id" 74 | 75 | private const val RECORD_ID_INVALID = -1 76 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/play/PlayInteractor.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.play 2 | 3 | interface PlayInteractor {} -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/play/PlayPresenter.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.play 2 | 3 | import com.tomclaw.drawa.draw.DrawHost 4 | import com.tomclaw.drawa.util.StreamDrawable 5 | import io.reactivex.disposables.CompositeDisposable 6 | import io.reactivex.rxkotlin.plusAssign 7 | 8 | interface PlayPresenter { 9 | 10 | fun attachView(view: PlayView) 11 | 12 | fun detachView() 13 | 14 | fun attachRouter(router: PlayRouter) 15 | 16 | fun detachRouter() 17 | 18 | interface PlayRouter { 19 | 20 | fun showShareScreen() 21 | 22 | fun leaveScreen() 23 | 24 | } 25 | 26 | } 27 | 28 | class PlayPresenterImpl( 29 | private val drawHost: DrawHost, 30 | private val drawable: EventsDrawable, 31 | private val eventsProvider: EventsProvider 32 | ) : PlayPresenter { 33 | 34 | private var view: PlayView? = null 35 | private var router: PlayPresenter.PlayRouter? = null 36 | 37 | private val subscriptions = CompositeDisposable() 38 | 39 | override fun attachView(view: PlayView) { 40 | this.view = view 41 | 42 | subscriptions += view.navigationClicks().subscribe { router?.leaveScreen() } 43 | subscriptions += view.shareClicks().subscribe { onShare() } 44 | subscriptions += view.replayClicks().subscribe { onReplay() } 45 | 46 | drawable.listener = object : StreamDrawable.AnimationListener { 47 | 48 | override fun onAnimationStart() { 49 | view.hideReplayButton() 50 | drawHost.clearBitmap() 51 | } 52 | 53 | override fun onAnimationEnd() { 54 | view.showReplayButton() 55 | } 56 | } 57 | 58 | showDrawable() 59 | } 60 | 61 | override fun detachView() { 62 | drawable.stop() 63 | subscriptions.clear() 64 | this.view = null 65 | } 66 | 67 | override fun attachRouter(router: PlayPresenter.PlayRouter) { 68 | this.router = router 69 | } 70 | 71 | override fun detachRouter() { 72 | this.router = null 73 | } 74 | 75 | private fun showDrawable() { 76 | view?.showDrawable(drawable) 77 | } 78 | 79 | private fun onShare() { 80 | router?.showShareScreen() 81 | router?.leaveScreen() 82 | } 83 | 84 | private fun onReplay() { 85 | eventsProvider.reset() 86 | drawable.start() 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/play/PlayView.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.play 2 | 3 | import android.graphics.drawable.Animatable 4 | import android.graphics.drawable.Drawable 5 | import android.view.View 6 | import android.widget.ImageView 7 | import androidx.appcompat.widget.Toolbar 8 | import com.jakewharton.rxrelay2.PublishRelay 9 | import com.tomclaw.drawa.R 10 | import com.tomclaw.drawa.util.show 11 | import com.tomclaw.drawa.util.toggle 12 | import io.reactivex.Observable 13 | 14 | interface PlayView { 15 | 16 | fun navigationClicks(): Observable 17 | 18 | fun shareClicks(): Observable 19 | 20 | fun replayClicks(): Observable 21 | 22 | fun showDrawable(drawable: Drawable) 23 | 24 | fun showReplayButton() 25 | 26 | fun hideReplayButton() 27 | 28 | } 29 | 30 | class PlayViewImpl(view: View) : PlayView { 31 | 32 | private val toolbar: Toolbar = view.findViewById(R.id.toolbar) 33 | private val imageView: ImageView = view.findViewById(R.id.image_view) 34 | 35 | private val navigationRelay = PublishRelay.create() 36 | private val shareRelay = PublishRelay.create() 37 | private val replayRelay = PublishRelay.create() 38 | 39 | init { 40 | toolbar.setTitle(R.string.play) 41 | toolbar.setNavigationOnClickListener { navigationRelay.accept(Unit) } 42 | toolbar.inflateMenu(R.menu.play) 43 | toolbar.setOnMenuItemClickListener { item -> 44 | when (item.itemId) { 45 | R.id.menu_share -> shareRelay.accept(Unit) 46 | R.id.menu_replay -> replayRelay.accept(Unit) 47 | } 48 | true 49 | } 50 | imageView.scaleType = ImageView.ScaleType.FIT_CENTER 51 | imageView.setOnClickListener { toolbar.toggle() } 52 | } 53 | 54 | override fun navigationClicks(): Observable = navigationRelay 55 | 56 | override fun shareClicks(): Observable = shareRelay 57 | 58 | override fun replayClicks(): Observable = replayRelay 59 | 60 | override fun showDrawable(drawable: Drawable) { 61 | imageView.setImageDrawable(drawable) 62 | (drawable as? Animatable)?.start() 63 | } 64 | 65 | override fun showReplayButton() { 66 | toolbar.menu.findItem(R.id.menu_replay).isVisible = true 67 | toolbar.show() 68 | } 69 | 70 | override fun hideReplayButton() { 71 | toolbar.menu.findItem(R.id.menu_replay).isVisible = false 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/play/di/PlayComponent.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.play.di 2 | 3 | import com.tomclaw.drawa.draw.di.ToolsModule 4 | import com.tomclaw.drawa.play.PlayActivity 5 | import com.tomclaw.drawa.util.PerActivity 6 | import dagger.Subcomponent 7 | 8 | @PerActivity 9 | @Subcomponent(modules = [PlayModule::class, ToolsModule::class]) 10 | interface PlayComponent { 11 | 12 | fun inject(activity: PlayActivity) 13 | 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/play/di/PlayModule.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.play.di 2 | 3 | import com.tomclaw.drawa.core.BITMAP_HEIGHT 4 | import com.tomclaw.drawa.core.BITMAP_WIDTH 5 | import com.tomclaw.drawa.draw.DrawHost 6 | import com.tomclaw.drawa.draw.History 7 | import com.tomclaw.drawa.draw.HistoryImpl 8 | import com.tomclaw.drawa.draw.ToolProvider 9 | import com.tomclaw.drawa.play.EventsDrawable 10 | import com.tomclaw.drawa.play.EventsProvider 11 | import com.tomclaw.drawa.play.EventsRenderer 12 | import com.tomclaw.drawa.play.PlayPresenter 13 | import com.tomclaw.drawa.play.PlayPresenterImpl 14 | import com.tomclaw.drawa.share.DetachedDrawHost 15 | import com.tomclaw.drawa.util.Logger 16 | import com.tomclaw.drawa.util.MetricsProvider 17 | import com.tomclaw.drawa.util.PerActivity 18 | import com.tomclaw.drawa.util.SchedulersFactory 19 | import dagger.Module 20 | import dagger.Provides 21 | import java.io.File 22 | 23 | @Module 24 | class PlayModule(private val recordId: Int) { 25 | 26 | @Provides 27 | @PerActivity 28 | fun providePlayPresenter( 29 | drawHost: DrawHost, 30 | drawable: EventsDrawable, 31 | decoder: EventsProvider 32 | ): PlayPresenter { 33 | return PlayPresenterImpl(drawHost, drawable, decoder) 34 | } 35 | 36 | @Provides 37 | @PerActivity 38 | fun provideHistory(filesDir: File, logger: Logger): History { 39 | return HistoryImpl(recordId, filesDir, logger) 40 | } 41 | 42 | @Provides 43 | @PerActivity 44 | fun provideStreamDrawable( 45 | drawHost: DrawHost, 46 | decoder: EventsProvider, 47 | renderer: EventsRenderer 48 | ) = EventsDrawable(drawHost, decoder, renderer) 49 | 50 | @Provides 51 | @PerActivity 52 | fun provideStreamRenderer( 53 | toolProvider: ToolProvider, 54 | metricsProvider: MetricsProvider, 55 | drawHost: DrawHost 56 | ) = EventsRenderer(toolProvider, metricsProvider, drawHost) 57 | 58 | @Provides 59 | @PerActivity 60 | fun provideStreamDecoder( 61 | history: History, 62 | schedulers: SchedulersFactory 63 | ) = EventsProvider(history, schedulers) 64 | 65 | @Provides 66 | @PerActivity 67 | fun provideDrawHost(): DrawHost { 68 | return DetachedDrawHost(PLAY_WIDTH, PLAY_HEIGHT) 69 | } 70 | 71 | } 72 | 73 | const val PLAY_WIDTH = BITMAP_WIDTH 74 | const val PLAY_HEIGHT = BITMAP_HEIGHT -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/share/DetachedDrawHost.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.share 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.Canvas 5 | import android.graphics.Color 6 | import android.graphics.Paint 7 | import android.graphics.Rect 8 | import com.tomclaw.drawa.draw.BitmapDrawHost 9 | import com.tomclaw.drawa.draw.BitmapHost 10 | import com.tomclaw.drawa.draw.DrawHost 11 | 12 | class DetachedDrawHost(width: Int, height: Int) : DrawHost, BitmapHost by BitmapDrawHost() { 13 | 14 | private var dst: Rect = Rect(0, 0, width, height) 15 | 16 | override val paint: Paint = Paint().apply { 17 | isAntiAlias = true 18 | isDither = true 19 | isFilterBitmap = true 20 | } 21 | 22 | override val bitmap: Bitmap = Bitmap.createBitmap( 23 | width, 24 | height, 25 | Bitmap.Config.ARGB_8888 26 | ) 27 | 28 | override val canvas: Canvas = Canvas(bitmap) 29 | 30 | override fun invalidate() { 31 | canvas.drawBitmap(normalBitmap, src, dst, paint) 32 | } 33 | 34 | override fun clearBitmap() { 35 | canvas.drawColor(Color.TRANSPARENT) 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/share/ShareActivity.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.share 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.content.pm.PackageManager.MATCH_DEFAULT_ONLY 6 | import android.os.Bundle 7 | import androidx.appcompat.app.AppCompatActivity 8 | import androidx.core.app.ShareCompat 9 | import androidx.core.content.FileProvider 10 | import com.tomclaw.drawa.R 11 | import com.tomclaw.drawa.main.getComponent 12 | import com.tomclaw.drawa.share.di.ShareModule 13 | import com.tomclaw.drawa.util.DataProvider 14 | import java.io.File 15 | import javax.inject.Inject 16 | 17 | class ShareActivity : AppCompatActivity(), SharePresenter.ShareRouter { 18 | 19 | @Inject 20 | lateinit var presenter: SharePresenter 21 | 22 | @Inject 23 | lateinit var dataProvider: DataProvider 24 | 25 | override fun onCreate(savedInstanceState: Bundle?) { 26 | val recordId = intent.getRecordId() 27 | val presenterState = savedInstanceState?.getBundle(KEY_PRESENTER_STATE) 28 | application.getComponent() 29 | .shareComponent(ShareModule(recordId, presenterState)) 30 | .inject(activity = this) 31 | 32 | super.onCreate(savedInstanceState) 33 | setContentView(R.layout.share) 34 | 35 | val adapter = ShareAdapter(layoutInflater, dataProvider) 36 | val view = ShareViewImpl(window.decorView, adapter) 37 | 38 | presenter.attachView(view) 39 | } 40 | 41 | override fun onStart() { 42 | super.onStart() 43 | presenter.attachRouter(this) 44 | } 45 | 46 | override fun onStop() { 47 | presenter.detachRouter() 48 | super.onStop() 49 | } 50 | 51 | override fun onDestroy() { 52 | presenter.detachView() 53 | super.onDestroy() 54 | } 55 | 56 | override fun onSaveInstanceState(outState: Bundle) { 57 | super.onSaveInstanceState(outState) 58 | outState.putBundle(KEY_PRESENTER_STATE, presenter.saveState()) 59 | } 60 | 61 | override fun leaveScreen() { 62 | finish() 63 | } 64 | 65 | override fun shareFile(file: File, mime: String) { 66 | val uri = FileProvider.getUriForFile(this, packageName, file) 67 | ShareCompat.IntentBuilder.from(this) 68 | .setStream(uri) 69 | .setType(mime) 70 | .intent 71 | .setAction(Intent.ACTION_SEND) 72 | .setDataAndType(uri, mime) 73 | .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) 74 | .run { 75 | val list = packageManager.queryIntentActivities(this, MATCH_DEFAULT_ONLY) 76 | for (resolveInfo in list) { 77 | val packageName = resolveInfo.activityInfo.packageName 78 | grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) 79 | } 80 | if (resolveActivity(packageManager) != null) { 81 | startActivity(this) 82 | } 83 | } 84 | } 85 | 86 | private fun Intent.getRecordId() = getIntExtra(EXTRA_RECORD_ID, RECORD_ID_INVALID).apply { 87 | if (this == RECORD_ID_INVALID) { 88 | throw IllegalArgumentException("record id must be specified") 89 | } 90 | } 91 | 92 | } 93 | 94 | fun createShareActivityIntent( 95 | context: Context, 96 | recordId: Int 97 | ): Intent = Intent(context, ShareActivity::class.java) 98 | .putExtra(EXTRA_RECORD_ID, recordId) 99 | 100 | private const val KEY_PRESENTER_STATE = "presenter_state" 101 | 102 | private const val EXTRA_RECORD_ID = "record_id" 103 | 104 | private const val RECORD_ID_INVALID = -1 105 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/share/ShareAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.share 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.recyclerview.widget.RecyclerView 6 | import com.jakewharton.rxrelay2.PublishRelay 7 | import com.tomclaw.drawa.R 8 | import com.tomclaw.drawa.util.DataProvider 9 | 10 | class ShareAdapter( 11 | private val layoutInflater: LayoutInflater, 12 | private val dataProvider: DataProvider 13 | ) : RecyclerView.Adapter() { 14 | 15 | var itemRelay: PublishRelay? = null 16 | 17 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ShareItemHolder { 18 | val view = layoutInflater.inflate(R.layout.share_item_view, parent, false) 19 | return ShareItemHolder(view, itemRelay) 20 | } 21 | 22 | override fun onBindViewHolder(holder: ShareItemHolder, position: Int) { 23 | val item = dataProvider.getItem(position) 24 | holder.bind(item) 25 | } 26 | 27 | override fun getItemId(position: Int): Long = dataProvider.getItem(position).id.toLong() 28 | 29 | override fun getItemCount(): Int = dataProvider.size() 30 | 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/share/ShareInteractor.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.share 2 | 3 | import com.tomclaw.drawa.draw.History 4 | import com.tomclaw.drawa.util.SchedulersFactory 5 | import io.reactivex.Observable 6 | 7 | interface ShareInteractor { 8 | 9 | fun loadHistory(): Observable 10 | 11 | } 12 | 13 | class ShareInteractorImpl( 14 | private val history: History, 15 | private val schedulers: SchedulersFactory 16 | ) : ShareInteractor { 17 | 18 | override fun loadHistory(): Observable { 19 | return history.load() 20 | .toObservable() 21 | .subscribeOn(schedulers.io()) 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/share/ShareItem.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.share 2 | 3 | import android.os.Parcel 4 | import android.os.Parcelable 5 | import androidx.annotation.DrawableRes 6 | import androidx.annotation.StringRes 7 | 8 | class ShareItem( 9 | val id: Int, 10 | @DrawableRes val image: Int, 11 | @StringRes val title: Int, 12 | @StringRes val description: Int 13 | ) : Parcelable { 14 | 15 | override fun writeToParcel(dest: Parcel, flags: Int) = with(dest) { 16 | writeInt(id) 17 | writeInt(image) 18 | writeInt(title) 19 | writeInt(description) 20 | } 21 | 22 | override fun describeContents(): Int = 0 23 | 24 | companion object CREATOR : Parcelable.Creator { 25 | override fun createFromParcel(parcel: Parcel): ShareItem { 26 | val id = parcel.readInt() 27 | val image = parcel.readInt() 28 | val title = parcel.readInt() 29 | val description = parcel.readInt() 30 | return ShareItem(id, image, title, description) 31 | } 32 | 33 | override fun newArray(size: Int): Array { 34 | return arrayOfNulls(size) 35 | } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/share/ShareItemHolder.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.share 2 | 3 | import android.view.View 4 | import android.widget.ImageView 5 | import android.widget.TextView 6 | import androidx.recyclerview.widget.RecyclerView 7 | import com.jakewharton.rxrelay2.PublishRelay 8 | import com.tomclaw.drawa.R 9 | 10 | class ShareItemHolder( 11 | view: View, 12 | private val itemRelay: PublishRelay? 13 | ) : RecyclerView.ViewHolder(view) { 14 | 15 | private val imageView: ImageView = view.findViewById(R.id.type_image) 16 | private val titleView: TextView = view.findViewById(R.id.type_title) 17 | private val descriptionView: TextView = view.findViewById(R.id.type_description) 18 | private val selectButton: View = view.findViewById(R.id.select_button) 19 | 20 | fun bind(item: ShareItem) { 21 | imageView.setImageResource(item.image) 22 | titleView.setText(item.title) 23 | descriptionView.setText(item.description) 24 | 25 | selectButton.setOnClickListener { 26 | itemRelay?.accept(item) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/share/SharePlugin.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.share 2 | 3 | import io.reactivex.Observable 4 | import io.reactivex.Single 5 | 6 | interface SharePlugin { 7 | 8 | val weight: Int 9 | 10 | val image: Int 11 | 12 | val title: Int 13 | 14 | val description: Int 15 | 16 | val progress: Observable 17 | 18 | val operation: Single 19 | 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/share/SharePresenter.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.share 2 | 3 | import android.os.Bundle 4 | import com.tomclaw.drawa.util.DataProvider 5 | import com.tomclaw.drawa.util.SchedulersFactory 6 | import io.reactivex.disposables.CompositeDisposable 7 | import io.reactivex.rxkotlin.plusAssign 8 | import java.io.File 9 | import java.util.concurrent.TimeUnit 10 | 11 | interface SharePresenter { 12 | 13 | fun attachView(view: ShareView) 14 | 15 | fun detachView() 16 | 17 | fun attachRouter(router: ShareRouter) 18 | 19 | fun detachRouter() 20 | 21 | fun saveState(): Bundle 22 | 23 | interface ShareRouter { 24 | 25 | fun leaveScreen() 26 | 27 | fun shareFile(file: File, mime: String) 28 | 29 | } 30 | 31 | } 32 | 33 | class SharePresenterImpl( 34 | private val interactor: ShareInteractor, 35 | private val dataProvider: DataProvider, 36 | private val sharePlugins: Set, 37 | private val schedulers: SchedulersFactory, 38 | state: Bundle? 39 | ) : SharePresenter { 40 | 41 | private var view: ShareView? = null 42 | private var router: SharePresenter.ShareRouter? = null 43 | 44 | private val subscriptions = CompositeDisposable() 45 | 46 | private var itemsMap: Map = emptyMap() 47 | 48 | override fun attachView(view: ShareView) { 49 | this.view = view 50 | 51 | subscriptions += view.navigationClicks().subscribe { 52 | router?.leaveScreen() 53 | } 54 | subscriptions += view.itemClicks().subscribe { shareItem -> 55 | itemsMap[shareItem.id]?.let { runPlugin(it) } 56 | } 57 | 58 | loadHistory() 59 | } 60 | 61 | override fun detachView() { 62 | subscriptions.clear() 63 | this.view = null 64 | } 65 | 66 | override fun attachRouter(router: SharePresenter.ShareRouter) { 67 | this.router = router 68 | } 69 | 70 | override fun detachRouter() { 71 | this.router = null 72 | } 73 | 74 | override fun saveState() = Bundle().apply {} 75 | 76 | private fun loadHistory() { 77 | subscriptions += interactor.loadHistory() 78 | .observeOn(schedulers.mainThread()) 79 | .doOnSubscribe { view?.showProgress() } 80 | .doAfterTerminate { view?.showContent() } 81 | .subscribe( 82 | { onLoaded() }, 83 | { onError() } 84 | ) 85 | } 86 | 87 | private fun onLoaded() { 88 | itemsMap = sharePlugins.associateBy { it.weight } 89 | val shareItems = itemsMap.entries.asSequence() 90 | .map { entry -> 91 | ShareItem( 92 | id = entry.key, 93 | image = entry.value.image, 94 | title = entry.value.title, 95 | description = entry.value.description 96 | ) 97 | } 98 | .sortedBy { it.id } 99 | .toList() 100 | dataProvider.setData(shareItems) 101 | } 102 | 103 | private fun runPlugin(plugin: SharePlugin) { 104 | subscriptions += plugin.progress 105 | .throttleLast(PROGRESS_DEBOUNCE_DELAY, TimeUnit.MILLISECONDS) 106 | .doOnSubscribe { view?.resetOverlayProgress() } 107 | .observeOn(schedulers.mainThread()) 108 | .subscribe { view?.setOverlayProgress(it) } 109 | subscriptions += plugin.operation 110 | .subscribeOn(schedulers.io()) 111 | .observeOn(schedulers.mainThread()) 112 | .doOnSubscribe { view?.showOverlayProgress() } 113 | .doAfterTerminate { view?.showContent() } 114 | .subscribe( 115 | { router?.shareFile(it.file, it.mime) }, 116 | { onError() } 117 | ) 118 | } 119 | 120 | private fun onError() { 121 | } 122 | 123 | } 124 | 125 | private const val PROGRESS_DEBOUNCE_DELAY: Long = 500 126 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/share/ShareResult.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.share 2 | 3 | import java.io.File 4 | 5 | data class ShareResult( 6 | val file: File, 7 | val mime: String 8 | ) 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/share/ShareView.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.share 2 | 3 | import android.view.View 4 | import android.view.animation.Animation 5 | import android.view.animation.LinearInterpolator 6 | import android.view.animation.RotateAnimation 7 | import android.widget.Toast 8 | import android.widget.ViewFlipper 9 | import androidx.appcompat.widget.Toolbar 10 | import androidx.recyclerview.widget.LinearLayoutManager 11 | import androidx.recyclerview.widget.RecyclerView 12 | import com.jakewharton.rxrelay2.PublishRelay 13 | import com.tomclaw.drawa.R 14 | import com.tomclaw.drawa.util.CircleProgressView 15 | import com.tomclaw.drawa.util.hideWithAlphaAnimation 16 | import com.tomclaw.drawa.util.showWithAlphaAnimation 17 | import io.reactivex.Observable 18 | import java.util.concurrent.TimeUnit 19 | 20 | 21 | interface ShareView { 22 | 23 | fun showProgress() 24 | 25 | fun showOverlayProgress() 26 | 27 | fun resetOverlayProgress() 28 | 29 | fun setOverlayProgress(value: Float) 30 | 31 | fun showContent() 32 | 33 | fun showMessage(text: String) 34 | 35 | fun navigationClicks(): Observable 36 | 37 | fun itemClicks(): Observable 38 | 39 | } 40 | 41 | class ShareViewImpl( 42 | view: View, 43 | adapter: ShareAdapter 44 | ) : ShareView { 45 | 46 | private val context = view.context 47 | private val toolbar: Toolbar = view.findViewById(R.id.toolbar) 48 | private val overlayProgress: View = view.findViewById(R.id.overlay_progress) 49 | private val progress: CircleProgressView = view.findViewById(R.id.progress) 50 | private val flipper: ViewFlipper = view.findViewById(R.id.flipper) 51 | private val recycler: RecyclerView = view.findViewById(R.id.recycler) 52 | 53 | private val navigationRelay = PublishRelay.create() 54 | private val itemRelay = PublishRelay.create() 55 | 56 | init { 57 | toolbar.setTitle(R.string.share) 58 | toolbar.setNavigationOnClickListener { 59 | navigationRelay.accept(Unit) 60 | } 61 | val layoutManager = LinearLayoutManager( 62 | context, 63 | RecyclerView.VERTICAL, 64 | false 65 | ) 66 | adapter.setHasStableIds(true) 67 | adapter.itemRelay = itemRelay 68 | recycler.adapter = adapter 69 | recycler.layoutManager = layoutManager 70 | } 71 | 72 | override fun showProgress() { 73 | flipper.displayedChild = 0 74 | } 75 | 76 | override fun showOverlayProgress() { 77 | overlayProgress.showWithAlphaAnimation(animateFully = true) 78 | progress.animation = RotateAnimation( 79 | 0.0f, 80 | 360.0f, 81 | Animation.RELATIVE_TO_SELF, 82 | 0.5f, 83 | Animation.RELATIVE_TO_SELF, 84 | 0.5f).apply { 85 | duration = TimeUnit.SECONDS.toMillis(1) 86 | repeatCount = Animation.INFINITE 87 | repeatMode = Animation.RESTART 88 | fillAfter = true 89 | interpolator = LinearInterpolator() 90 | } 91 | } 92 | 93 | override fun resetOverlayProgress() { 94 | progress.progress = 0f 95 | } 96 | 97 | override fun setOverlayProgress(value: Float) { 98 | progress.setProgressWithAnimation(value, 500) 99 | } 100 | 101 | override fun showContent() { 102 | flipper.displayedChild = 1 103 | overlayProgress.hideWithAlphaAnimation( 104 | animateFully = false, 105 | endCallback = { progress.clearAnimation() } 106 | ) 107 | } 108 | 109 | override fun showMessage(text: String) { 110 | Toast.makeText(context, text, Toast.LENGTH_LONG).show() 111 | } 112 | 113 | override fun navigationClicks(): Observable = navigationRelay 114 | 115 | override fun itemClicks(): Observable = itemRelay 116 | 117 | } 118 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/share/di/ShareComponent.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.share.di 2 | 3 | import com.tomclaw.drawa.draw.di.ToolsModule 4 | import com.tomclaw.drawa.share.ShareActivity 5 | import com.tomclaw.drawa.util.PerActivity 6 | import dagger.Subcomponent 7 | 8 | @PerActivity 9 | @Subcomponent(modules = [ShareModule::class, ToolsModule::class]) 10 | interface ShareComponent { 11 | 12 | fun inject(activity: ShareActivity) 13 | 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/share/di/ShareModule.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.share.di 2 | 3 | import android.os.Bundle 4 | import com.tomclaw.cache.DiskLruCache 5 | import com.tomclaw.drawa.core.Journal 6 | import com.tomclaw.drawa.draw.DrawHost 7 | import com.tomclaw.drawa.draw.History 8 | import com.tomclaw.drawa.draw.HistoryImpl 9 | import com.tomclaw.drawa.draw.ImageProvider 10 | import com.tomclaw.drawa.draw.ToolProvider 11 | import com.tomclaw.drawa.share.DetachedDrawHost 12 | import com.tomclaw.drawa.share.ShareInteractor 13 | import com.tomclaw.drawa.share.ShareInteractorImpl 14 | import com.tomclaw.drawa.share.ShareItem 15 | import com.tomclaw.drawa.share.SharePlugin 16 | import com.tomclaw.drawa.share.SharePresenter 17 | import com.tomclaw.drawa.share.SharePresenterImpl 18 | import com.tomclaw.drawa.share.plugin.AnimSharePlugin 19 | import com.tomclaw.drawa.share.plugin.StaticSharePlugin 20 | import com.tomclaw.drawa.share.plugin.VideoSharePlugin 21 | import com.tomclaw.drawa.util.DataProvider 22 | import com.tomclaw.drawa.util.Logger 23 | import com.tomclaw.drawa.util.MetricsProvider 24 | import com.tomclaw.drawa.util.PerActivity 25 | import com.tomclaw.drawa.util.SchedulersFactory 26 | import dagger.Module 27 | import dagger.Provides 28 | import dagger.multibindings.IntoSet 29 | import java.io.File 30 | 31 | @Module 32 | class ShareModule( 33 | private val recordId: Int, 34 | private val presenterState: Bundle? 35 | ) { 36 | 37 | @Provides 38 | @PerActivity 39 | fun provideSharePresenter( 40 | interactor: ShareInteractor, 41 | dataProvider: DataProvider, 42 | sharePlugins: Set<@JvmSuppressWildcards SharePlugin>, 43 | schedulers: SchedulersFactory 44 | ): SharePresenter { 45 | return SharePresenterImpl( 46 | interactor, 47 | dataProvider, 48 | sharePlugins, 49 | schedulers, 50 | presenterState 51 | ) 52 | } 53 | 54 | @Provides 55 | @PerActivity 56 | fun provideShareInteractor( 57 | history: History, 58 | schedulers: SchedulersFactory 59 | ): ShareInteractor { 60 | return ShareInteractorImpl(history, schedulers) 61 | } 62 | 63 | @Provides 64 | @PerActivity 65 | fun provideHistory(filesDir: File, logger: Logger): History { 66 | return HistoryImpl(recordId, filesDir, logger) 67 | } 68 | 69 | @Provides 70 | @PerActivity 71 | fun provideShareItemDataProvider(): DataProvider { 72 | return DataProvider() 73 | } 74 | 75 | @Provides 76 | @IntoSet 77 | fun provideAnimSharePlugin( 78 | toolProvider: ToolProvider, 79 | metricsProvider: MetricsProvider, 80 | journal: Journal, 81 | history: History, 82 | drawHost: DrawHost, 83 | cache: DiskLruCache 84 | ): SharePlugin { 85 | return AnimSharePlugin( 86 | recordId, 87 | toolProvider, 88 | metricsProvider, 89 | journal, 90 | history, 91 | drawHost, 92 | cache 93 | ) 94 | } 95 | 96 | @Provides 97 | @IntoSet 98 | fun provideVideoSharePlugin( 99 | toolProvider: ToolProvider, 100 | metricsProvider: MetricsProvider, 101 | journal: Journal, 102 | history: History, 103 | drawHost: DrawHost, 104 | cache: DiskLruCache 105 | ): SharePlugin { 106 | return VideoSharePlugin( 107 | recordId, 108 | toolProvider, 109 | metricsProvider, 110 | journal, 111 | history, 112 | drawHost, 113 | cache 114 | ) 115 | } 116 | 117 | @Provides 118 | @IntoSet 119 | fun provideStaticSharePlugin( 120 | journal: Journal, 121 | imageProvider: ImageProvider, 122 | cache: DiskLruCache 123 | ): SharePlugin { 124 | return StaticSharePlugin(recordId, journal, imageProvider, cache) 125 | } 126 | 127 | @Provides 128 | @PerActivity 129 | fun provideDrawHost(): DrawHost { 130 | return DetachedDrawHost(SHARE_WIDTH, SHARE_HEIGHT) 131 | } 132 | 133 | } 134 | 135 | const val SHARE_WIDTH = 256 136 | const val SHARE_HEIGHT = 256 137 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/share/plugin/AnimSharePlugin.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.share.plugin 2 | 3 | import android.view.MotionEvent 4 | import com.jakewharton.rxrelay2.PublishRelay 5 | import com.tomclaw.cache.DiskLruCache 6 | import com.tomclaw.drawa.R 7 | import com.tomclaw.drawa.core.BITMAP_HEIGHT 8 | import com.tomclaw.drawa.core.BITMAP_WIDTH 9 | import com.tomclaw.drawa.core.Journal 10 | import com.tomclaw.drawa.draw.DrawHost 11 | import com.tomclaw.drawa.draw.Event 12 | import com.tomclaw.drawa.draw.History 13 | import com.tomclaw.drawa.draw.ToolProvider 14 | import com.tomclaw.drawa.gif.GifEncoder 15 | import com.tomclaw.drawa.share.SharePlugin 16 | import com.tomclaw.drawa.share.ShareResult 17 | import com.tomclaw.drawa.util.MetricsProvider 18 | import com.tomclaw.drawa.util.safeClose 19 | import com.tomclaw.drawa.util.uniqueKey 20 | import io.reactivex.Observable 21 | import io.reactivex.Single 22 | import java.io.File 23 | import java.io.FileOutputStream 24 | import java.io.OutputStream 25 | 26 | class AnimSharePlugin( 27 | private val recordId: Int, 28 | private val toolProvider: ToolProvider, 29 | private val metricsProvider: MetricsProvider, 30 | private val journal: Journal, 31 | private val history: History, 32 | private val drawHost: DrawHost, 33 | private val cache: DiskLruCache 34 | ) : SharePlugin { 35 | 36 | init { 37 | toolProvider.listTools().forEach { it.initialize(drawHost, metricsProvider) } 38 | } 39 | 40 | override val weight: Int 41 | get() = 2 42 | override val image: Int 43 | get() = R.drawable.animation 44 | override val title: Int 45 | get() = R.string.anim_share_title 46 | override val description: Int 47 | get() = R.string.anim_share_description 48 | 49 | override val progress: Observable 50 | get() = progressRelay 51 | private val progressRelay = PublishRelay.create() 52 | 53 | override val operation: Single = journal.load() 54 | .map { journal.get(recordId) } 55 | .flatMap { record -> 56 | val key = "anim-${record.uniqueKey()}" 57 | val cached = cache.get(key) 58 | val result = when { 59 | cached != null -> { 60 | updateProgress(value = 1f) 61 | Single.just(ShareResult(cached, MIME_TYPE)) 62 | } 63 | else -> Single.create { emitter -> 64 | val animFile: File = createTempFile("anim", ".gif") 65 | applyHistory(animFile) 66 | val file = cache.put(key, animFile) 67 | emitter.onSuccess(ShareResult(file, MIME_TYPE)) 68 | } 69 | } 70 | result 71 | } 72 | 73 | private fun applyHistory(file: File) { 74 | var stream: OutputStream? = null 75 | try { 76 | stream = FileOutputStream(file) 77 | val encoder = GifEncoder().apply { 78 | setQuality(15) 79 | start(stream) 80 | setRepeat(1) 81 | } 82 | drawHost.clearBitmap() 83 | val totalEventsCount = history.getEventsCount() 84 | var eventCount = 0 85 | history.getEvents().forEach { event -> 86 | processToolEvent(event) 87 | eventCount++ 88 | if ((eventCount % 10) == 0 || event.action == MotionEvent.ACTION_UP) { 89 | encoder.setDelay(100) 90 | encoder.addFrame(drawHost.bitmap) 91 | updateProgress(value = eventCount.toFloat() / totalEventsCount.toFloat()) 92 | } 93 | } 94 | encoder.finish() 95 | } finally { 96 | stream.safeClose() 97 | } 98 | } 99 | 100 | private fun processToolEvent(event: Event) { 101 | val tool = toolProvider.getTool(event.toolType) 102 | val x = (event.x * drawHost.bitmap.width / BITMAP_WIDTH) 103 | val y = (event.y * drawHost.bitmap.height / BITMAP_HEIGHT) 104 | with(tool) { 105 | when (event.action) { 106 | MotionEvent.ACTION_DOWN -> { 107 | color = event.color 108 | size = event.size 109 | onTouchDown(x, y) 110 | } 111 | MotionEvent.ACTION_MOVE -> onTouchMove(x, y) 112 | MotionEvent.ACTION_UP -> onTouchUp(x, y) 113 | } 114 | onDraw() 115 | } 116 | } 117 | 118 | private fun updateProgress(value: Float) { 119 | progressRelay.accept(value) 120 | } 121 | 122 | } 123 | 124 | private const val MIME_TYPE = "image/gif" 125 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/share/plugin/StaticSharePlugin.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.share.plugin 2 | 3 | import android.graphics.Bitmap 4 | import com.tomclaw.cache.DiskLruCache 5 | import com.tomclaw.drawa.R 6 | import com.tomclaw.drawa.core.Journal 7 | import com.tomclaw.drawa.draw.ImageProvider 8 | import com.tomclaw.drawa.share.SharePlugin 9 | import com.tomclaw.drawa.share.ShareResult 10 | import com.tomclaw.drawa.util.safeClose 11 | import com.tomclaw.drawa.util.uniqueKey 12 | import io.reactivex.Observable 13 | import io.reactivex.Single 14 | import java.io.File 15 | import java.io.FileOutputStream 16 | import java.io.OutputStream 17 | 18 | class StaticSharePlugin( 19 | recordId: Int, 20 | journal: Journal, 21 | imageProvider: ImageProvider, 22 | private val cache: DiskLruCache 23 | ) : SharePlugin { 24 | 25 | override val weight: Int 26 | get() = 1 27 | override val image: Int 28 | get() = R.drawable.image 29 | override val title: Int 30 | get() = R.string.static_share_title 31 | override val description: Int 32 | get() = R.string.static_share_description 33 | override val progress: Observable 34 | get() = Single.just(1f).toObservable() 35 | 36 | override val operation: Single = journal.load() 37 | .map { journal.get(recordId) } 38 | .flatMap { record -> 39 | val key = "static-${record.uniqueKey()}" 40 | val cached = cache.get(key) 41 | if (cached != null) { 42 | Single.just(ShareResult(cached, MIME_TYPE)) 43 | } else { 44 | imageProvider.readImage(recordId) 45 | .map { bitmap -> 46 | val imageFile: File = createTempFile("stat", ".png") 47 | var stream: OutputStream? = null 48 | try { 49 | stream = FileOutputStream(imageFile) 50 | bitmap.compress(Bitmap.CompressFormat.PNG, 90, stream) 51 | } finally { 52 | stream.safeClose() 53 | } 54 | val file = cache.put(key, imageFile) 55 | ShareResult(file, MIME_TYPE) 56 | } 57 | } 58 | } 59 | 60 | } 61 | 62 | private const val MIME_TYPE = "image/png" 63 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/share/plugin/VideoSharePlugin.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.share.plugin 2 | 3 | import android.view.MotionEvent 4 | import com.jakewharton.rxrelay2.PublishRelay 5 | import com.tomclaw.cache.DiskLruCache 6 | import com.tomclaw.drawa.R 7 | import com.tomclaw.drawa.core.BITMAP_HEIGHT 8 | import com.tomclaw.drawa.core.BITMAP_WIDTH 9 | import com.tomclaw.drawa.core.Journal 10 | import com.tomclaw.drawa.draw.DrawHost 11 | import com.tomclaw.drawa.draw.Event 12 | import com.tomclaw.drawa.draw.History 13 | import com.tomclaw.drawa.draw.ToolProvider 14 | import com.tomclaw.drawa.share.SharePlugin 15 | import com.tomclaw.drawa.share.ShareResult 16 | import com.tomclaw.drawa.util.MetricsProvider 17 | import com.tomclaw.drawa.util.uniqueKey 18 | import io.reactivex.Observable 19 | import io.reactivex.Single 20 | import org.jcodec.api.android.AndroidSequenceEncoder 21 | import java.io.File 22 | 23 | class VideoSharePlugin( 24 | private val recordId: Int, 25 | private val toolProvider: ToolProvider, 26 | private val metricsProvider: MetricsProvider, 27 | private val journal: Journal, 28 | private val history: History, 29 | private val drawHost: DrawHost, 30 | private val cache: DiskLruCache 31 | ) : SharePlugin { 32 | 33 | init { 34 | toolProvider.listTools().forEach { it.initialize(drawHost, metricsProvider) } 35 | } 36 | 37 | override val weight: Int 38 | get() = 3 39 | override val image: Int 40 | get() = R.drawable.videocam 41 | override val title: Int 42 | get() = R.string.video_share_title 43 | override val description: Int 44 | get() = R.string.video_share_description 45 | 46 | override val progress: Observable 47 | get() = progressRelay 48 | private val progressRelay = PublishRelay.create() 49 | 50 | override val operation: Single = journal.load() 51 | .map { journal.get(recordId) } 52 | .flatMap { record -> 53 | val key = "video-${record.uniqueKey()}" 54 | val cached = cache.get(key) 55 | val result = when { 56 | cached != null -> { 57 | updateProgress(value = 1f) 58 | Single.just(ShareResult(cached, MIME_TYPE)) 59 | } 60 | else -> Single.create { emitter -> 61 | val videoFile: File = createTempFile("video", ".mp4") 62 | applyHistory(videoFile) 63 | val file = cache.put(key, videoFile) 64 | emitter.onSuccess(ShareResult(file, MIME_TYPE)) 65 | } 66 | } 67 | result 68 | } 69 | 70 | private fun applyHistory(file: File) { 71 | var encoder: AndroidSequenceEncoder? = null 72 | try { 73 | encoder = AndroidSequenceEncoder.createSequenceEncoder(file, 10) 74 | drawHost.clearBitmap() 75 | val totalEventsCount = history.getEventsCount() 76 | var eventCount = 0 77 | history.getEvents().forEach { event -> 78 | processToolEvent(event) 79 | eventCount++ 80 | if ((eventCount % 10) == 0 || event.action == MotionEvent.ACTION_UP) { 81 | encoder?.encodeImage(drawHost.bitmap) 82 | updateProgress(value = eventCount.toFloat() / totalEventsCount.toFloat()) 83 | } 84 | } 85 | } finally { 86 | encoder?.finish() 87 | } 88 | } 89 | 90 | private fun processToolEvent(event: Event) { 91 | val tool = toolProvider.getTool(event.toolType) 92 | val x = (event.x * drawHost.bitmap.width / BITMAP_WIDTH) 93 | val y = (event.y * drawHost.bitmap.height / BITMAP_HEIGHT) 94 | with(tool) { 95 | when (event.action) { 96 | MotionEvent.ACTION_DOWN -> { 97 | color = event.color 98 | size = event.size 99 | onTouchDown(x, y) 100 | } 101 | MotionEvent.ACTION_MOVE -> onTouchMove(x, y) 102 | MotionEvent.ACTION_UP -> onTouchUp(x, y) 103 | } 104 | onDraw() 105 | } 106 | } 107 | 108 | private fun updateProgress(value: Float) { 109 | progressRelay.accept(value) 110 | } 111 | 112 | } 113 | 114 | private const val MIME_TYPE = "video/mp4" 115 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/stock/RecordConverter.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.stock 2 | 3 | import com.tomclaw.drawa.dto.Record 4 | import com.tomclaw.drawa.util.imageFile 5 | import java.io.File 6 | 7 | interface RecordConverter { 8 | 9 | fun convert(record: Record): StockItem 10 | 11 | } 12 | 13 | class RecordConverterImpl(private val filesDir: File) : RecordConverter { 14 | 15 | override fun convert(record: Record): StockItem { 16 | return StockItem( 17 | record.id, 18 | record.imageFile(filesDir).absolutePath, 19 | record.size.width, 20 | record.size.height 21 | ) 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/stock/StockActivity.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.stock 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import androidx.appcompat.app.AppCompatActivity 6 | import com.tomclaw.drawa.R 7 | import com.tomclaw.drawa.draw.createDrawActivityIntent 8 | import com.tomclaw.drawa.dto.Record 9 | import com.tomclaw.drawa.info.createInfoActivityIntent 10 | import com.tomclaw.drawa.main.getComponent 11 | import com.tomclaw.drawa.stock.di.StockModule 12 | import com.tomclaw.drawa.util.DataProvider 13 | import javax.inject.Inject 14 | 15 | class StockActivity : AppCompatActivity(), StockPresenter.StockRouter { 16 | 17 | @Inject 18 | lateinit var presenter: StockPresenter 19 | 20 | @Inject 21 | lateinit var dataProvider: DataProvider 22 | 23 | override fun onCreate(savedInstanceState: Bundle?) { 24 | val presenterState = savedInstanceState?.getBundle(KEY_PRESENTER_STATE) 25 | application.getComponent() 26 | .stockComponent(StockModule(this, presenterState)) 27 | .inject(activity = this) 28 | 29 | super.onCreate(savedInstanceState) 30 | setContentView(R.layout.stock) 31 | 32 | val adapter = StockAdapter(this, dataProvider) 33 | val view = StockViewImpl(window.decorView, adapter) 34 | 35 | presenter.attachView(view) 36 | } 37 | 38 | override fun onStart() { 39 | super.onStart() 40 | presenter.attachRouter(this) 41 | } 42 | 43 | override fun onStop() { 44 | presenter.detachRouter() 45 | super.onStop() 46 | } 47 | 48 | override fun onDestroy() { 49 | presenter.detachView() 50 | super.onDestroy() 51 | } 52 | 53 | override fun onSaveInstanceState(outState: Bundle) { 54 | super.onSaveInstanceState(outState) 55 | outState.putBundle(KEY_PRESENTER_STATE, presenter.saveState()) 56 | } 57 | 58 | @Deprecated("Deprecated in Java") 59 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 60 | when (requestCode) { 61 | REQUEST_DRAW -> { 62 | if (resultCode == RESULT_OK) { 63 | presenter.onUpdate() 64 | } 65 | } 66 | } 67 | super.onActivityResult(requestCode, resultCode, data) 68 | } 69 | 70 | override fun showDrawingScreen(record: Record) { 71 | val intent = createDrawActivityIntent(context = this, recordId = record.id) 72 | startActivityForResult(intent, REQUEST_DRAW) 73 | } 74 | 75 | override fun showInfoScreen() { 76 | val intent = createInfoActivityIntent(context = this) 77 | startActivity(intent) 78 | } 79 | 80 | } 81 | 82 | private const val KEY_PRESENTER_STATE = "presenter_state" 83 | private const val REQUEST_DRAW = 1 84 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/stock/StockAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.stock 2 | 3 | import android.content.Context 4 | import android.view.LayoutInflater 5 | import android.view.ViewGroup 6 | import androidx.recyclerview.widget.RecyclerView 7 | import com.jakewharton.rxrelay2.PublishRelay 8 | import com.tomclaw.drawa.R 9 | import com.tomclaw.drawa.util.DataProvider 10 | 11 | class StockAdapter( 12 | private val context: Context, 13 | private val dataProvider: DataProvider 14 | ) : RecyclerView.Adapter() { 15 | 16 | var itemsRelay: PublishRelay? = null 17 | 18 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StockItemHolder { 19 | val view = LayoutInflater.from(context).inflate(R.layout.stock_item_view, parent, false) 20 | return StockItemHolder(view, itemsRelay) 21 | } 22 | 23 | override fun onBindViewHolder(holder: StockItemHolder, position: Int) { 24 | val item = dataProvider.getItem(position) 25 | holder.bind(item) 26 | } 27 | 28 | override fun getItemId(position: Int): Long = dataProvider.getItem(position).id.toLong() 29 | 30 | override fun getItemCount(): Int = dataProvider.size() 31 | 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/stock/StockInteractor.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.stock 2 | 3 | import com.tomclaw.drawa.core.Journal 4 | import com.tomclaw.drawa.dto.Record 5 | import com.tomclaw.drawa.util.SchedulersFactory 6 | import io.reactivex.Observable 7 | 8 | interface StockInteractor { 9 | 10 | fun create(): Record 11 | 12 | fun isLoaded(): Boolean 13 | 14 | fun get(): List 15 | 16 | fun get(id: Int): Record? 17 | 18 | fun add(record: Record): List 19 | 20 | fun saveJournal(): Observable 21 | 22 | fun loadJournal(): Observable> 23 | 24 | } 25 | 26 | class StockInteractorImpl( 27 | private val journal: Journal, 28 | private val schedulers: SchedulersFactory 29 | ) : StockInteractor { 30 | 31 | override fun create() = journal.create() 32 | 33 | override fun isLoaded() = journal.isLoaded() 34 | 35 | override fun get() = journal.get() 36 | 37 | override fun get(id: Int): Record? = journal.get().find { it.id == id } 38 | 39 | override fun add(record: Record) = journal.add(record) 40 | 41 | override fun saveJournal(): Observable = 42 | journal.save() 43 | .toObservable() 44 | .subscribeOn(schedulers.io()) 45 | 46 | override fun loadJournal(): Observable> = 47 | journal.load() 48 | .toObservable() 49 | .subscribeOn(schedulers.io()) 50 | 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/stock/StockItem.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.stock 2 | 3 | import android.os.Parcel 4 | import android.os.Parcelable 5 | 6 | class StockItem( 7 | val id: Int, 8 | val image: String, 9 | val width: Int, 10 | val height: Int 11 | ) : Parcelable { 12 | 13 | override fun writeToParcel(dest: Parcel, flags: Int) = with(dest) { 14 | writeInt(id) 15 | writeString(image) 16 | writeInt(width) 17 | writeInt(height) 18 | } 19 | 20 | override fun describeContents(): Int = 0 21 | 22 | companion object CREATOR : Parcelable.Creator { 23 | override fun createFromParcel(parcel: Parcel): StockItem { 24 | val id = parcel.readInt() 25 | val image = parcel.readString().orEmpty() 26 | val width = parcel.readInt() 27 | val height = parcel.readInt() 28 | return StockItem(id, image, width, height) 29 | } 30 | 31 | override fun newArray(size: Int): Array { 32 | return arrayOfNulls(size) 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/stock/StockItemHolder.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.stock 2 | 3 | import android.view.View 4 | import androidx.cardview.widget.CardView 5 | import androidx.recyclerview.widget.RecyclerView 6 | import com.jakewharton.rxrelay2.PublishRelay 7 | import com.tomclaw.drawa.R 8 | import com.tomclaw.drawa.core.GlideApp 9 | import com.tomclaw.drawa.util.AspectRatioImageView 10 | 11 | class StockItemHolder( 12 | view: View, 13 | private val itemsRelay: PublishRelay? 14 | ) : RecyclerView.ViewHolder(view) { 15 | 16 | private val cardView: CardView = view.findViewById(R.id.card_view) 17 | private val imageView: AspectRatioImageView = view.findViewById(R.id.image_view) 18 | 19 | fun bind(item: StockItem) { 20 | val aspectRatio = item.height.toFloat() / item.width.toFloat() 21 | imageView.aspectRatio = aspectRatio 22 | 23 | cardView.setOnClickListener { 24 | itemsRelay?.accept(item) 25 | } 26 | 27 | GlideApp.with(imageView) 28 | .load(item.image) 29 | .centerCrop() 30 | .into(imageView) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/stock/StockPresenter.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.stock 2 | 3 | import android.os.Bundle 4 | import com.tomclaw.drawa.dto.Record 5 | import com.tomclaw.drawa.util.DataProvider 6 | import com.tomclaw.drawa.util.SchedulersFactory 7 | import io.reactivex.disposables.CompositeDisposable 8 | import io.reactivex.rxkotlin.plusAssign 9 | 10 | interface StockPresenter { 11 | 12 | fun attachView(view: StockView) 13 | 14 | fun detachView() 15 | 16 | fun attachRouter(router: StockRouter) 17 | 18 | fun detachRouter() 19 | 20 | fun saveState(): Bundle 21 | 22 | fun onUpdate() 23 | 24 | interface StockRouter { 25 | 26 | fun showDrawingScreen(record: Record) 27 | 28 | fun showInfoScreen() 29 | 30 | } 31 | 32 | } 33 | 34 | class StockPresenterImpl( 35 | private val interactor: StockInteractor, 36 | private val dataProvider: DataProvider, 37 | private val recordConverter: RecordConverter, 38 | private val schedulers: SchedulersFactory, 39 | state: Bundle? 40 | ) : StockPresenter { 41 | 42 | private var view: StockView? = null 43 | private var router: StockPresenter.StockRouter? = null 44 | 45 | private val subscriptions = CompositeDisposable() 46 | 47 | override fun attachView(view: StockView) { 48 | this.view = view 49 | 50 | subscriptions += view.itemClicks().subscribe { item -> 51 | interactor.get(item.id)?.let { record -> 52 | router?.showDrawingScreen(record) 53 | } 54 | } 55 | 56 | subscriptions += view.createClicks().subscribe { createStockItem() } 57 | subscriptions += view.infoClicks().subscribe { router?.showInfoScreen() } 58 | 59 | if (interactor.isLoaded()) { 60 | bindRecords(interactor.get()) 61 | } else { 62 | loadStockItems() 63 | } 64 | } 65 | 66 | private fun createStockItem() { 67 | val record = interactor.create() 68 | val records = interactor.add(record) 69 | subscriptions += interactor.saveJournal() 70 | .observeOn(schedulers.mainThread()) 71 | .doOnSubscribe { view?.showProgress() } 72 | .doAfterTerminate { view?.showContent() } 73 | .subscribe({ 74 | bindRecords(records) 75 | router?.showDrawingScreen(record) 76 | }, {}) 77 | } 78 | 79 | private fun loadStockItems() { 80 | subscriptions += interactor.loadJournal() 81 | .observeOn(schedulers.mainThread()) 82 | .doOnSubscribe { view?.showProgress() } 83 | .doAfterTerminate { view?.showContent() } 84 | .subscribe({ records -> 85 | bindRecords(records) 86 | }, {}) 87 | } 88 | 89 | private fun bindRecords(records: List) { 90 | val items = records 91 | .sortedBy { it.time } 92 | .reversed() 93 | .map { recordConverter.convert(it) } 94 | dataProvider.setData(items) 95 | view?.updateList() 96 | view?.showContent() 97 | } 98 | 99 | override fun detachView() { 100 | subscriptions.clear() 101 | this.view = null 102 | } 103 | 104 | override fun attachRouter(router: StockPresenter.StockRouter) { 105 | this.router = router 106 | } 107 | 108 | override fun detachRouter() { 109 | this.router = null 110 | } 111 | 112 | override fun saveState() = Bundle().apply {} 113 | 114 | override fun onUpdate() { 115 | loadStockItems() 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/stock/StockView.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.stock 2 | 3 | import android.view.View 4 | import android.widget.ViewFlipper 5 | import androidx.appcompat.widget.Toolbar 6 | import androidx.recyclerview.widget.RecyclerView 7 | import com.google.android.material.floatingactionbutton.FloatingActionButton 8 | import com.jakewharton.rxrelay2.PublishRelay 9 | import com.tomclaw.drawa.R 10 | import io.reactivex.Observable 11 | 12 | interface StockView { 13 | 14 | fun showProgress() 15 | 16 | fun showContent() 17 | 18 | fun updateList() 19 | 20 | fun itemClicks(): Observable 21 | 22 | fun createClicks(): Observable 23 | 24 | fun infoClicks(): Observable 25 | 26 | } 27 | 28 | class StockViewImpl( 29 | view: View, 30 | private val adapter: StockAdapter 31 | ) : StockView { 32 | 33 | private val context = view.context 34 | private val toolbar: Toolbar = view.findViewById(R.id.toolbar) 35 | private val createButton: FloatingActionButton = view.findViewById(R.id.create_button) 36 | private val flipper: ViewFlipper = view.findViewById(R.id.flipper) 37 | private val recycler: RecyclerView = view.findViewById(R.id.recycler) 38 | 39 | private val itemsRelay = PublishRelay.create() 40 | private val createRelay = PublishRelay.create() 41 | private val infoRelay = PublishRelay.create() 42 | 43 | init { 44 | val layoutManager = androidx.recyclerview.widget.GridLayoutManager( 45 | context, 46 | 2, 47 | RecyclerView.VERTICAL, 48 | false 49 | ) 50 | adapter.setHasStableIds(true) 51 | adapter.itemsRelay = itemsRelay 52 | recycler.adapter = adapter 53 | recycler.layoutManager = layoutManager 54 | 55 | toolbar.setTitle(R.string.stock) 56 | toolbar.inflateMenu(R.menu.stock) 57 | toolbar.setOnMenuItemClickListener { item -> 58 | when (item.itemId) { 59 | R.id.menu_info -> infoRelay.accept(Unit) 60 | } 61 | true 62 | } 63 | 64 | createButton.setOnClickListener { 65 | createRelay.accept(Unit) 66 | } 67 | } 68 | 69 | override fun showProgress() { 70 | flipper.displayedChild = 0 71 | } 72 | 73 | override fun showContent() { 74 | flipper.displayedChild = 1 75 | } 76 | 77 | override fun updateList() { 78 | adapter.notifyDataSetChanged() 79 | } 80 | 81 | override fun itemClicks(): Observable { 82 | return itemsRelay 83 | } 84 | 85 | override fun createClicks(): Observable { 86 | return createRelay 87 | } 88 | 89 | override fun infoClicks(): Observable { 90 | return infoRelay 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/stock/di/StockComponent.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.stock.di 2 | 3 | import com.tomclaw.drawa.stock.StockActivity 4 | import com.tomclaw.drawa.util.PerActivity 5 | import dagger.Subcomponent 6 | 7 | @PerActivity 8 | @Subcomponent(modules = [StockModule::class]) 9 | interface StockComponent { 10 | 11 | fun inject(activity: StockActivity) 12 | 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/stock/di/StockModule.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.stock.di 2 | 3 | import android.content.Context 4 | import android.os.Bundle 5 | import com.tomclaw.drawa.core.Journal 6 | import com.tomclaw.drawa.stock.RecordConverter 7 | import com.tomclaw.drawa.stock.RecordConverterImpl 8 | import com.tomclaw.drawa.stock.StockInteractor 9 | import com.tomclaw.drawa.stock.StockInteractorImpl 10 | import com.tomclaw.drawa.stock.StockItem 11 | import com.tomclaw.drawa.stock.StockPresenter 12 | import com.tomclaw.drawa.stock.StockPresenterImpl 13 | import com.tomclaw.drawa.util.DataProvider 14 | import com.tomclaw.drawa.util.PerActivity 15 | import com.tomclaw.drawa.util.SchedulersFactory 16 | import dagger.Module 17 | import dagger.Provides 18 | import java.io.File 19 | 20 | @Module 21 | class StockModule(private val context: Context, 22 | private val presenterState: Bundle?) { 23 | 24 | @Provides 25 | @PerActivity 26 | fun provideStockPresenter(interactor: StockInteractor, 27 | dataProvider: DataProvider, 28 | recordConverter: RecordConverter, 29 | schedulers: SchedulersFactory): StockPresenter { 30 | return StockPresenterImpl(interactor, dataProvider, recordConverter, schedulers, presenterState) 31 | } 32 | 33 | @Provides 34 | @PerActivity 35 | fun provideStockInteractor(journal: Journal, 36 | schedulers: SchedulersFactory): StockInteractor { 37 | return StockInteractorImpl(journal, schedulers) 38 | } 39 | 40 | @Provides 41 | @PerActivity 42 | fun provideStockItemDataProvider(): DataProvider { 43 | return DataProvider() 44 | } 45 | 46 | @Provides 47 | @PerActivity 48 | fun provideRecordConverter(filesDir: File): RecordConverter { 49 | return RecordConverterImpl(filesDir) 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/util/AspectRatioImageView.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.util 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import androidx.appcompat.widget.AppCompatImageView 6 | 7 | class AspectRatioImageView : AppCompatImageView { 8 | 9 | var aspectRatio = 1.0f 10 | 11 | constructor(context: Context) : super(context) 12 | 13 | constructor(context: Context, attrs: AttributeSet) : super(context, attrs) 14 | 15 | constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) 16 | 17 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 18 | super.onMeasure(widthMeasureSpec, heightMeasureSpec) 19 | val width = measuredWidth 20 | val height = (width.toFloat() * aspectRatio).toInt() 21 | setMeasuredDimension(width, height) 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/util/DataProvider.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.util 2 | 3 | import java.util.ArrayList 4 | import java.util.Collections.emptyList 5 | 6 | class DataProvider { 7 | 8 | private var data: List = emptyList() 9 | 10 | fun getItem(position: Int): A = data[position] 11 | 12 | fun size() = data.size 13 | 14 | fun setData(data: List) { 15 | this.data = ArrayList(data) 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/util/Logger.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.util 2 | 3 | import android.util.Log 4 | import com.tomclaw.drawa.core.LOG_TAG 5 | 6 | interface Logger { 7 | 8 | fun log(message: String) 9 | 10 | fun log(message: String, ex: Throwable) 11 | 12 | } 13 | 14 | class LoggerImpl : Logger { 15 | 16 | override fun log(message: String) { 17 | Log.d(LOG_TAG, message) 18 | } 19 | 20 | override fun log(message: String, ex: Throwable) { 21 | Log.d(LOG_TAG, message, ex) 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/util/MetricsProvider.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.util 2 | 3 | import android.content.Context 4 | import android.graphics.Rect 5 | import android.util.DisplayMetrics 6 | import android.view.WindowManager 7 | 8 | interface MetricsProvider { 9 | 10 | /** 11 | * This method converts dp unit to equivalent pixels, depending on device density. 12 | * 13 | * @param dp A value in dp (density independent pixels) unit. Which we need to convert into pixels 14 | * @return A float value to represent px equivalent to dp depending on device density 15 | */ 16 | fun convertDpToPixel(dp: Float): Float 17 | 18 | /** 19 | * This method converts device specific pixels to density independent pixels. 20 | * 21 | * @param px A value in px (pixels) unit. Which we need to convert into db 22 | * @return A float value to represent dp equivalent to px value 23 | */ 24 | fun convertPixelsToDp(px: Float): Float 25 | 26 | fun getScreenSize(): Rect 27 | 28 | } 29 | 30 | class MetricsProviderImpl(private val context: Context) : MetricsProvider { 31 | 32 | override fun convertDpToPixel(dp: Float): Float { 33 | val metrics = context.resources.displayMetrics 34 | return dp * (metrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT) 35 | } 36 | 37 | override fun convertPixelsToDp(px: Float): Float { 38 | val metrics = context.resources.displayMetrics 39 | return px / (metrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT) 40 | } 41 | 42 | override fun getScreenSize(): Rect { 43 | val size = Rect() 44 | val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager 45 | wm.defaultDisplay.getRectSize(size) 46 | return size 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/util/PerActivity.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.util 2 | 3 | import javax.inject.Scope 4 | 5 | @Scope 6 | @Retention(AnnotationRetention.RUNTIME) 7 | annotation class PerActivity 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/util/RecordNotFoundException.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.util 2 | 3 | class RecordNotFoundException : Exception() 4 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/util/Records.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.util 2 | 3 | import com.tomclaw.drawa.dto.Record 4 | import java.io.File 5 | 6 | fun recordName(recordId: Int): String = "draw-$recordId" 7 | 8 | fun Record.touch() { 9 | time = System.currentTimeMillis() 10 | } 11 | 12 | fun Record.imageFile(dir: File): File = File(dir, recordName(id) + "-" + time + ".png") 13 | 14 | fun Record.uniqueKey(): String { 15 | return "draw-$id-${size.width}-${size.height}-$time" 16 | } 17 | 18 | fun historyFile(recordId: Int, dir: File): File = File(dir, recordName(recordId) + ".bin") 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/util/RectF.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.util 2 | 3 | import android.graphics.RectF 4 | import android.os.Parcel 5 | import android.os.Parcelable 6 | 7 | class SizeF(var width: Float = 0.0f, var height: Float = 0.0f) : Parcelable { 8 | 9 | val widthInt: Int 10 | get() = width.toInt() 11 | 12 | val heightInt: Int 13 | get() = height.toInt() 14 | 15 | fun middle(size: SizeF) = RectF( 16 | (width - size.width) / 2, 17 | (height - size.height) / 2, 18 | (width - size.width) / 2, 19 | (height - size.height) / 2 20 | ) 21 | 22 | constructor(parcel: Parcel) : this( 23 | width = parcel.readFloat(), 24 | height = parcel.readFloat()) 25 | 26 | override fun writeToParcel(parcel: Parcel, flags: Int) { 27 | parcel.writeFloat(width) 28 | parcel.writeFloat(height) 29 | } 30 | 31 | override fun describeContents(): Int { 32 | return 0 33 | } 34 | 35 | companion object CREATOR : Parcelable.Creator { 36 | override fun createFromParcel(parcel: Parcel): SizeF { 37 | return SizeF(parcel) 38 | } 39 | 40 | override fun newArray(size: Int): Array { 41 | return arrayOfNulls(size) 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/util/SchedulersFactory.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.util 2 | 3 | import io.reactivex.Scheduler 4 | import io.reactivex.android.schedulers.AndroidSchedulers 5 | import io.reactivex.schedulers.Schedulers 6 | 7 | interface SchedulersFactory { 8 | 9 | fun io(): Scheduler 10 | 11 | fun single(): Scheduler 12 | 13 | fun trampoline(): Scheduler 14 | 15 | fun mainThread(): Scheduler 16 | 17 | } 18 | 19 | class SchedulersFactoryImpl : SchedulersFactory { 20 | 21 | override fun io(): Scheduler { 22 | return Schedulers.io() 23 | } 24 | 25 | override fun single(): Scheduler { 26 | return Schedulers.single() 27 | } 28 | 29 | override fun trampoline(): Scheduler { 30 | return Schedulers.trampoline() 31 | } 32 | 33 | override fun mainThread(): Scheduler { 34 | return AndroidSchedulers.mainThread() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/util/StreamDecoder.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.util 2 | 3 | interface StreamDecoder { 4 | 5 | fun getWidth(): Int 6 | 7 | fun getHeight(): Int 8 | 9 | fun hasFrame(): Boolean 10 | 11 | fun readFrame(): F? 12 | 13 | fun getDelay(): Int 14 | 15 | fun stop() 16 | 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/util/StreamRenderer.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.util 2 | 3 | interface StreamRenderer { 4 | 5 | fun render(frame: F) 6 | 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/util/Streams.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.util 2 | 3 | import java.io.Closeable 4 | import java.io.IOException 5 | 6 | fun Closeable?.safeClose() { 7 | try { 8 | this?.close() 9 | } catch (ignored: IOException) { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomclaw/drawa/util/Views.kt: -------------------------------------------------------------------------------- 1 | package com.tomclaw.drawa.util 2 | 3 | import android.animation.Animator 4 | import android.animation.AnimatorListenerAdapter 5 | import android.view.View 6 | import android.view.View.GONE 7 | import android.view.View.VISIBLE 8 | import android.view.ViewPropertyAnimator 9 | import android.view.animation.AccelerateDecelerateInterpolator 10 | 11 | fun View?.toggle() { 12 | if (this?.visibility == VISIBLE) hide() else show() 13 | } 14 | 15 | fun View?.isVisible(): Boolean = this?.visibility == VISIBLE 16 | 17 | fun View?.show() { 18 | this?.visibility = VISIBLE 19 | } 20 | 21 | fun View?.hide() { 22 | this?.visibility = GONE 23 | } 24 | 25 | fun View.showWithAlphaAnimation( 26 | duration: Long = ANIMATION_DURATION, 27 | animateFully: Boolean = true, 28 | endCallback: (() -> Unit)? = null 29 | ): ViewPropertyAnimator { 30 | if (animateFully) { 31 | alpha = 0.0f 32 | } 33 | show() 34 | return animate() 35 | .setDuration(duration) 36 | .alpha(1.0f) 37 | .setInterpolator(AccelerateDecelerateInterpolator()) 38 | .setListener(object : AnimatorListenerAdapter() { 39 | override fun onAnimationEnd(animation: Animator) { 40 | alpha = 1.0f 41 | show() 42 | endCallback?.invoke() 43 | } 44 | }) 45 | } 46 | 47 | fun View.hideWithAlphaAnimation( 48 | duration: Long = ANIMATION_DURATION, 49 | animateFully: Boolean = true, 50 | endCallback: (() -> Unit)? = null 51 | ): ViewPropertyAnimator { 52 | if (animateFully) { 53 | alpha = 1.0f 54 | } 55 | return animate() 56 | .setDuration(duration) 57 | .alpha(0.0f) 58 | .setInterpolator(AccelerateDecelerateInterpolator()) 59 | .setListener(object : AnimatorListenerAdapter() { 60 | override fun onAnimationEnd(animation: Animator) { 61 | hide() 62 | alpha = 1.0f 63 | endCallback?.invoke() 64 | } 65 | }) 66 | } 67 | 68 | fun View.showWithTranslationAnimation( 69 | height: Float 70 | ): ViewPropertyAnimator { 71 | translationY = height 72 | alpha = 0.0f 73 | show() 74 | return animate() 75 | .setDuration(ANIMATION_DURATION) 76 | .alpha(1.0f) 77 | .translationY(0f) 78 | .setInterpolator(AccelerateDecelerateInterpolator()) 79 | .setListener(object : AnimatorListenerAdapter() { 80 | override fun onAnimationEnd(animation: Animator) { 81 | translationY = 0f 82 | alpha = 1.0f 83 | show() 84 | } 85 | }) 86 | } 87 | 88 | fun View.moveWithTranslationAnimation( 89 | fromTranslationY: Float, 90 | tillTranslationY: Float, 91 | endCallback: () -> (Unit) 92 | 93 | ): ViewPropertyAnimator { 94 | translationY = fromTranslationY 95 | return animate() 96 | .setDuration(ANIMATION_DURATION) 97 | .translationY(tillTranslationY) 98 | .setInterpolator(AccelerateDecelerateInterpolator()) 99 | .setListener(object : AnimatorListenerAdapter() { 100 | override fun onAnimationEnd(animation: Animator) { 101 | translationY = 0f 102 | endCallback.invoke() 103 | } 104 | }) 105 | } 106 | 107 | fun View.hideWithTranslationAnimation( 108 | endCallback: () -> (Unit) 109 | ): ViewPropertyAnimator { 110 | alpha = 1.0f 111 | translationY = 0f 112 | val endTranslationY = height.toFloat() 113 | return animate() 114 | .setDuration(ANIMATION_DURATION) 115 | .alpha(0.0f) 116 | .translationY(endTranslationY) 117 | .setInterpolator(AccelerateDecelerateInterpolator()) 118 | .setListener(object : AnimatorListenerAdapter() { 119 | override fun onAnimationEnd(animation: Animator) { 120 | translationY = endTranslationY 121 | hide() 122 | alpha = 1.0f 123 | endCallback.invoke() 124 | } 125 | }) 126 | } 127 | 128 | const val ANIMATION_DURATION: Long = 250 129 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/doodle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solkin/drawa-android/a156fa2a3745a9167ae13d750dd4102adf754de2/app/src/main/res/drawable-xhdpi/doodle.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/doodle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solkin/drawa-android/a156fa2a3745a9167ae13d750dd4102adf754de2/app/src/main/res/drawable-xxxhdpi/doodle.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/animation.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/background_doodle.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/brush.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/circle.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/delete.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/duplicate.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/eraser.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/format_color_fill.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 8 | 14 | 20 | 26 | 32 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/image.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/info.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/lead_pencil.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/marker.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/play.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/plus.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/replay.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shadow_toolbar.xml: -------------------------------------------------------------------------------- 1 | 3 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shadow_top.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/share_variant.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/size_l.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/size_m.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/size_s.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/size_xl.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/size_xxl.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/spray.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/undo_variant.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/videocam.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/layout/choosers_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | 23 | 24 | 29 | 30 | 35 | 36 | 41 | 42 | 47 | 48 | 53 | 54 | 55 | 56 | 61 | 62 | 67 | 68 | 69 | 70 | 78 | 79 | 84 | 85 | 90 | 91 | 96 | 97 | 102 | 103 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /app/src/main/res/layout/controls_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/layout/draw.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 19 | 20 | 24 | 25 | 26 | 27 | 31 | 32 | 38 | 39 | 45 | 46 | 54 | 55 | 59 | 60 | 61 | 62 | 63 | 64 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /app/src/main/res/layout/info.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | 15 | 21 | 22 | 27 | 28 | 29 | 30 | 36 | 37 | 41 | 42 |