├── .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 | 
5 | 
6 |
7 | 
8 | 
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 |
52 |
53 |
64 |
65 |
66 |
67 |
77 |
78 |
87 |
88 |
100 |
101 |
102 |
103 |
104 |
105 |
112 |
113 |
114 |
115 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/play.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
16 |
17 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/progress_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/shadow_toolbar.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/share.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
14 |
18 |
19 |
24 |
25 |
26 |
27 |
36 |
37 |
45 |
46 |
47 |
48 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/share_item_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
14 |
15 |
20 |
21 |
27 |
28 |
41 |
42 |
43 |
44 |
52 |
53 |
54 |
55 |
62 |
63 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/stock.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
12 |
13 |
17 |
18 |
24 |
25 |
26 |
27 |
37 |
38 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/stock_item_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
25 |
26 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/tools_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
13 |
14 |
20 |
21 |
26 |
27 |
33 |
34 |
38 |
39 |
40 |
41 |
46 |
47 |
48 |
49 |
50 |
51 |
58 |
59 |
64 |
65 |
70 |
71 |
76 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/draw.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/play.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/stock.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solkin/drawa-android/a156fa2a3745a9167ae13d750dd4102adf754de2/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solkin/drawa-android/a156fa2a3745a9167ae13d750dd4102adf754de2/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solkin/drawa-android/a156fa2a3745a9167ae13d750dd4102adf754de2/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solkin/drawa-android/a156fa2a3745a9167ae13d750dd4102adf754de2/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solkin/drawa-android/a156fa2a3745a9167ae13d750dd4102adf754de2/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solkin/drawa-android/a156fa2a3745a9167ae13d750dd4102adf754de2/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solkin/drawa-android/a156fa2a3745a9167ae13d750dd4102adf754de2/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solkin/drawa-android/a156fa2a3745a9167ae13d750dd4102adf754de2/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solkin/drawa-android/a156fa2a3745a9167ae13d750dd4102adf754de2/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #dfac20
4 | #d87021
5 | #cd0219
6 | #5200be
7 | #1f4c86
8 | #42a220
9 | #4a3c35
10 | #ecd937
11 | #ce4b1d
12 | #cf1a48
13 | #8c00ba
14 | #2b96c3
15 | #a8c225
16 | #85766e
17 | #ffffff
18 | #040404
19 | #282828
20 | #595959
21 | #8b8b8b
22 | #c5c5c5
23 | #c0b2aa
24 |
25 |
26 | - @color/color1
27 | - @color/color2
28 | - @color/color3
29 | - @color/color4
30 | - @color/color5
31 | - @color/color6
32 | - @color/color7
33 | - @color/color8
34 | - @color/color9
35 | - @color/color10
36 | - @color/color11
37 | - @color/color12
38 | - @color/color13
39 | - @color/color14
40 | - @color/color15
41 | - @color/color16
42 | - @color/color17
43 | - @color/color18
44 | - @color/color19
45 | - @color/color20
46 | - @color/color21
47 |
48 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 60dp
4 | 22dp
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #EDEAE7
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Drawa
4 | Отменить
5 | Просмотр
6 | Удалить
7 | Дублировать
8 | Мои рисунки
9 | Рисунок
10 | Поделиться
11 | Повторить
12 | Выбрать
13 | Анимация
14 | Поделитесь анимацией процесса рисования изображения
15 | Видео
16 | Поделитесь видео процесса рисования изображения
17 | Картинка
18 | Поделитесь статической картинкой
19 | Информация
20 | TomClaw Software
21 | Оценить приложение
22 | Другие проекты
23 | Версия %1$s (%2$d)
24 | Главный автор и программист - Солкин Игорь Викторович.\nПо любым вопросам обращайтесь по почте\ninbox@tomclaw.com
25 |
26 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | #32a945
5 | #2c953d
6 | #EF6C00
7 | #E65100
8 | #F5F5F5
9 | #BDBDBD
10 | #EEEEEE
11 | #8a8a8a
12 | #baffffff
13 | #000000
14 | #cc000000
15 | #ffffff
16 | #fff2f3f2
17 | #ffe6e7e8
18 |
19 |
38 |
39 |
46 |
47 |
53 |
54 |
57 |
58 |
69 |
70 |
81 |
82 |
91 |
92 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/filepaths.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | buildscript {
3 | ext.kotlin_version = '2.0.21'
4 | repositories {
5 | mavenCentral()
6 | google()
7 | }
8 | dependencies {
9 | classpath 'com.android.tools.build:gradle:8.7.2'
10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
11 | classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version"
12 | }
13 | }
14 |
15 | allprojects {
16 | repositories {
17 | mavenCentral()
18 | mavenLocal()
19 | maven { url 'https://jitpack.io' }
20 | google()
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/circle.yml:
--------------------------------------------------------------------------------
1 | general:
2 | artifacts:
3 | - /home/ubuntu/appsend/app/build/outputs/apk/
4 |
5 | machine:
6 | environment:
7 | ANDROID_HOME: /usr/local/android-sdk-linux
8 | java:
9 | version: oraclejdk8
10 |
11 | dependencies:
12 | pre:
13 | - ANDROID_HOME=/usr/local/android-sdk-linux
14 | - (./gradlew dependencies || true)
15 |
16 | compile:
17 | override:
18 | - (./gradlew assemble):
19 | timeout: 360
20 |
--------------------------------------------------------------------------------
/gif-encoder/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/gif-encoder/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 |
3 | android {
4 | compileSdk 34
5 |
6 | defaultConfig {
7 | minSdkVersion 14
8 | targetSdkVersion 34
9 | }
10 |
11 | buildTypes {
12 | release {
13 | minifyEnabled true
14 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
15 | }
16 | }
17 | compileOptions {
18 | sourceCompatibility JavaVersion.VERSION_17
19 | targetCompatibility JavaVersion.VERSION_17
20 | }
21 | namespace 'com.tomclaw.drawa.gif'
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/gif-encoder/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/gif-encoder/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xms4096m -Xmx8192m -XX:MaxPermSize=2048m
2 | android.enableJetifier=true
3 | android.nonFinalResIds=false
4 | android.nonTransitiveRClass=false
5 | android.useAndroidX=true
6 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solkin/drawa-android/a156fa2a3745a9167ae13d750dd4102adf754de2/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Apr 12 01:38:06 MSK 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/graphics/doodle.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solkin/drawa-android/a156fa2a3745a9167ae13d750dd4102adf754de2/graphics/doodle.sketch
--------------------------------------------------------------------------------
/graphics/draw.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solkin/drawa-android/a156fa2a3745a9167ae13d750dd4102adf754de2/graphics/draw.png
--------------------------------------------------------------------------------
/graphics/icon-2.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solkin/drawa-android/a156fa2a3745a9167ae13d750dd4102adf754de2/graphics/icon-2.sketch
--------------------------------------------------------------------------------
/graphics/icon.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solkin/drawa-android/a156fa2a3745a9167ae13d750dd4102adf754de2/graphics/icon.sketch
--------------------------------------------------------------------------------
/graphics/main.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solkin/drawa-android/a156fa2a3745a9167ae13d750dd4102adf754de2/graphics/main.png
--------------------------------------------------------------------------------
/graphics/palette.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solkin/drawa-android/a156fa2a3745a9167ae13d750dd4102adf754de2/graphics/palette.png
--------------------------------------------------------------------------------
/graphics/share.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solkin/drawa-android/a156fa2a3745a9167ae13d750dd4102adf754de2/graphics/share.png
--------------------------------------------------------------------------------
/import-summary.txt:
--------------------------------------------------------------------------------
1 | ECLIPSE ANDROID PROJECT IMPORT SUMMARY
2 | ======================================
3 |
4 | Manifest Merging:
5 | -----------------
6 | Your project uses libraries that provide manifests, and your Eclipse
7 | project did not explicitly turn on manifest merging. In Android Gradle
8 | projects, manifests are always merged (meaning that contents from your
9 | libraries' manifests will be merged into the app manifest. If you had
10 | manually copied contents from library manifests into your app manifest
11 | you may need to remove these for the app to build correctly.
12 |
13 | Ignored Files:
14 | --------------
15 | The following files were *not* copied into the new Gradle project; you
16 | should evaluate whether these are still needed in your project and if
17 | so manually move them:
18 |
19 | * .gitignore
20 | * ant.properties
21 | * build.xml
22 | * proguard-project.txt
23 |
24 | Replaced Jars with Dependencies:
25 | --------------------------------
26 | The importer recognized the following .jar files as third party
27 | libraries and replaced them with Gradle dependencies instead. This has
28 | the advantage that more explicit version information is known, and the
29 | libraries can be updated automatically. However, it is possible that
30 | the .jar file in your project was of an older version than the
31 | dependency we picked, which could render the project not compileable.
32 | You can disable the jar replacement in the import wizard and try again:
33 |
34 | android-support-v4.jar => com.android.support:support-v4:22.2.1
35 | android-support-v7-appcompat.jar => com.android.support:appcompat-v7:22.2.1
36 |
37 | Replaced Libraries with Dependencies:
38 | -------------------------------------
39 | The importer recognized the following library projects as third party
40 | libraries and replaced them with Gradle dependencies instead. This has
41 | the advantage that more explicit version information is known, and the
42 | libraries can be updated automatically. However, it is possible that
43 | the source files in your project were of an older version than the
44 | dependency we picked, which could render the project not compileable.
45 | You can disable the library replacement in the import wizard and try
46 | again:
47 |
48 | android-support-v7-appcompat => [com.android.support:appcompat-v7:22.2.1]
49 |
50 | Moved Files:
51 | ------------
52 | Android Gradle projects use a different directory structure than ADT
53 | Eclipse projects. Here's how the projects were restructured:
54 |
55 | * AndroidManifest.xml => app/src/main/AndroidManifest.xml
56 | * res/ => app/src/main/res/
57 | * src/ => app/src/main/java/
58 |
59 | Next Steps:
60 | -----------
61 | You can now build the project. The Gradle project needs network
62 | connectivity to download dependencies.
63 |
64 | Bugs:
65 | -----
66 | If for some reason your project does not build, and you determine that
67 | it is due to a bug or limitation of the Eclipse to Gradle importer,
68 | please file a bug at http://b.android.com with category
69 | Component-Tools.
70 |
71 | (This import summary is for your information only, and can be deleted
72 | after import once you are satisfied with the results.)
73 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':gif-encoder'
2 |
--------------------------------------------------------------------------------
/tomclaw.keystore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solkin/drawa-android/a156fa2a3745a9167ae13d750dd4102adf754de2/tomclaw.keystore
--------------------------------------------------------------------------------