├── .gitignore
├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── redhoodhan
│ │ └── drawing
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── redhoodhan
│ │ │ └── drawing
│ │ │ ├── ui
│ │ │ ├── common
│ │ │ │ ├── DrawCircleView.kt
│ │ │ │ └── StateImageButton.kt
│ │ │ └── draw
│ │ │ │ ├── DrawActivity.kt
│ │ │ │ ├── DrawBackgroundFragment.kt
│ │ │ │ ├── DrawEraserFragment.kt
│ │ │ │ ├── DrawOptionFragment.kt
│ │ │ │ ├── DrawRepository.kt
│ │ │ │ ├── DrawViewModel.kt
│ │ │ │ └── adapter
│ │ │ │ ├── DrawColorResAdapter.kt
│ │ │ │ ├── DrawImgResAdapter.kt
│ │ │ │ └── DrawPagerAdapter.kt
│ │ │ └── util
│ │ │ ├── AnimationUtil.kt
│ │ │ └── DisplayUtil.kt
│ └── res
│ │ ├── drawable-hdpi
│ │ ├── ic_launcher_background.xml
│ │ ├── icon_background.png
│ │ ├── icon_background_night.png
│ │ ├── icon_chisel_tip.png
│ │ ├── icon_dashed_line.png
│ │ ├── icon_dashed_line_night.png
│ │ ├── icon_eraser.png
│ │ ├── icon_eraser_night.png
│ │ ├── icon_paint.png
│ │ ├── icon_paint_dashed.png
│ │ ├── icon_paint_night.png
│ │ ├── icon_paint_solid.png
│ │ ├── icon_palette.png
│ │ ├── icon_palette_night.png
│ │ ├── icon_redo.png
│ │ ├── icon_solid_line.png
│ │ ├── icon_solid_line_night.png
│ │ └── icon_undo.png
│ │ ├── drawable-xhdpi
│ │ ├── ic_launcher_background.xml
│ │ ├── icon_background.png
│ │ ├── icon_background_night.png
│ │ ├── icon_chisel_tip.png
│ │ ├── icon_dashed_line.png
│ │ ├── icon_dashed_line_night.png
│ │ ├── icon_eraser.png
│ │ ├── icon_eraser_night.png
│ │ ├── icon_paint.png
│ │ ├── icon_paint_dashed.png
│ │ ├── icon_paint_night.png
│ │ ├── icon_paint_solid.png
│ │ ├── icon_palette.png
│ │ ├── icon_palette_night.png
│ │ ├── icon_redo.png
│ │ ├── icon_solid_line.png
│ │ ├── icon_solid_line_night.png
│ │ └── icon_undo.png
│ │ ├── drawable-xxhdpi
│ │ ├── ic_launcher_background.xml
│ │ ├── icon_background.png
│ │ ├── icon_background_night.png
│ │ ├── icon_chisel_tip.png
│ │ ├── icon_dashed_line.png
│ │ ├── icon_dashed_line_night.png
│ │ ├── icon_eraser.png
│ │ ├── icon_eraser_night.png
│ │ ├── icon_paint.png
│ │ ├── icon_paint_dashed.png
│ │ ├── icon_paint_night.png
│ │ ├── icon_paint_solid.png
│ │ ├── icon_palette.png
│ │ ├── icon_palette_night.png
│ │ ├── icon_redo.png
│ │ ├── icon_solid_line.png
│ │ ├── icon_solid_line_night.png
│ │ └── icon_undo.png
│ │ ├── drawable-xxxhdpi
│ │ ├── ic_launcher_background.xml
│ │ ├── icon_background.png
│ │ ├── icon_background_night.png
│ │ ├── icon_chisel_tip.png
│ │ ├── icon_dashed_line.png
│ │ ├── icon_dashed_line_night.png
│ │ ├── icon_eraser.png
│ │ ├── icon_eraser_night.png
│ │ ├── icon_paint.png
│ │ ├── icon_paint_dashed.png
│ │ ├── icon_paint_night.png
│ │ ├── icon_paint_solid.png
│ │ ├── icon_palette.png
│ │ ├── icon_palette_night.png
│ │ ├── icon_redo.png
│ │ ├── icon_solid_line.png
│ │ ├── icon_solid_line_night.png
│ │ └── icon_undo.png
│ │ ├── drawable
│ │ ├── img_draw_background_1.webp
│ │ ├── img_draw_background_2.webp
│ │ └── shape_pure_color_circle.xml
│ │ ├── layout
│ │ ├── activity_main.xml
│ │ ├── fragment_draw_background.xml
│ │ ├── fragment_draw_eraser.xml
│ │ ├── fragment_draw_option.xml
│ │ ├── item_bg_img_res.xml
│ │ └── item_draw_res.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── values-night
│ │ └── themes.xml
│ │ └── values
│ │ ├── arrays.xml
│ │ ├── attr.xml
│ │ ├── colors.xml
│ │ ├── dimens.xml
│ │ ├── strings.xml
│ │ ├── styles.xml
│ │ └── themes.xml
│ └── test
│ └── java
│ └── com
│ └── redhoodhan
│ └── drawing
│ └── ExampleUnitTest.kt
├── build.gradle
├── draw
├── .gitignore
├── build.gradle
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── redhoodhan
│ │ └── draw
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── redhoodhan
│ │ │ └── draw
│ │ │ ├── DrawView.kt
│ │ │ ├── DrawViewState.kt
│ │ │ ├── draw_option
│ │ │ ├── BaseDrawOptionStrategy.kt
│ │ │ ├── ChiselTipLineStrategy.kt
│ │ │ ├── DashedLineStrategy.kt
│ │ │ ├── DrawOptionContext.kt
│ │ │ ├── DrawOptionStrategy.kt
│ │ │ ├── EraserLineStrategy.kt
│ │ │ └── data
│ │ │ │ ├── BrushType.kt
│ │ │ │ ├── DrawConst.kt
│ │ │ │ ├── DrawPath.kt
│ │ │ │ ├── LineType.kt
│ │ │ │ └── PaintOption.kt
│ │ │ ├── extension
│ │ │ └── CanvasExtension.kt
│ │ │ └── path_effect
│ │ │ └── LineDashPathEffect.kt
│ └── res
│ │ └── values
│ │ └── attr.xml
│ └── test
│ └── java
│ └── com
│ └── redhoodhan
│ └── draw
│ └── ExampleUnitTest.kt
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── jitpack.yml
├── screenshots
└── demo_with_canvas_background.gif
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.aar
4 | *.ap_
5 | *.aab
6 |
7 | # Files for the ART/Dalvik VM
8 | *.dex
9 |
10 | # Java class files
11 | *.class
12 |
13 | # Generated files
14 | bin/
15 | gen/
16 | out/
17 | # Uncomment the following line in case you need and you don't have the release build type files in your app
18 | # release/
19 |
20 | # Gradle files
21 | .gradle/
22 | build/
23 |
24 | # Local configuration file (sdk path, etc)
25 | local.properties
26 |
27 | # Proguard folder generated by Eclipse
28 | proguard/
29 |
30 | # Log Files
31 | *.log
32 |
33 | # Android Studio Navigation editor temp files
34 | .navigation/
35 |
36 | # Android Studio captures folder
37 | captures/
38 |
39 | # IntelliJ
40 | .idea/
41 | *.iml
42 | .idea/workspace.xml
43 | .idea/tasks.xml
44 | .idea/gradle.xml
45 | .idea/assetWizardSettings.xml
46 | .idea/dictionaries
47 | .idea/libraries
48 | # Android Studio 3 in .gitignore file.
49 | .idea/caches
50 | .idea/modules.xml
51 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you
52 | .idea/navEditor.xml
53 |
54 | # Keystore files
55 | # Uncomment the following lines if you do not want to check your keystore files in.
56 | #*.jks
57 | #*.keystore
58 |
59 | # External native build folder generated in Android Studio 2.2 and later
60 | .externalNativeBuild
61 | .cxx/
62 |
63 | # Google Services (e.g. APIs or Firebase)
64 | # google-services.json
65 |
66 | # Freeline
67 | freeline.py
68 | freeline/
69 | freeline_project_description.json
70 |
71 | # fastlane
72 | fastlane/report.xml
73 | fastlane/Preview.html
74 | fastlane/screenshots
75 | fastlane/test_output
76 | fastlane/readme.md
77 |
78 | # Version control
79 | vcs.xml
80 |
81 | # lint
82 | lint/intermediates/
83 | lint/generated/
84 | lint/outputs/
85 | lint/tmp/
86 | # lint/reports/
87 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Draw [](https://jitpack.io/#DonghanX/DrawView)
2 |
3 | A simple Android view for drawing.
4 |
5 |
6 |
7 | ## Installation
8 |
9 | #### Step 1. Add the JitPack repository to your build file
10 |
11 | If you are using Gradle 6.8 or higher version, add it in `setting.gradle` at the end of repositories:
12 | ```gradle
13 | dependencyResolutionManagement {
14 | repositories {
15 | maven { url 'https://jitpack.io' }
16 | }
17 | }
18 | ```
19 |
20 | If not, add it in your root `build.gradle` at the end of repositories:
21 | ```gradle
22 | allprojects {
23 | repositories {
24 | maven { url 'https://jitpack.io' }
25 | }
26 | }
27 | ```
28 |
29 | #### Step 2. Add the dependency
30 | ```gradle
31 | dependencies {
32 | implementation 'com.github.DonghanX:Draw:v1.0.3'
33 | }
34 | ```
35 |
36 | ## Features
37 | * Support multiple types of lines, including solid line, dashed line and Chisel Tip line.
38 | * Eraser, Redo, Undo and ClearAll.
39 | * Change the background of the Canvas by setting image resource or color.
40 | * Keep track of the state after performing drawing actions.
41 | * Save the Canvas as Bitmap.
42 |
43 | ## Usage
44 |
45 | ### Setup:
46 |
47 | #### Include `DrawView` in your layout.xml directly
48 | ```XML
49 |
53 | ```
54 |
55 | #### (Alternatively) Include `DrawView` in your layout.xml with custom attributes setting
56 | ```XML
57 |
65 | ```
66 |
67 | | Attribute Name | Format | Description |
68 | | :----: | :----: | :----: |
69 | | defaultBrushSize | Float | The default value of brush size |
70 | | defaultEraserSize | Float | The default value of eraser size |
71 | | defaultBrushColor | Color | The default stroke color of brush paint |
72 | | defaultCanvasBackgroundColor | Color | The default color of canvas background |
73 | | defaultCanvasBackgroundImageRes | Reference | The default image resource ID of canvas background|
74 | > Note that setting `defaultCanvasBackgroundImageRes` will override the effect of `defaultCanvasBackgroundColor`.
75 |
76 | ### Further Usage:
77 |
78 | #### (Optional) Implement the callbacks
79 | * `drawViewPressCallback`: Invoked when you press the `DrawView`.
80 | * `undoStateCallback` / `redoStateCallback`: Invoked when the content in the `DrawView` changes. The given receiver parameter returns true if Undo / Redo action is available after the change.
81 | ```Kotlin
82 | binding.drawView.apply {
83 | drawViewPressCallback = {
84 | // Do your stuff here
85 | }
86 |
87 | undoStateCallback = { isUndoAvailable ->
88 | // Do your stuff here
89 | }
90 |
91 | redoStateCallback = { isRedoAvailable ->
92 | // Do your stuff here
93 | }
94 | }
95 | ```
96 |
97 | #### Switch the type of lines by setting `lineType` variable in the `DrawView`
98 | ```Kotlin
99 | binding.drawView.lineType = LineType.SOLID
100 | ```
101 | `LineType` is utilized by `DrawView` to modify the path properties and paint options, such as `StrokeStyle`, `Alpha` and `PathEffect`.
102 |
103 | | LineType | Description |
104 | | :----: | :----: |
105 | | LineType.SOLID | Solid line |
106 | | LineType.DASH | Dashed line with gap interval that is calculated by `brushSize` |
107 | | LineType.CHISEL | Chisel Tip line with alpha channel set to a specific value |
108 | | LineType.ERASER | Solid line with `Xfermode` set to `PorterDuff.Mode.CLEAR` |
109 |
110 | #### Undo
111 | ```Kotlin
112 | binding.drawView.undo()
113 | ```
114 |
115 | #### Redo
116 | ```Kotlin
117 | binding.drawView.redo()
118 | ```
119 |
120 | #### ClearAll
121 | ```Kotlin
122 | binding.drawView.clearCanvas(needsSaving = true)
123 | ```
124 | If `needsSaving` is true, then this ClearAll action is capable of being redone.
125 |
126 | #### Set color background
127 | ```kotlin
128 | binding.drawView.canvasBackgroundColor = Color.BLACK
129 | ```
130 | or
131 | ```kotlin
132 | binding.drawView.canvasBackgroundColor = ResourcesCompat.getColor(resources, yourColorResId, null)
133 | ```
134 | > Note that `canvasBackgroundColor` receives a single color value in the form 0xAARRGGBB.
135 |
136 | #### Set image resource background
137 | ```kotlin
138 | binding.drawView.canvasBackgroundImg = yourImageResId
139 | ```
140 |
141 | #### Change brush color
142 | ```kotlin
143 | binding.drawView.brushColor = Color.BLACK
144 | ```
145 | or
146 | ```kotlin
147 | binding.drawView.brushColor = ResourcesCompat.getColor(resources, yourcolorResId, null)
148 | ```
149 | > Note that `brushColor` receives a single color value in the form 0xAARRGGBB.
150 |
151 | #### Change brush size
152 | ```kotlin
153 | binding.drawView.brushSize = 10F
154 | ```
155 | > Note that `brushSize` receives a single float value.
156 | > Also note that the minimum stroke width is set to 3F.
157 |
158 | #### Change eraser size
159 | ```kotlin
160 | binding.drawView.eraserSize = 10F
161 | ```
162 | > Note that `eraserSize` receives a single float value.
163 | > Also note that the minimum stroke width is set to 3F.
164 |
165 | #### Save the canvas as Bitmap
166 | ```kotlin
167 | val bitmap = binding.drawView.saveAsBitmap()
168 | ```
169 |
170 | ## Thanks
171 | * Inspired by `DrawingView` in [mvojtkovszky](https://github.com/mvojtkovszky).
172 |
173 | ## License
174 | ```
175 | Copyright 2022 Donghan X
176 |
177 | Licensed under the Apache License, Version 2.0 (the "License");
178 | you may not use this file except in compliance with the License.
179 | You may obtain a copy of the License at
180 |
181 | http://www.apache.org/licenses/LICENSE-2.0
182 |
183 | Unless required by applicable law or agreed to in writing, software
184 | distributed under the License is distributed on an "AS IS" BASIS,
185 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
186 | See the License for the specific language governing permissions and
187 | limitations under the License.
188 | ```
189 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'kotlin-android'
4 | }
5 |
6 | android {
7 |
8 | buildFeatures {
9 | viewBinding true
10 | }
11 |
12 | compileSdk 31
13 |
14 | defaultConfig {
15 | applicationId "com.redhoodhan.drawing"
16 | minSdk 21
17 | targetSdk 31
18 | versionCode 1
19 | versionName "1.0"
20 |
21 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
22 | }
23 |
24 | buildTypes {
25 | release {
26 | minifyEnabled false
27 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
28 | }
29 | }
30 | compileOptions {
31 | sourceCompatibility JavaVersion.VERSION_1_8
32 | targetCompatibility JavaVersion.VERSION_1_8
33 | }
34 | kotlinOptions {
35 | jvmTarget = '1.8'
36 | }
37 | }
38 |
39 | dependencies {
40 |
41 | implementation 'androidx.core:core-ktx:1.7.0'
42 | implementation 'androidx.appcompat:appcompat:1.4.1'
43 | implementation 'com.google.android.material:material:1.5.0'
44 | implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
45 | testImplementation 'junit:junit:4.+'
46 | androidTestImplementation 'androidx.test.ext:junit:1.1.3'
47 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
48 |
49 | // KTX
50 | def activity_version = "1.4.0"
51 | implementation "androidx.activity:activity-ktx:$activity_version"
52 |
53 | def fragment_version = "1.4.1"
54 | implementation "androidx.fragment:fragment-ktx:$fragment_version"
55 |
56 | // ViewModel related libs
57 | def lifecycle_version = "2.5.0-rc01"
58 |
59 | // ViewModel
60 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
61 | // LiveData
62 | implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
63 |
64 | // Saved state module for ViewModel
65 | implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version"
66 |
67 | // alternately - if using Java8, use the following instead of lifecycle-compiler
68 | implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
69 |
70 | // ViewPager2
71 | implementation "androidx.viewpager2:viewpager2:1.0.0"
72 |
73 | // Draw
74 | implementation project(":draw")
75 |
76 | }
--------------------------------------------------------------------------------
/app/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
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/redhoodhan/drawing/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.redhoodhan.drawing
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.redhoodhan.drawing", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
12 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/java/com/redhoodhan/drawing/ui/common/DrawCircleView.kt:
--------------------------------------------------------------------------------
1 | package com.redhoodhan.drawing.ui.common
2 |
3 | import android.content.Context
4 | import android.graphics.Canvas
5 | import android.graphics.Color
6 | import android.graphics.Paint
7 | import android.util.AttributeSet
8 | import android.view.View
9 | import androidx.core.content.res.ResourcesCompat
10 |
11 | /**
12 | * TODO: Add click animation (expand)
13 | */
14 | private const val DEFAULT_RADIUS_FACTOR = 0.35F
15 | private const val SELECTED_RADIUS_FACTOR = 0.5F
16 | private const val DEFAULT_STROKE_WIDTH_FACTOR = 0.1F
17 |
18 | class DrawCircleView @JvmOverloads constructor(
19 | context: Context,
20 | attributeSet: AttributeSet?,
21 | defStyle: Int = 0
22 | ) : View(context, attributeSet, defStyle) {
23 |
24 | private var mPaint = Paint()
25 |
26 | private var mSelectPaint = Paint()
27 |
28 | private var drawRadiusFactor: Float = DEFAULT_RADIUS_FACTOR
29 |
30 | var isCurSelected: Boolean = false
31 | set(value) {
32 | if (field == value) {
33 | return
34 | }
35 | field = value
36 |
37 | changeCircleRadius(value)
38 | }
39 |
40 | var drawColorResId: Int = Color.BLACK
41 | set(value) {
42 | if (field == value) {
43 | return
44 | }
45 | field = value
46 |
47 | changePaintColor(value)
48 | }
49 |
50 | private fun changePaintColor(colorResId: Int) {
51 | ResourcesCompat.getColor(resources, colorResId, null).let {
52 | mPaint.color = it
53 |
54 | mSelectPaint.color = it
55 | }
56 | invalidate()
57 | }
58 |
59 | private fun changeCircleRadius(isCurSelected: Boolean) {
60 | drawRadiusFactor = if (isCurSelected) {
61 | SELECTED_RADIUS_FACTOR
62 | } else {
63 | DEFAULT_RADIUS_FACTOR
64 | }
65 | invalidate()
66 | }
67 |
68 | init {
69 | mPaint.apply {
70 | style = Paint.Style.FILL
71 | isAntiAlias = true
72 | color = Color.BLACK
73 | }
74 | }
75 |
76 | override fun onDraw(canvas: Canvas) {
77 | canvas.drawCircle(
78 | (width / 2).toFloat(),
79 | (height / 2).toFloat(),
80 | width * drawRadiusFactor,
81 | mPaint
82 | )
83 | super.onDraw(canvas)
84 | }
85 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/redhoodhan/drawing/ui/common/StateImageButton.kt:
--------------------------------------------------------------------------------
1 | package com.redhoodhan.drawing.ui.common
2 |
3 | import android.content.Context
4 | import android.graphics.Color
5 | import android.util.AttributeSet
6 | import android.util.Log
7 | import androidx.appcompat.widget.AppCompatImageButton
8 | import androidx.core.content.res.ResourcesCompat
9 | import androidx.core.graphics.BlendModeColorFilterCompat
10 | import androidx.core.graphics.BlendModeCompat
11 | import com.redhoodhan.drawing.R
12 |
13 | private const val TAG = "StateImageButton"
14 |
15 | class StateImageButton @JvmOverloads constructor(
16 | context: Context,
17 | attributeSet: AttributeSet?,
18 | defStyle: Int = 0,
19 | ) : AppCompatImageButton(context, attributeSet, defStyle) {
20 |
21 | private var originalColorResId: Int = R.color.black
22 | private var clickedColorResId: Int = R.color.purple_500
23 |
24 | private val originalColor: Int by lazy {
25 | retrieveColorFromRes(originalColorResId)
26 | }
27 | private val clickedColor: Int by lazy {
28 | retrieveColorFromRes(clickedColorResId)
29 | }
30 |
31 | var isClicked: Boolean = false
32 | set(value) {
33 | if (field == value) {
34 | return
35 | }
36 | field = value
37 | switchImageState(value)
38 | }
39 |
40 | init {
41 | scaleType = ScaleType.CENTER
42 |
43 | context.theme.obtainStyledAttributes(attributeSet, R.styleable.StateImageButton, 0, 0)
44 | .apply {
45 | try {
46 | originalColorResId =
47 | getResourceId(R.styleable.StateImageButton_originalColor, R.color.black)
48 | clickedColorResId =
49 | getResourceId(
50 | R.styleable.StateImageButton_selectedColor,
51 | R.color.purple_500
52 | )
53 | } finally {
54 | recycle()
55 | }
56 | }
57 |
58 | switchImageState(false)
59 | }
60 |
61 | private fun retrieveColorFromRes(colorResId: Int): Int =
62 | ResourcesCompat.getColor(resources, colorResId, null)
63 |
64 | private fun switchImageState(isClicked: Boolean) {
65 | drawable.colorFilter = if (isClicked) {
66 | blendWithColorFilter(clickedColor)
67 | } else {
68 | blendWithColorFilter(originalColor)
69 | }
70 | }
71 |
72 | private fun blendWithColorFilter(
73 | color: Int,
74 | blendMode: BlendModeCompat = BlendModeCompat.SRC_ATOP
75 | ) =
76 | BlendModeColorFilterCompat.createBlendModeColorFilterCompat(
77 | color,
78 | blendMode
79 | )
80 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/redhoodhan/drawing/ui/draw/DrawActivity.kt:
--------------------------------------------------------------------------------
1 | package com.redhoodhan.drawing.ui.draw
2 |
3 | import android.os.Bundle
4 | import android.view.View
5 | import androidx.activity.viewModels
6 | import androidx.appcompat.app.AppCompatActivity
7 | import androidx.core.content.res.ResourcesCompat
8 | import androidx.viewpager2.widget.ViewPager2
9 | import com.redhoodhan.draw.DrawView
10 | import com.redhoodhan.draw.draw_option.data.LineType
11 | import com.redhoodhan.drawing.R
12 | import com.redhoodhan.drawing.databinding.ActivityMainBinding
13 | import com.redhoodhan.drawing.ui.common.StateImageButton
14 | import com.redhoodhan.drawing.ui.draw.adapter.DrawPagerAdapter
15 | import com.redhoodhan.drawing.util.AnimationUtil
16 |
17 |
18 | private const val TAG = "MainActivity"
19 |
20 | private const val POSITION_DRAW_OPTION_FRAGMENT = 0
21 | private const val POSITION_DRAW_BACKGROUND_FRAGMENT = 1
22 | private const val POSITION_DRAW_ERASER_FRAGMENT = 2
23 |
24 | class MainActivity : AppCompatActivity() {
25 |
26 | private lateinit var binding: ActivityMainBinding
27 |
28 | private val viewModel: DrawViewModel by viewModels()
29 |
30 | private lateinit var pagerAdapter: DrawPagerAdapter
31 | private lateinit var viewPager: ViewPager2
32 |
33 | private val drawButtonList = mutableListOf()
34 |
35 | /**
36 | * List that stores all the button that should perform with the drawing panel fragments, such as
37 | * [DrawBackgroundFragment] and [DrawOptionFragment]
38 | */
39 | private val invokeFragmentButtonList = mutableListOf()
40 |
41 | private val doubleClickExceptionButtonList = mutableListOf()
42 |
43 | private val drawOptionOriginalTranslationY: Float by lazy {
44 | resources.getDimension(R.dimen.draw_option_translation_y_init)
45 | }
46 |
47 | override fun onCreate(savedInstanceState: Bundle?) {
48 | super.onCreate(savedInstanceState)
49 |
50 | initBinding()
51 |
52 | initButtonState()
53 |
54 | initClickListener()
55 |
56 | initDrawView()
57 |
58 | initObservers()
59 |
60 | initDrawFragmentsInPager()
61 | }
62 |
63 | private fun initBinding() {
64 | binding = ActivityMainBinding.inflate(layoutInflater)
65 | setContentView(binding.root)
66 | }
67 |
68 | private fun initButtonState() {
69 | binding.paintSolidButton.isClicked = true
70 |
71 | // Initialize the id of the default clicked StateButton
72 | binding.paintSolidButton.id.let {
73 | viewModel.curClickedStateButtonId = it
74 | viewModel.storedClickedStateButtonId = it
75 | }
76 |
77 | doubleClickExceptionButtonList.add(binding.backgroundButton)
78 | }
79 |
80 | private fun initClickListener() {
81 | binding.apply {
82 | paintSolidButton.setOnStateClickListener(true) {
83 | switchFragmentInPager(POSITION_DRAW_OPTION_FRAGMENT)
84 | switchToLineType(LineType.SOLID)
85 | }
86 |
87 | paintDashedButton.setOnStateClickListener(true) {
88 | switchFragmentInPager(POSITION_DRAW_OPTION_FRAGMENT)
89 | switchToLineType(LineType.DASH)
90 | }
91 |
92 | eraserButton.setOnStateClickListener(true) {
93 | switchFragmentInPager(POSITION_DRAW_ERASER_FRAGMENT)
94 | toggleEraser()
95 | }
96 |
97 | paintChiselTipButton.setOnStateClickListener(true) {
98 | switchFragmentInPager(POSITION_DRAW_OPTION_FRAGMENT)
99 | switchToLineType(LineType.CHISEL)
100 | }
101 |
102 | backgroundButton.setOnStateClickListener(
103 | needsPerformWithPanel = true,
104 | needsStoreButtonId = false
105 | ) {
106 | switchFragmentInPager(POSITION_DRAW_BACKGROUND_FRAGMENT)
107 | }
108 |
109 | undoButton.setOnClickListener {
110 | drawView.undo()
111 | }
112 |
113 | redoButton.setOnClickListener {
114 | drawView.redo()
115 | }
116 | }
117 | }
118 |
119 | private fun initDrawView() {
120 | initDrawCallback()
121 |
122 | initDefaultDrawOptions()
123 | }
124 |
125 | private fun initDrawCallback() {
126 | binding.drawView.apply {
127 | drawViewPressCallback = {
128 | hideDrawOptionPanel()
129 | refreshDrawButtonStateByMode()
130 | }
131 |
132 | undoStateCallback = { isAvailable ->
133 | binding.undoButton.isClicked = isAvailable
134 | }
135 |
136 | redoStateCallback = { isAvailable ->
137 | binding.redoButton.isClicked = isAvailable
138 | }
139 | }
140 | }
141 |
142 | private fun initObservers() {
143 | viewModel.let {
144 | it.drawColorLiveData.observe(this) { colorId ->
145 | onDrawColorChanged(colorId)
146 | }
147 |
148 | it.drawBrushSizeLiveData.observe(this) { brushSize ->
149 | onBrushSizeChanged(brushSize)
150 | }
151 |
152 | it.drawEraserSizeLiveData.observe(this) { eraserSize ->
153 | onBrushSizeChanged(eraserSize, true)
154 | }
155 |
156 | it.backgroundColorLiveData.observe(this) { colorId ->
157 | onBackgroundColorChanged(colorId)
158 | }
159 |
160 | it.backgroundImgResLiveData.observe(this) { imgResId ->
161 | onBackgroundImgResChanged(imgResId)
162 | }
163 |
164 | it.clearCanvasLiveData.observe(this) { needsSaving ->
165 | onCanvasCleared(needsSaving)
166 | }
167 | }
168 | }
169 |
170 | private fun initDrawFragmentsInPager() {
171 | pagerAdapter = DrawPagerAdapter(this)
172 |
173 | viewPager = binding.drawPager.apply {
174 | isUserInputEnabled = false
175 | adapter = pagerAdapter
176 | }
177 | }
178 |
179 | private fun switchFragmentInPager(position: Int) {
180 | viewPager.setCurrentItem(position, false)
181 | }
182 |
183 | private fun onDrawColorChanged(colorResId: Int) {
184 | binding.drawView.brushColor = ResourcesCompat.getColor(resources, colorResId, null)
185 | }
186 |
187 | private fun onBrushSizeChanged(size: Float, isFromEraser: Boolean = false) {
188 | if (isFromEraser) {
189 | binding.drawView.eraserSize = size
190 | } else {
191 | binding.drawView.brushSize = size
192 | }
193 | }
194 |
195 | private fun onBackgroundColorChanged(colorResId: Int) {
196 | binding.drawView.canvasBackgroundColor =
197 | ResourcesCompat.getColor(resources, colorResId, null)
198 | }
199 |
200 | private fun onBackgroundImgResChanged(imgResId: Int) {
201 | binding.drawView.canvasBackgroundImg = imgResId
202 | }
203 |
204 | private fun onCanvasCleared(needsSaving: Boolean) {
205 | binding.drawView.clearCanvas(needsSaving)
206 | }
207 |
208 | private fun initDefaultDrawOptions() {
209 | viewModel.defaultBrushSize = binding.drawView.brushSize
210 | }
211 |
212 | /**
213 | * Wrapper of the [View.OnClickListener] to extract common operations when clicking the
214 | * [StateImageButton] on the draw-options layout and leaves a callback [clickCallback] for each
215 | * [StateImageButton] to perform its own click event.
216 | *
217 | */
218 | private fun StateImageButton.setOnStateClickListener(
219 | needsPerformWithPanel: Boolean,
220 | needsStoreButtonId: Boolean = true,
221 | clickCallback: (View) -> Unit
222 | ) {
223 | // Adds the current state button to a general list for a easier retrieving and iterating.
224 | drawButtonList.add(this)
225 |
226 | if (needsPerformWithPanel) {
227 | invokeFragmentButtonList.add(this)
228 | }
229 |
230 | setOnClickListener {
231 | viewModel.curClickedStateButtonId = it.id
232 | refreshAllButtonState(id)
233 |
234 | // If stored, the button ID could be retrieved for calling refreshDrawButtonStateByMode
235 | if (needsStoreButtonId) {
236 | viewModel.storedClickedStateButtonId = id
237 | }
238 |
239 | // TODO: Refactor the if-else logic
240 | if (needsPerformWithPanel && viewModel.isStateButtonDoubleClicked) {
241 | performWithDrawOptionPanel()
242 | } else if (!invokeFragmentButtonList.contains(this)) {
243 | // If the new clicked state button should also perform with the draw option panel,
244 | // then we do not hide the fragment container. We just switch the original fragment
245 | // to the new fragment corresponding to what state button we clicked.
246 | hideDrawOptionPanel()
247 | } else {
248 | // Case for the StateButton in the exception list
249 | showDrawOptionPanel()
250 | }
251 |
252 | // Callback that performs the actual click event logic
253 | clickCallback.invoke(this)
254 | }
255 | }
256 |
257 | /**
258 | * This function is called when user has clicked the buttons that has nothing to do with the
259 | * paint option and paths and only affects the canvas, i.e., the background button, and then
260 | * press the [DrawView] directly without clicking other button on the draw option panel.
261 | *
262 | * By calling this function, we retrieve the state button ID by accessing to the ???
263 | * we are using and then retrieve the button by ID. Then
264 | * set the button we retrieved as the clicked one.
265 | */
266 | private fun refreshDrawButtonStateByMode() {
267 | // TODO: abstraction is needed
268 | when (viewModel.curClickedStateButtonId) {
269 | binding.backgroundButton.id -> {
270 | refreshAllButtonState(viewModel.storedClickedStateButtonId)
271 | }
272 | else -> {}
273 | }
274 | }
275 |
276 | private fun performWithDrawOptionPanel() {
277 | val curTranslationY = binding.drawOptionLayout.translationY
278 |
279 | AnimationUtil.repTranslateY(
280 | binding.drawOptionLayout,
281 | curTranslationY,
282 | drawOptionOriginalTranslationY
283 | )
284 | }
285 |
286 | private fun hideDrawOptionPanel() {
287 | binding.drawOptionLayout.apply {
288 | if (translationY == drawOptionOriginalTranslationY) {
289 | return
290 | }
291 | animate().translationY(drawOptionOriginalTranslationY)
292 | }
293 | }
294 |
295 | private fun showDrawOptionPanel() {
296 | binding.drawOptionLayout.apply {
297 | if (translationY == 0F) {
298 | return
299 | }
300 | animate().translationY(0F)
301 | }
302 | }
303 |
304 | /**
305 | * This function is called when one of the [StateImageButton] on the button panel is clicked, in
306 | * order to refresh the buttons' clicked states.
307 | *
308 | * Note that we can simply repeatedly assigning value to [StateImageButton.isClicked] without
309 | * worrying about calling ColorFilter redundantly, because [StateImageButton.isClicked] will
310 | * call ColorFilter related function only if the state is changed.
311 | */
312 | private fun refreshAllButtonState(clickedViewId: Int) {
313 | drawButtonList.forEach {
314 | it.isClicked = (it.id == clickedViewId)
315 | }
316 | }
317 |
318 | private fun switchToLineType(lineType: LineType = LineType.SOLID) {
319 | binding.drawView.lineType = lineType
320 | }
321 |
322 | private fun toggleEraser() {
323 | switchToLineType(LineType.ERASER)
324 | }
325 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/redhoodhan/drawing/ui/draw/DrawBackgroundFragment.kt:
--------------------------------------------------------------------------------
1 | package com.redhoodhan.drawing.ui.draw
2 |
3 | import android.os.Bundle
4 | import androidx.fragment.app.Fragment
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import androidx.fragment.app.activityViewModels
9 | import androidx.recyclerview.widget.LinearLayoutManager
10 | import com.redhoodhan.drawing.databinding.FragmentDrawBackgroundBinding
11 | import com.redhoodhan.drawing.ui.draw.adapter.DrawColorResAdapter
12 | import com.redhoodhan.drawing.ui.draw.adapter.DrawImgResAdapter
13 |
14 | private const val TAG = "DrawOptionFragment"
15 |
16 | class DrawBackgroundFragment : Fragment() {
17 |
18 | private var _binding: FragmentDrawBackgroundBinding? = null
19 |
20 | private val binding
21 | get() = _binding!!
22 |
23 | private val viewModel: DrawViewModel by activityViewModels()
24 |
25 | private var colorAdapter: DrawColorResAdapter? = null
26 |
27 | private var imgResAdapter: DrawImgResAdapter? = null
28 |
29 | override fun onCreate(savedInstanceState: Bundle?) {
30 | super.onCreate(savedInstanceState)
31 | }
32 |
33 | override fun onCreateView(
34 | inflater: LayoutInflater, container: ViewGroup?,
35 | savedInstanceState: Bundle?
36 | ): View? {
37 | _binding = FragmentDrawBackgroundBinding.inflate(inflater, container, false)
38 |
39 | return _binding?.root
40 | }
41 |
42 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
43 | super.onViewCreated(view, savedInstanceState)
44 |
45 | initColorRecyclerView()
46 | initImgResRecyclerView()
47 | }
48 |
49 | companion object {
50 | @JvmStatic
51 | fun newInstance() = DrawBackgroundFragment()
52 | }
53 |
54 | private fun initImgResRecyclerView() {
55 | val linearLayoutManager =
56 | LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
57 |
58 | viewModel.backgroundList?.let {
59 | imgResAdapter = DrawImgResAdapter(it).also { adapter ->
60 | adapter.imgSelectCallback = { imgResId ->
61 | onImgItemSelected(imgResId)
62 | }
63 | }
64 | }
65 |
66 | binding.drawBgRecyclerView.apply {
67 | adapter = imgResAdapter
68 | layoutManager = linearLayoutManager
69 | }
70 | }
71 |
72 | private fun initColorRecyclerView() {
73 | val linearLayoutManager =
74 | LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
75 |
76 | viewModel.colorList?.let {
77 | colorAdapter = DrawColorResAdapter(it).also { adapter ->
78 | adapter.colorSelectCallback = { colorResId ->
79 | onColorItemSelected(colorResId)
80 | }
81 | }
82 | }
83 |
84 | binding.drawColorRecyclerView.apply {
85 | adapter = colorAdapter
86 | layoutManager = linearLayoutManager
87 | }
88 | }
89 |
90 | private fun onColorItemSelected(colorResId: Int) {
91 | viewModel.backgroundColorLiveData.postValue(colorResId)
92 | }
93 |
94 | private fun onImgItemSelected(imgResId: Int) {
95 | viewModel.backgroundImgResLiveData.postValue(imgResId)
96 | }
97 |
98 |
99 | override fun onDestroyView() {
100 | super.onDestroyView()
101 | _binding = null
102 | }
103 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/redhoodhan/drawing/ui/draw/DrawEraserFragment.kt:
--------------------------------------------------------------------------------
1 | package com.redhoodhan.drawing.ui.draw
2 |
3 | import android.os.Bundle
4 | import androidx.fragment.app.Fragment
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import android.widget.SeekBar
9 | import androidx.fragment.app.activityViewModels
10 | import com.redhoodhan.drawing.databinding.FragmentDrawEraserBinding
11 |
12 | class DrawEraserFragment : Fragment() {
13 |
14 | private var _binding: FragmentDrawEraserBinding? = null
15 |
16 | private val binding
17 | get() = _binding!!
18 |
19 | private val viewModel: DrawViewModel by activityViewModels()
20 |
21 | companion object {
22 | @JvmStatic
23 | fun newInstance() = DrawEraserFragment()
24 | }
25 |
26 | override fun onCreateView(
27 | inflater: LayoutInflater, container: ViewGroup?,
28 | savedInstanceState: Bundle?
29 | ): View? {
30 | _binding = FragmentDrawEraserBinding.inflate(inflater, container, false)
31 |
32 | return _binding?.root
33 | }
34 |
35 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
36 | super.onViewCreated(view, savedInstanceState)
37 |
38 | initClickListener()
39 |
40 | initSeekbar()
41 | }
42 |
43 | override fun onDestroyView() {
44 | super.onDestroyView()
45 | _binding = null
46 | }
47 |
48 | private fun initClickListener() {
49 | binding.clearButton.setOnClickListener {
50 | viewModel.notifyClearCanvas(true)
51 | }
52 | }
53 |
54 | private fun initSeekbar() {
55 | // Initialize default brush size that the SeekBar indicates
56 | binding.eraseSizeSeekbar.progress = viewModel.defaultBrushSize.toInt()
57 |
58 | binding.eraseSizeSeekbar.setOnSeekBarChangeListener(object :
59 | SeekBar.OnSeekBarChangeListener {
60 | override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
61 | }
62 |
63 | override fun onStartTrackingTouch(seekBar: SeekBar?) {
64 | }
65 |
66 | override fun onStopTrackingTouch(seekBar: SeekBar?) {
67 | seekBar?.let {
68 | viewModel.notifyChangeBrushSize(it.progress, isFromEraser = true)
69 | }
70 | }
71 |
72 | })
73 | }
74 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/redhoodhan/drawing/ui/draw/DrawOptionFragment.kt:
--------------------------------------------------------------------------------
1 | package com.redhoodhan.drawing.ui.draw
2 |
3 | import android.os.Bundle
4 | import androidx.fragment.app.Fragment
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import android.widget.SeekBar
9 | import androidx.core.content.res.ResourcesCompat
10 | import androidx.core.graphics.BlendModeColorFilterCompat
11 | import androidx.core.graphics.BlendModeCompat
12 | import androidx.fragment.app.activityViewModels
13 | import androidx.recyclerview.widget.LinearLayoutManager
14 | import com.redhoodhan.drawing.databinding.FragmentDrawOptionBinding
15 | import com.redhoodhan.drawing.ui.draw.adapter.DrawColorResAdapter
16 |
17 | private const val TAG = "DrawOptionFragment"
18 |
19 | class DrawOptionFragment : Fragment() {
20 |
21 | private var _binding: FragmentDrawOptionBinding? = null
22 |
23 | private val binding
24 | get() = _binding!!
25 |
26 | private val viewModel: DrawViewModel by activityViewModels()
27 |
28 | private var colorAdapter: DrawColorResAdapter? = null
29 |
30 | override fun onCreate(savedInstanceState: Bundle?) {
31 | super.onCreate(savedInstanceState)
32 | }
33 |
34 | override fun onCreateView(
35 | inflater: LayoutInflater, container: ViewGroup?,
36 | savedInstanceState: Bundle?
37 | ): View? {
38 | _binding = FragmentDrawOptionBinding.inflate(inflater, container, false)
39 |
40 | return _binding?.root
41 | }
42 |
43 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
44 | super.onViewCreated(view, savedInstanceState)
45 |
46 | initSeekBarListener()
47 |
48 | initRecyclerView()
49 | }
50 |
51 | companion object {
52 | @JvmStatic
53 | fun newInstance() = DrawOptionFragment()
54 | }
55 |
56 | private fun initRecyclerView() {
57 | viewModel.colorList?.let {
58 | colorAdapter = DrawColorResAdapter(it).also { adapter ->
59 | adapter.colorSelectCallback = { colorResId ->
60 | onColorItemSelected(colorResId)
61 | }
62 | }
63 | }
64 |
65 | val linearLayoutManager =
66 | LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
67 |
68 | binding.drawColorRecyclerView.apply {
69 | adapter = colorAdapter
70 | layoutManager = linearLayoutManager
71 | }
72 |
73 | // Initialize default brush color
74 | colorAdapter?.curSelectItemPosition?.let { pos ->
75 | viewModel.colorList?.let { list ->
76 | onColorItemSelected(list[pos])
77 | }
78 | }
79 | }
80 |
81 | private fun onColorItemSelected(colorResId: Int) {
82 | viewModel.drawColorLiveData.postValue(colorResId)
83 |
84 | changeSeekBarThumbColor(colorResId)
85 | }
86 |
87 | private fun changeSeekBarThumbColor(
88 | colorResId: Int,
89 | blendMode: BlendModeCompat = BlendModeCompat.SRC_ATOP
90 | ) {
91 | ResourcesCompat.getColor(resources, colorResId, null).let { color ->
92 | binding.brushSizeSeekbar.apply {
93 | val colorFilter =
94 | BlendModeColorFilterCompat.createBlendModeColorFilterCompat(color, blendMode)
95 | thumb.colorFilter = colorFilter
96 | progressDrawable.colorFilter = colorFilter
97 | }
98 | }
99 | }
100 |
101 | private fun initSeekBarListener() {
102 | // Initialize default brush size that the SeekBar indicates
103 | binding.brushSizeSeekbar.progress = viewModel.defaultBrushSize.toInt()
104 |
105 | binding.brushSizeSeekbar.setOnSeekBarChangeListener(object :
106 | SeekBar.OnSeekBarChangeListener {
107 | override fun onProgressChanged(seekbar: SeekBar?, progress: Int, fromUser: Boolean) {
108 | }
109 |
110 | override fun onStartTrackingTouch(seekbar: SeekBar?) {
111 |
112 | }
113 |
114 | override fun onStopTrackingTouch(seekbar: SeekBar?) {
115 | seekbar?.let {
116 | viewModel.notifyChangeBrushSize(it.progress)
117 | }
118 | }
119 | })
120 | }
121 |
122 | override fun onDestroyView() {
123 | super.onDestroyView()
124 | _binding = null
125 | }
126 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/redhoodhan/drawing/ui/draw/DrawRepository.kt:
--------------------------------------------------------------------------------
1 | package com.redhoodhan.drawing.ui.draw
2 |
3 | import com.redhoodhan.drawing.R
4 |
5 | class DrawRepository {
6 |
7 | val colorList = mutableListOf()
8 |
9 | val backgroundList = mutableListOf()
10 |
11 | init {
12 | initColorList()
13 |
14 | initBackgroundList()
15 | }
16 |
17 | private fun initColorList() {
18 | colorList.apply {
19 | add(R.color.black)
20 | add(R.color.hotpink)
21 | add(R.color.coral)
22 | add(R.color.deeppink)
23 | add(R.color.salmon)
24 | add(R.color.palevioletred)
25 | add(R.color.papayawhip)
26 | add(R.color.gold)
27 | add(R.color.pink)
28 | add(R.color.lightyellow)
29 | add(R.color.peachpuff)
30 | add(R.color.red)
31 | add(R.color.orchid)
32 | add(R.color.mediumvioletred)
33 | add(R.color.darkkhaki)
34 | add(R.color.yellowgreen)
35 | add(R.color.darkseagreen)
36 | add(R.color.powderblue)
37 | add(R.color.paleturquoise)
38 | add(R.color.olivedrab)
39 | add(R.color.mediumaquamarine)
40 | add(R.color.cornflowerblue)
41 | add(R.color.cadetblue)
42 | add(R.color.steelblue)
43 | add(R.color.royalblue)
44 | add(R.color.darkslateblue)
45 | add(R.color.royalblue)
46 | add(R.color.seagreen)
47 | add(R.color.darkturquoise)
48 | add(R.color.mediumblue)
49 | add(R.color.white)
50 | }
51 | }
52 |
53 | private fun initBackgroundList() {
54 | backgroundList.apply {
55 | add(R.drawable.img_draw_background_1)
56 | add(R.drawable.img_draw_background_2)
57 | }
58 | }
59 |
60 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/redhoodhan/drawing/ui/draw/DrawViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.redhoodhan.drawing.ui.draw
2 |
3 | import androidx.lifecycle.MutableLiveData
4 | import androidx.lifecycle.ViewModel
5 | import com.redhoodhan.draw.draw_option.data.DrawConst.Companion.DEFAULT_STROKE_WIDTH
6 | import com.redhoodhan.drawing.ui.draw.DrawRepository
7 | import kotlin.properties.Delegates
8 |
9 | class DrawViewModel : ViewModel() {
10 |
11 | private var mRepo: DrawRepository? = null
12 |
13 | init {
14 | mRepo = DrawRepository()
15 | }
16 |
17 | private var _needsShowDrawOptionPanel: Boolean = false
18 |
19 | /**
20 | * LiveData that stores the color resource id corresponding to the selected item in the recycler
21 | * view in the [DrawOptionFragment].
22 | */
23 | val drawColorLiveData: MutableLiveData = MutableLiveData()
24 |
25 | /**
26 | * LiveData that stores the color resource id corresponding to the selected item in the recycler
27 | * view in the [DrawBackgroundFragment].
28 | */
29 | val backgroundColorLiveData: MutableLiveData = MutableLiveData()
30 |
31 | /**
32 | * LiveData that stores the drawable resource id corresponding to the selected item in the recycler
33 | * view in the [DrawBackgroundFragment].
34 | */
35 | val backgroundImgResLiveData: MutableLiveData = MutableLiveData()
36 |
37 | /**
38 | * LiveData that stores the brush size in Float corresponding to the seekbar in the
39 | * [DrawEraserFragment].
40 | */
41 | val drawBrushSizeLiveData: MutableLiveData = MutableLiveData()
42 |
43 | val drawEraserSizeLiveData: MutableLiveData = MutableLiveData()
44 |
45 | val clearCanvasLiveData: MutableLiveData = MutableLiveData()
46 |
47 | var storedClickedStateButtonId: Int = 0
48 |
49 | var curClickedStateButtonId: Int
50 | by Delegates.observable(0) { _, oldValue, newValue ->
51 | _needsShowDrawOptionPanel =
52 | ((oldValue == newValue))
53 | }
54 |
55 | val isStateButtonDoubleClicked: Boolean
56 | get() = _needsShowDrawOptionPanel
57 |
58 | val colorList: List?
59 | get() = mRepo?.colorList
60 |
61 | val backgroundList: List?
62 | get() = mRepo?.backgroundList
63 |
64 | var defaultBrushSize: Float = DEFAULT_STROKE_WIDTH
65 |
66 | fun notifyChangeBrushSize(
67 | progress: Int,
68 | maxProgress: Int = 50,
69 | isFromEraser: Boolean = false
70 | ) {
71 | if (isFromEraser) {
72 | drawEraserSizeLiveData.postValue(progress.toFloat())
73 | } else {
74 | drawBrushSizeLiveData.postValue(progress.toFloat())
75 | }
76 | }
77 |
78 | fun notifyClearCanvas(needsSaving: Boolean = true) {
79 | clearCanvasLiveData.postValue(needsSaving)
80 | }
81 |
82 | override fun onCleared() {
83 | super.onCleared()
84 | mRepo = null
85 | }
86 |
87 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/redhoodhan/drawing/ui/draw/adapter/DrawColorResAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.redhoodhan.drawing.ui.draw.adapter
2 |
3 | import android.view.LayoutInflater
4 | import android.view.ViewGroup
5 | import androidx.recyclerview.widget.RecyclerView
6 | import com.redhoodhan.drawing.databinding.ItemDrawResBinding
7 |
8 | class DrawColorResAdapter(private val colorList: List) :
9 | RecyclerView.Adapter() {
10 |
11 | private var _curSelectItemPosition: Int = 0
12 |
13 | val curSelectItemPosition: Int
14 | get() = _curSelectItemPosition
15 |
16 | var colorSelectCallback: ((colorResId: Int) -> Unit)? = null
17 |
18 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
19 | ItemDrawResBinding.inflate(LayoutInflater.from(parent.context), parent, false)
20 | .also {
21 | return ViewHolder(it)
22 | }
23 | }
24 |
25 | override fun onBindViewHolder(holder: ViewHolder, position: Int) {
26 | val colorResId = colorList[position]
27 |
28 | holder.bindBackgroundColor(colorResId)
29 | // Refresh the select state of the current item
30 | if (position == _curSelectItemPosition) {
31 | holder.setCurSelected(true)
32 | } else {
33 | holder.setCurSelected(false)
34 | }
35 | }
36 |
37 | override fun getItemCount(): Int = colorList.size
38 |
39 | inner class ViewHolder(private val binding: ItemDrawResBinding) :
40 | RecyclerView.ViewHolder(binding.root) {
41 |
42 | init {
43 | itemView.setOnClickListener { performClickEvent() }
44 | }
45 |
46 | private fun performClickEvent() {
47 | val tempPosition = _curSelectItemPosition
48 | colorSelectCallback?.invoke(colorList[adapterPosition])
49 | _curSelectItemPosition = adapterPosition
50 | setCurSelected(true)
51 |
52 | // Refresh select states of the recycler view
53 | notifyItemChanged(_curSelectItemPosition)
54 | notifyItemChanged(tempPosition)
55 | }
56 |
57 | fun bindBackgroundColor(colorRes: Int) {
58 | binding.drawColorItem.drawColorResId = colorRes
59 | }
60 |
61 | fun setCurSelected(isSelected: Boolean) {
62 | binding.drawColorItem.isCurSelected = isSelected
63 | }
64 |
65 | }
66 | }
67 |
68 |
--------------------------------------------------------------------------------
/app/src/main/java/com/redhoodhan/drawing/ui/draw/adapter/DrawImgResAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.redhoodhan.drawing.ui.draw.adapter
2 |
3 | import android.view.LayoutInflater
4 | import android.view.ViewGroup
5 | import androidx.core.content.res.ResourcesCompat
6 | import androidx.recyclerview.widget.RecyclerView
7 | import com.redhoodhan.drawing.databinding.ItemBgImgResBinding
8 |
9 | class DrawImgResAdapter(private val imgList: List) :
10 | RecyclerView.Adapter() {
11 |
12 | private var _curSelectItemPosition: Int = 0
13 |
14 | val curSelectItemPosition: Int
15 | get() = _curSelectItemPosition
16 |
17 | var imgSelectCallback: ((colorResId: Int) -> Unit)? = null
18 |
19 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
20 | ItemBgImgResBinding.inflate(LayoutInflater.from(parent.context), parent, false)
21 | .also {
22 | return ViewHolder(it)
23 | }
24 | }
25 |
26 | override fun onBindViewHolder(holder: ViewHolder, position: Int) {
27 | val imgResId = imgList[position]
28 |
29 | holder.bindBackgroundRes(imgResId)
30 | // Refresh the select state of the current item
31 | if (position == _curSelectItemPosition) {
32 | holder.setCurSelected(true)
33 | } else {
34 | holder.setCurSelected(false)
35 | }
36 | }
37 |
38 | override fun getItemCount(): Int = imgList.size
39 |
40 | inner class ViewHolder(private val binding: ItemBgImgResBinding) :
41 | RecyclerView.ViewHolder(binding.root) {
42 |
43 | init {
44 | itemView.setOnClickListener { performClickEvent() }
45 | }
46 |
47 | private fun performClickEvent() {
48 | val tempPosition = _curSelectItemPosition
49 | imgSelectCallback?.invoke(imgList[adapterPosition])
50 | _curSelectItemPosition = adapterPosition
51 | setCurSelected(true)
52 |
53 | // Refresh select states of the recycler view
54 | notifyItemChanged(_curSelectItemPosition)
55 | notifyItemChanged(tempPosition)
56 | }
57 |
58 | fun bindBackgroundRes(imgRes: Int) {
59 | binding.bgImage.apply {
60 | setImageDrawable(ResourcesCompat.getDrawable(resources, imgRes, null))
61 | }
62 | }
63 |
64 | fun setCurSelected(isSelected: Boolean) {
65 | }
66 |
67 | }
68 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/redhoodhan/drawing/ui/draw/adapter/DrawPagerAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.redhoodhan.drawing.ui.draw.adapter
2 |
3 | import androidx.fragment.app.Fragment
4 | import androidx.fragment.app.FragmentActivity
5 | import androidx.viewpager2.adapter.FragmentStateAdapter
6 | import com.redhoodhan.drawing.ui.draw.DrawBackgroundFragment
7 | import com.redhoodhan.drawing.ui.draw.DrawEraserFragment
8 | import com.redhoodhan.drawing.ui.draw.DrawOptionFragment
9 |
10 | private const val TOTAL_FRAGMENT_NUM = 3
11 | private const val TAG = "DrawPagerAdapter"
12 |
13 | class DrawPagerAdapter(fragmentActivity: FragmentActivity) :
14 | FragmentStateAdapter(fragmentActivity) {
15 |
16 | override fun getItemCount(): Int {
17 | return TOTAL_FRAGMENT_NUM
18 | }
19 |
20 | /**
21 | * Create certain type of fragment according to the item position in the viewPager.
22 | *
23 | * Note that the [position] equaling to 0 corresponds to [DrawOptionFragment], and the [position]
24 | * equaling to 1 corresponds to [DrawBackgroundFragment]
25 | */
26 | override fun createFragment(position: Int): Fragment {
27 | return when (position) {
28 | 0 -> DrawOptionFragment.newInstance()
29 | 1 -> DrawBackgroundFragment.newInstance()
30 | 2 -> DrawEraserFragment.newInstance()
31 | else -> DrawOptionFragment.newInstance()
32 | }
33 | }
34 |
35 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/redhoodhan/drawing/util/AnimationUtil.kt:
--------------------------------------------------------------------------------
1 | package com.redhoodhan.drawing.util
2 |
3 | import android.util.Log
4 | import android.view.View
5 |
6 | class AnimationUtil {
7 |
8 | companion object {
9 |
10 | /**
11 | * This function is called when we need to translate a view along the Y-axis and specially
12 | * the view is expected to translate between two Y-location repeatedly.
13 | *
14 | * [curTranslationY] is the current translationY value of the view.
15 | * [originalTranslationY] is the initial value of the translationY of the view.
16 | */
17 | fun repTranslateY(
18 | view: View,
19 | curTranslationY: Float,
20 | originalTranslationY: Float
21 | ) {
22 | val newTranslationY = if (curTranslationY == originalTranslationY) {
23 | 0f
24 | } else {
25 | originalTranslationY
26 | }
27 |
28 | view.animate().translationY(newTranslationY)
29 | }
30 |
31 | fun scaleXY(view: View, curScaleValue: Float) {
32 | view.animate().scaleX(curScaleValue).scaleY(curScaleValue)
33 | }
34 | }
35 |
36 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/redhoodhan/drawing/util/DisplayUtil.kt:
--------------------------------------------------------------------------------
1 | package com.redhoodhan.drawing.util
2 |
3 | import android.content.res.Resources
4 |
5 | fun Float.dp2px(): Float = this * Resources.getSystem().displayMetrics.density
6 |
7 | fun Float.px2dp(): Float = this / Resources.getSystem().displayMetrics.density
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/icon_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-hdpi/icon_background.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/icon_background_night.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-hdpi/icon_background_night.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/icon_chisel_tip.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-hdpi/icon_chisel_tip.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/icon_dashed_line.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-hdpi/icon_dashed_line.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/icon_dashed_line_night.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-hdpi/icon_dashed_line_night.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/icon_eraser.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-hdpi/icon_eraser.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/icon_eraser_night.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-hdpi/icon_eraser_night.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/icon_paint.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-hdpi/icon_paint.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/icon_paint_dashed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-hdpi/icon_paint_dashed.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/icon_paint_night.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-hdpi/icon_paint_night.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/icon_paint_solid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-hdpi/icon_paint_solid.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/icon_palette.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-hdpi/icon_palette.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/icon_palette_night.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-hdpi/icon_palette_night.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/icon_redo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-hdpi/icon_redo.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/icon_solid_line.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-hdpi/icon_solid_line.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/icon_solid_line_night.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-hdpi/icon_solid_line_night.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/icon_undo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-hdpi/icon_undo.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/icon_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xhdpi/icon_background.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/icon_background_night.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xhdpi/icon_background_night.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/icon_chisel_tip.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xhdpi/icon_chisel_tip.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/icon_dashed_line.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xhdpi/icon_dashed_line.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/icon_dashed_line_night.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xhdpi/icon_dashed_line_night.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/icon_eraser.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xhdpi/icon_eraser.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/icon_eraser_night.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xhdpi/icon_eraser_night.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/icon_paint.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xhdpi/icon_paint.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/icon_paint_dashed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xhdpi/icon_paint_dashed.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/icon_paint_night.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xhdpi/icon_paint_night.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/icon_paint_solid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xhdpi/icon_paint_solid.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/icon_palette.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xhdpi/icon_palette.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/icon_palette_night.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xhdpi/icon_palette_night.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/icon_redo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xhdpi/icon_redo.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/icon_solid_line.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xhdpi/icon_solid_line.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/icon_solid_line_night.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xhdpi/icon_solid_line_night.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/icon_undo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xhdpi/icon_undo.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/icon_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xxhdpi/icon_background.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/icon_background_night.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xxhdpi/icon_background_night.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/icon_chisel_tip.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xxhdpi/icon_chisel_tip.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/icon_dashed_line.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xxhdpi/icon_dashed_line.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/icon_dashed_line_night.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xxhdpi/icon_dashed_line_night.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/icon_eraser.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xxhdpi/icon_eraser.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/icon_eraser_night.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xxhdpi/icon_eraser_night.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/icon_paint.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xxhdpi/icon_paint.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/icon_paint_dashed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xxhdpi/icon_paint_dashed.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/icon_paint_night.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xxhdpi/icon_paint_night.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/icon_paint_solid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xxhdpi/icon_paint_solid.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/icon_palette.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xxhdpi/icon_palette.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/icon_palette_night.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xxhdpi/icon_palette_night.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/icon_redo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xxhdpi/icon_redo.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/icon_solid_line.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xxhdpi/icon_solid_line.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/icon_solid_line_night.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xxhdpi/icon_solid_line_night.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/icon_undo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xxhdpi/icon_undo.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/icon_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xxxhdpi/icon_background.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/icon_background_night.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xxxhdpi/icon_background_night.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/icon_chisel_tip.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xxxhdpi/icon_chisel_tip.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/icon_dashed_line.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xxxhdpi/icon_dashed_line.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/icon_dashed_line_night.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xxxhdpi/icon_dashed_line_night.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/icon_eraser.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xxxhdpi/icon_eraser.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/icon_eraser_night.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xxxhdpi/icon_eraser_night.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/icon_paint.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xxxhdpi/icon_paint.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/icon_paint_dashed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xxxhdpi/icon_paint_dashed.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/icon_paint_night.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xxxhdpi/icon_paint_night.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/icon_paint_solid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xxxhdpi/icon_paint_solid.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/icon_palette.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xxxhdpi/icon_palette.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/icon_palette_night.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xxxhdpi/icon_palette_night.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/icon_redo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xxxhdpi/icon_redo.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/icon_solid_line.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xxxhdpi/icon_solid_line.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/icon_solid_line_night.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xxxhdpi/icon_solid_line_night.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/icon_undo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable-xxxhdpi/icon_undo.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/img_draw_background_1.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable/img_draw_background_1.webp
--------------------------------------------------------------------------------
/app/src/main/res/drawable/img_draw_background_2.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/drawable/img_draw_background_2.webp
--------------------------------------------------------------------------------
/app/src/main/res/drawable/shape_pure_color_circle.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
13 |
14 |
25 |
26 |
37 |
38 |
51 |
52 |
62 |
63 |
72 |
73 |
82 |
83 |
92 |
93 |
102 |
103 |
109 |
110 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_draw_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
15 |
16 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_draw_eraser.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
17 |
18 |
28 |
29 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_draw_option.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
15 |
16 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_bg_img_res.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_draw_res.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
24 |
--------------------------------------------------------------------------------
/app/src/main/res/values/arrays.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - @color/ivory
5 | - @color/lightyellow
6 | - @color/yellow
7 | - @color/snow
8 | - @color/papayawhip
9 | - @color/blanchedalmond
10 | - @color/mistyrose
11 | - @color/bisque
12 | - @color/moccasin
13 | - @color/navajowhite
14 | - @color/peachpuff
15 | - @color/gold
16 | - @color/pink
17 | - @color/lightpink
18 | - @color/orange
19 | - @color/lightsalmon
20 | - @color/darkorange
21 | - @color/coral
22 | - @color/hotpink
23 | - @color/tomato
24 | - @color/orangered
25 | - @color/deeppink
26 | - @color/fuchsia
27 | - @color/magenta
28 | - @color/red
29 | - @color/oldlace
30 | - @color/lightgoldenrodyellow
31 | - @color/antiquewhite
32 | - @color/salmon
33 | - @color/ghostwhite
34 | - @color/mintcream
35 | - @color/sandybrown
36 | - @color/azure
37 | - @color/honeydew
38 | - @color/aliceblue
39 | - @color/khaki
40 | - @color/lightcoral
41 | - @color/palegoldenrod
42 | - @color/violet
43 | - @color/darksalmon
44 | - @color/lavender
45 | - @color/lightcyan
46 | - @color/burlywood
47 | - @color/plum
48 | - @color/gainsboro
49 | - @color/crimson
50 | - @color/palevioletred
51 | - @color/goldenrod
52 | - @color/orchid
53 | - @color/thistle
54 | - @color/lightgrey
55 | - @color/chocolate
56 | - @color/peru
57 | - @color/indianred
58 | - @color/mediumvioletred
59 | - @color/silver
60 | - @color/darkkhaki
61 | - @color/rosybrown
62 | - @color/mediumorchid
63 | - @color/darkgoldenrod
64 | - @color/firebrick
65 | - @color/powderblue
66 | - @color/lightsteelblue
67 | - @color/paleturquoise
68 | - @color/greenyellow
69 | - @color/lightblue
70 | - @color/darkgray
71 | - @color/brown
72 | - @color/sienna
73 | - @color/yellowgreen
74 | - @color/lightgreen
75 | - @color/darkseagreen
76 | - @color/olive
77 | - @color/aquamarine
78 | - @color/chartreuse
79 | - @color/mediumslateblue
80 | - @color/lightslategray
81 | - @color/slategray
82 | - @color/olivedrab
83 | - @color/mediumaquamarine
84 | - @color/cornflowerblue
85 | - @color/cadetblue
86 | - @color/mediumturquoise
87 | - @color/darkslateblue
88 | - @color/steelblue
89 | - @color/royalblue
90 | - @color/turquoise
91 | - @color/mediumseagreen
92 | - @color/limegreen
93 | - @color/darkslategray
94 | - @color/seagreen
95 | - @color/lightseagreen
96 | - @color/dodgerblue
97 | - @color/cyan
98 | - @color/mediumspringgreen
99 | - @color/darkturquoise
100 | - @color/deepskyblue
101 | - @color/darkcyan
102 | - @color/teal
103 | - @color/blue
104 | - @color/mediumblue
105 |
106 |
--------------------------------------------------------------------------------
/app/src/main/res/values/attr.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
11 |
12 | #FFFFF0
13 | #FFFFE0
14 | #FFFF00
15 | #FFFAFA
16 | #FFFAF0
17 | #FFFACD
18 | #FFF8DC
19 | #FFF5EE
20 | #FFF0F5
21 | #FFEFD5
22 | #FFEBCD
23 | #FFE4E1
24 | #FFE4C4
25 | #FFE4B5
26 | #FFDEAD
27 | #FFDAB9
28 | #FFD700
29 | #FFC0CB
30 | #FFB6C1
31 | #FFA500
32 | #FFA07A
33 | #FF8C00
34 | #FF7F50
35 | #FF69B4
36 | #FF6347
37 | #FF4500
38 | #FF1493
39 | #FF00FF
40 | #FF00FF
41 | #FF0000
42 | #FDF5E6
43 | #FAFAD2
44 | #FAF0E6
45 | #FAEBD7
46 | #FA8072
47 | #F8F8FF
48 | #F5FFFA
49 | #F5F5F5
50 | #F5F5DC
51 | #F5DEB3
52 | #F4A460
53 | #F0FFFF
54 | #F0FFF0
55 | #F0F8FF
56 | #F0E68C
57 | #F08080
58 | #EEE8AA
59 | #EE82EE
60 | #E9967A
61 | #E6E6FA
62 | #E0FFFF
63 | #DEB887
64 | #DDA0DD
65 | #DCDCDC
66 | #DC143C
67 | #DB7093
68 | #DAA520
69 | #DA70D6
70 | #D8BFD8
71 | #D3D3D3
72 | #D2B48C
73 | #D2691E
74 | #CD853F
75 | #CD5C5C
76 | #C71585
77 | #C0C0C0
78 | #BDB76B
79 | #BC8F8F
80 | #BA55D3
81 | #B8860B
82 | #B22222
83 | #B0E0E6
84 | #B0C4DE
85 | #AFEEEE
86 | #ADFF2F
87 | #ADD8E6
88 | #A9A9A9
89 | #A52A2A
90 | #A0522D
91 | #9ACD32
92 | #9932CC
93 | #98FB98
94 | #9400D3
95 | #9370DB
96 | #90EE90
97 | #8FBC8F
98 | #8B4513
99 | #8B008B
100 | #8B0000
101 | #8A2BE2
102 | #87CEFA
103 | #87CEEB
104 | #808080
105 | #808000
106 | #800080
107 | #800000
108 | #7FFFD4
109 | #7FFF00
110 | #7CFC00
111 | #7B68EE
112 | #778899
113 | #708090
114 | #6B8E23
115 | #6A5ACD
116 | #696969
117 | #66CDAA
118 | #6495ED
119 | #5F9EA0
120 | #556B2F
121 | #4B0082
122 | #48D1CC
123 | #483D8B
124 | #4682B4
125 | #4169E1
126 | #40E0D0
127 | #3CB371
128 | #32CD32
129 | #2F4F4F
130 | #2E8B57
131 | #228B22
132 | #20B2AA
133 | #1E90FF
134 | #191970
135 | #00FFFF
136 | #00FFFF
137 | #00FF7F
138 | #00FF00
139 | #00FA9A
140 | #00CED1
141 | #00BFFF
142 | #008B8B
143 | #008080
144 | #008000
145 | #006400
146 | #0000FF
147 | #0000CD
148 | #00008B
149 | #000080
150 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 80dp
4 | 32dp
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Drawing
3 |
4 | Hello blank fragment
5 | Clear All
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
25 |
--------------------------------------------------------------------------------
/app/src/test/java/com/redhoodhan/drawing/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.redhoodhan.drawing
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | buildscript {
3 | repositories {
4 | google()
5 | mavenCentral()
6 | maven {url 'https://jitpack.io'}
7 | }
8 | dependencies {
9 | classpath "com.android.tools.build:gradle:7.0.2"
10 | classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10'
11 |
12 | classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1'
13 | // NOTE: Do not place your application dependencies here; they belong
14 | // in the individual module build.gradle files
15 | }
16 | }
17 |
18 | task clean(type: Delete) {
19 | delete rootProject.buildDir
20 | }
--------------------------------------------------------------------------------
/draw/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/draw/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'org.jetbrains.kotlin.android'
4 | id 'maven-publish'
5 | }
6 |
7 | android {
8 | namespace = 'com.redhoodhan.draw'
9 |
10 | compileSdk 32
11 |
12 | defaultConfig {
13 | minSdk 21
14 | targetSdk 32
15 |
16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
17 | consumerProguardFiles "consumer-rules.pro"
18 | }
19 |
20 | buildTypes {
21 | release {
22 | minifyEnabled false
23 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
24 | }
25 | }
26 | compileOptions {
27 | sourceCompatibility JavaVersion.VERSION_1_8
28 | targetCompatibility JavaVersion.VERSION_1_8
29 | }
30 | kotlinOptions {
31 | jvmTarget = '1.8'
32 | }
33 | }
34 |
35 | dependencies {
36 |
37 | implementation 'androidx.core:core-ktx:1.7.0'
38 | implementation 'androidx.appcompat:appcompat:1.4.1'
39 | implementation 'com.google.android.material:material:1.6.0'
40 | testImplementation 'junit:junit:4.13.2'
41 | androidTestImplementation 'androidx.test.ext:junit:1.1.3'
42 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
43 | }
44 |
45 | afterEvaluate {
46 | publishing {
47 | publications {
48 | release(MavenPublication) {
49 | from components.release
50 |
51 | groupId = 'com.github.DonghanX'
52 | artifactId = 'Draw'
53 | version = '1.0.0'
54 | }
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/draw/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/draw/consumer-rules.pro
--------------------------------------------------------------------------------
/draw/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
--------------------------------------------------------------------------------
/draw/src/androidTest/java/com/redhoodhan/draw/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.redhoodhan.draw
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.redhoodhan.draw.test", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/draw/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/draw/src/main/java/com/redhoodhan/draw/DrawView.kt:
--------------------------------------------------------------------------------
1 | package com.redhoodhan.draw
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.content.res.TypedArray
6 | import android.graphics.*
7 | import android.util.AttributeSet
8 | import android.util.Log
9 | import android.view.MotionEvent
10 | import android.view.VelocityTracker
11 | import android.view.View
12 | import com.redhoodhan.draw.draw_option.DrawOptionContext
13 | import com.redhoodhan.draw.draw_option.data.*
14 | import com.redhoodhan.draw.extension.drawWithBlendLayer
15 | import kotlin.math.abs
16 | import kotlin.math.log10
17 |
18 |
19 | class DrawView @JvmOverloads constructor(
20 | context: Context,
21 | attributeSet: AttributeSet?,
22 | defStyle: Int = 0
23 | ) : View(context, attributeSet, defStyle) {
24 |
25 | // // Double buffering canvas
26 | // private var bufferCanvas: Canvas? = null
27 | //
28 | // // Double buffering bitmap
29 | // private var bufferBitmap: Bitmap? = null
30 |
31 | private var drawOptionContext: DrawOptionContext
32 |
33 | // Path of the current draw
34 | private var drawPath = DrawPath()
35 |
36 | // Paint options for the current draw
37 | private var drawPaintOption = PaintOption(Paint())
38 |
39 | // Coordinate x for the last move event
40 | private var curX = 0f
41 |
42 | // Coordinate y for the last move event
43 | private var curY = 0f
44 |
45 | // Boolean flag to determine the background resource type
46 | private var isCanvasBackgroundImg = false
47 |
48 | private var isCanvasBackgroundChanged = false
49 |
50 | private var _lineType: LineType = LineType.SOLID
51 | set(value) {
52 | if (field == value) {
53 | return
54 | }
55 | field = value
56 | modifyLineOptions(value)
57 | }
58 |
59 | private var brushType: BrushType = BrushType.NORMAL
60 |
61 | private var _brushSize = DrawConst.DEFAULT_STROKE_WIDTH
62 | set(value) {
63 | field = if (value <= DrawConst.DEFAULT_MIN_STROKE_WIDTH) {
64 | DrawConst.DEFAULT_MIN_STROKE_WIDTH
65 | } else {
66 | value
67 | }
68 | changeStrokeWidthSrc(false)
69 | updatePathEffect()
70 | }
71 |
72 | private var _eraserSize = DrawConst.DEFAULT_STROKE_WIDTH
73 | set(value) {
74 | field = if (value <= DrawConst.DEFAULT_MIN_STROKE_WIDTH) {
75 | DrawConst.DEFAULT_MIN_STROKE_WIDTH
76 | } else {
77 | value
78 | }
79 | changeStrokeWidthSrc(true)
80 | }
81 |
82 | private var velocityTracker: VelocityTracker? = null
83 |
84 | private var needsWidthBias: Boolean = false
85 |
86 | private var _drawState: DrawViewState? = null
87 | set(value) {
88 | field = value
89 | field?.stateActionCallback = {
90 | onStateNotified()
91 | }
92 |
93 | invalidate()
94 | }
95 |
96 | init {
97 | drawOptionContext = DrawOptionContext()
98 |
99 | _drawState = DrawViewState()
100 |
101 | initDrawPaint()
102 |
103 | initDrawAttrFromTypedArray(attributeSet)
104 | }
105 |
106 | val drawStateRef
107 | get() = _drawState!!
108 |
109 | var brushColor = DrawConst.DEFAULT_PAINT_COLOR
110 | set(value) {
111 | field = value
112 | updateBrushColor(value)
113 | }
114 |
115 | var lineType: LineType = _lineType
116 | set(value) {
117 | field = value
118 | _lineType = value
119 | }
120 | get() = _lineType
121 |
122 | var brushSize = _brushSize
123 | set(value) {
124 | field = value
125 | _brushSize = value
126 | }
127 | get() = _brushSize
128 |
129 | var eraserSize = _eraserSize
130 | set(value) {
131 | field = value
132 | _eraserSize = value
133 | }
134 | get() = _eraserSize
135 |
136 | /**
137 | * Color resource of the drawing canvas background
138 | */
139 | var canvasBackgroundColor = DrawConst.DEFAULT_CANVAS_COLOR
140 | set(value) {
141 | field = value
142 | isCanvasBackgroundChanged = true
143 | isCanvasBackgroundImg = false
144 | invalidate()
145 | }
146 |
147 | /**
148 | * Image resource of the drawing canvas background
149 | *
150 | * Note that canvasBackgroundImgRes is mutually exclusive with [canvasBackgroundColor]
151 | */
152 | var canvasBackgroundImg: Int? = null
153 | set(value) {
154 | field = value
155 | isCanvasBackgroundChanged = true
156 | isCanvasBackgroundImg = (value != null)
157 | invalidate()
158 | }
159 |
160 | /**
161 | * This callback is triggered when the [DrawView] is pressed.
162 | */
163 | var drawViewPressCallback: (() -> Unit)? = null
164 |
165 | /**
166 | * This callback is triggered when there is a state action happened, in order to notify other
167 | * components whether the undo function is available.
168 | *
169 | */
170 | var undoStateCallback: ((isAvailable: Boolean) -> Unit)? = null
171 |
172 | /**
173 | * This callback is triggered when there is a state action happened, in order to notify other
174 | * components whether the redo function is available.
175 | */
176 | var redoStateCallback: ((isAvailable: Boolean) -> Unit)? = null
177 |
178 | /**
179 | * Switches the current brush type to [BrushType.NORMAL] or [BrushType.IMAGE].
180 | *
181 | * Note that we call this function as we want to draw something with certain type of brush,
182 | * instead of erasing our previous work. Thus, the eraser mode should be switched off first to
183 | * reset XferMode of the corresponding [Paint].
184 | */
185 | // fun switchBrushType(brushType: BrushType) {
186 | // _isEraserOn = false
187 | // // TODO: Complete BrushType switching logic
188 | // // Note that this should be done after the actual drawing function is completed
189 | // }
190 |
191 | fun undo() {
192 | drawStateRef.undo()
193 | invalidate()
194 | }
195 |
196 | fun redo() {
197 | drawStateRef.redo()
198 | invalidate()
199 | }
200 |
201 | fun clearCanvas(needsSaving: Boolean) {
202 | with(drawStateRef) {
203 | if (needsSaving) {
204 | clearCanvasWithSaving()
205 | } else {
206 | clearCanvas()
207 | }
208 | }
209 | invalidate()
210 | }
211 |
212 | /**
213 | * Gets a bitmap that stores the content in the current drawing canvas.
214 | *
215 | */
216 | fun saveAsBitmap(): Bitmap {
217 | val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
218 |
219 | Canvas(bitmap).also {
220 |
221 | draw(it)
222 | }
223 |
224 | return bitmap
225 | }
226 |
227 | fun clearCallback() {
228 | drawViewPressCallback = null
229 | undoStateCallback = null
230 | redoStateCallback = null
231 |
232 | drawStateRef.clearCallback()
233 | }
234 |
235 | /**
236 | * Initial setting of the default brush [BrushType.NORMAL], with line type [LineType.SOLID]
237 | */
238 | private fun initDrawPaint() {
239 | drawPaintOption.paint.apply {
240 | color = Color.BLACK
241 | strokeWidth = DrawConst.DEFAULT_STROKE_WIDTH
242 | isAntiAlias = true
243 | style = Paint.Style.STROKE
244 | strokeJoin = Paint.Join.ROUND
245 | strokeCap = Paint.Cap.ROUND
246 | }
247 | }
248 |
249 | private fun initDrawAttrFromTypedArray(attributeSet: AttributeSet?) {
250 | context.theme.obtainStyledAttributes(
251 | attributeSet,
252 | R.styleable.DrawView,
253 | 0,
254 | 0
255 | ).apply {
256 | try {
257 | brushColor = getColor(R.styleable.DrawView_defaultBrushColor, brushColor)
258 | brushSize = getFloat(R.styleable.DrawView_defaultBrushSize, brushSize)
259 | eraserSize = getFloat(R.styleable.DrawView_defaultEraserSize, eraserSize)
260 |
261 | handleDefaultBackgroundSetting(this)
262 | } finally {
263 | recycle()
264 | }
265 | }
266 | }
267 |
268 | /**
269 | * This function is called to set the canvas background according to the custom styleable
270 | * attribute. If the [R.styleable.DrawView_defaultCanvasBackgroundColor] and the
271 | * [R.styleable.DrawView_defaultCanvasBackgroundImageRes] are both provided in the XML layouts, then
272 | * the [canvasBackgroundImg] will be set to high priority and the [canvasBackgroundColor] will
273 | * be ignored.
274 | */
275 | private fun handleDefaultBackgroundSetting(typedArray: TypedArray) {
276 | val flag = DrawConst.BACKGROUND_ATTR_NOT_SET
277 |
278 | val attrBackgroundColor =
279 | typedArray.getColor(R.styleable.DrawView_defaultCanvasBackgroundColor, flag)
280 | val attrBackgroundImgRes =
281 | typedArray.getResourceId(R.styleable.DrawView_defaultCanvasBackgroundImageRes, flag)
282 |
283 | // TODO: Refactor
284 | when {
285 | attrBackgroundColor == flag && attrBackgroundImgRes == flag -> return
286 |
287 | attrBackgroundImgRes != flag -> {
288 | post { canvasBackgroundImg = attrBackgroundImgRes }
289 | }
290 | attrBackgroundColor != flag && attrBackgroundImgRes == flag -> {
291 | post { canvasBackgroundColor = attrBackgroundColor }
292 | }
293 | }
294 |
295 | }
296 |
297 | /**
298 | * This function is called when the [DrawViewState.stateActionCallback] is triggered and triggers
299 | * the callbacks that passes the Boolean flag of corresponding state, by accessing the attribute
300 | * in the [DrawViewState].
301 | *
302 | * The callbacks to be triggered are as follows:
303 | * 1. [undoStateCallback]
304 | * 2. [redoStateCallback]
305 | */
306 | private fun onStateNotified() {
307 | undoStateCallback?.invoke(drawStateRef.isUndoAvailable)
308 |
309 | redoStateCallback?.invoke(drawStateRef.isRedoAvailable)
310 | }
311 |
312 | private fun modifyLineOptions(lineType: LineType) {
313 | val isEraserOn = lineType == LineType.ERASER
314 | val curSize = changeStrokeWidthSrc(isEraserOn)
315 |
316 | // Resets the corresponding brushType
317 | brushType = lineType.getBrushType()
318 |
319 | drawOptionContext.let {
320 | it.changeStrategyByLineType(lineType)
321 | it.updateOptionWhenSwitchingLineType(drawPaintOption, curSize)
322 | }
323 |
324 | updateVelocityTrackerStates(lineType)
325 | }
326 |
327 | /**
328 | * This function is called to update flags that indicates whether certain drawing property
329 | * correlates to the velocity of [MotionEvent.ACTION_MOVE].
330 | *
331 | * Note that If the current [lineType] is not velocity-invariant, then we clear the reference of
332 | * the velocityTracker in case of the [VelocityTracker] object is not cleared when processing
333 | * the [MotionEvent.ACTION_UP].
334 | *
335 | */
336 | private fun updateVelocityTrackerStates(lineType: LineType) {
337 | needsWidthBias = lineType.isVelocityVariant()
338 |
339 | if (!needsWidthBias) {
340 | clearVelocityTrackerRef()
341 | }
342 | }
343 |
344 | private fun updateBrushColor(color: Int) {
345 | drawOptionContext.updateBrushColor(drawPaintOption, color)
346 | }
347 |
348 | private fun updatePathEffect() {
349 | drawOptionContext.updatePathEffect(drawPaintOption, brushSize)
350 | }
351 |
352 | /**
353 | * This function is called to change the brush size source according to whether the eraser is
354 | * turned on.
355 | * Note that the brush size of eraser is individually independent.
356 | */
357 | private fun changeStrokeWidthSrc(isEraserOn: Boolean): Float {
358 | val curSize = if (isEraserOn) {
359 | eraserSize
360 | } else {
361 | brushSize
362 | }
363 |
364 | drawOptionContext.updateBrushSize(drawPaintOption, curSize)
365 |
366 | return curSize
367 | }
368 |
369 |
370 | override fun onDraw(canvas: Canvas) {
371 | setBackground(canvas)
372 |
373 | canvas.drawWithBlendLayer {
374 | drawPrevious(it)
375 | drawCurrent(it)
376 | }
377 |
378 | super.onDraw(canvas)
379 | }
380 |
381 | /**
382 | *
383 | * Note that this is really an expensive operation if we call this function in [onDraw], because
384 | * [onDraw] is called frequently to redraw the previous and the current paths.
385 | *
386 | * One possible optimizing solution is using the SurfaceView to operate the background drawing
387 | * process.
388 | *
389 | * Another possible optimizing solution might be using Double Buffering technique.
390 | */
391 | private fun setBackground(canvas: Canvas) {
392 |
393 | if (isCanvasBackgroundImg && isCanvasBackgroundChanged) {
394 | canvasBackgroundImg?.let {
395 | setBackgroundResource(it)
396 | }
397 | isCanvasBackgroundChanged = false
398 | } else if (isCanvasBackgroundChanged) {
399 | // Removes previous background
400 | setBackgroundResource(0)
401 | // draw the pure color background
402 | canvas.drawColor(canvasBackgroundColor)
403 | }
404 |
405 | }
406 |
407 | private fun drawCurrent(canvas: Canvas) {
408 | canvas.drawPath(drawPath, drawPaintOption.paint)
409 | }
410 |
411 | private fun drawPrevious(canvas: Canvas) {
412 | if (drawStateRef.prevSize == 0) {
413 | return
414 | }
415 |
416 | for (index in 0 until drawStateRef.prevSize) {
417 | canvas.drawPath(
418 | drawStateRef.getPrevPathWithIndexed(index),
419 | drawStateRef.getPrevPaintWithIndexed(index)
420 | )
421 | }
422 |
423 | }
424 |
425 | @SuppressLint("ClickableViewAccessibility")
426 | override fun onTouchEvent(event: MotionEvent): Boolean {
427 | val touchX = event.x
428 | val touchY = event.y
429 |
430 | // Intercepts the erasing process if the canvas is empty, thus eliminating unnecessary
431 | // recordings of the eraser path and paint.
432 | if (drawStateRef.isPathEmpty && lineType == LineType.ERASER) {
433 | return true
434 | }
435 |
436 | if (velocityTracker == null && needsWidthBias) {
437 | velocityTracker = VelocityTracker.obtain()
438 | }
439 | velocityTracker?.addMovement(event)
440 |
441 | when (event.action) {
442 | MotionEvent.ACTION_DOWN -> {
443 | drawViewPressCallback?.invoke()
444 | performActionDown(touchX, touchY)
445 | }
446 | MotionEvent.ACTION_MOVE -> {
447 | performActionMove(touchX, touchY)
448 | }
449 | MotionEvent.ACTION_UP -> {
450 | performActionUp()
451 | }
452 | else -> return false
453 | }
454 |
455 | invalidate()
456 |
457 | return true
458 | }
459 |
460 | /**
461 | * Resets the DrawPath object and records the starting coordinates of the following "trail".
462 | *
463 | * Note that this function is called when [MotionEvent.ACTION_DOWN] is intercepted
464 | */
465 | private fun performActionDown(touchX: Float, touchY: Float) {
466 | // Sets the beginning of the drawing contour to the touched points
467 | drawPath.reset()
468 | drawPath.moveTo(touchX, touchY)
469 |
470 | curX = touchX
471 | curY = touchY
472 | }
473 |
474 | /**
475 | * Quads the [drawPath] based on the coordinates of the last move and the coordinates of the
476 | * current move.
477 | *
478 | * Note that this function is called when [MotionEvent.ACTION_MOVE] is intercepted.
479 | */
480 | private fun performActionMove(touchX: Float, touchY: Float) {
481 | // Utilizes quadratic bezier fitting to smoothen the drawing path curve
482 | drawPath.quadTo(
483 | curX,
484 | curY,
485 | (curX + touchX) / 2,
486 | (curY + touchY) / 2
487 | )
488 | // Updates the coordinates of the last move
489 | curX = touchX
490 | curY = touchY
491 |
492 | // Computes the velocity of moving event based on time unit of 1000ms and calculates the
493 | // bias.
494 | velocityTracker?.let {
495 | it.computeCurrentVelocity(1000)
496 | drawPaintOption.strokeWidthBias = obtainBiasByVelocity(it.xVelocity, it.yVelocity)
497 | }
498 | }
499 |
500 | /**
501 | * Generates bias value of stroke width to simulate "Signing Pen" effect.
502 | *
503 | * Note that the minimum value of the bias is 0, which corresponds to zero velocity. If we do not
504 | * set any upper bound limit, the maximum value of the bias will be log10([Float.MAX_VALUE]),
505 | * which is equal to 38.5318394234 and literally not suitable for drawing as signing pen.
506 | *
507 | * Thus, we set the maximum bias value as [DrawConst.MAX_STROKE_WIDTH_BIAS].
508 | */
509 | private fun obtainBiasByVelocity(
510 | velocityX: Float,
511 | velocityY: Float,
512 | max: Float = DrawConst.MAX_STROKE_WIDTH_BIAS
513 | ): Float {
514 | log10(1F + abs(velocityX.coerceAtLeast(velocityY))).also {
515 | return if (it >= max) {
516 | max
517 | } else {
518 | it
519 | }
520 | }
521 | }
522 |
523 | /**
524 | * Adds the [drawPath] and the [drawPaintOption] to the stored lists respectively when the current
525 | * drawing process is ended.
526 | *
527 | * The lists will be used to retrieve the previous [drawPath] and the [drawPaintOption] when the
528 | * function [drawPrevious] is called.
529 | *
530 | * Note that this function is called when [MotionEvent.ACTION_UP] is intercepted.
531 | */
532 | private fun performActionUp() {
533 | drawStateRef.addToPrev(drawPath, drawPaintOption.paint)
534 |
535 | // Prepares for next draw. Also prevents the drawCurrent function from drawing the same
536 | // path again and again when the onDraw function is called.
537 | drawPath = DrawPath()
538 |
539 | drawPaintOption = PaintOption(Paint(drawPaintOption.paint))
540 |
541 | clearVelocityTrackerRef()
542 | }
543 |
544 | /**
545 | * Releases the VelocityTracker reference and returns the object back.
546 | */
547 | private fun clearVelocityTrackerRef() {
548 | velocityTracker?.let {
549 | it.recycle()
550 | velocityTracker = null
551 | }
552 | }
553 |
554 |
555 | }
--------------------------------------------------------------------------------
/draw/src/main/java/com/redhoodhan/draw/DrawViewState.kt:
--------------------------------------------------------------------------------
1 | package com.redhoodhan.draw
2 |
3 | import android.graphics.Paint
4 | import android.graphics.Path
5 |
6 |
7 | class DrawViewState {
8 |
9 | private val prevDrawPath = mutableListOf()
10 | private val prevDrawPaint = mutableListOf()
11 |
12 | private val undoneDrawPath = mutableListOf()
13 | private val undoneDrawPaint = mutableListOf()
14 |
15 | /**
16 | * Returns the size of list that contains all the previous [Path]. This is used to check if
17 | * we can perform any undo action.
18 | */
19 | val prevSize: Int
20 | get() = prevDrawPath.size
21 |
22 | val isPathEmpty: Boolean
23 | get() = (prevSize == 0)
24 |
25 | /**
26 | * Returns the size of [undoneDrawPath]. This is used to check if we can perform any redo
27 | * action.
28 | */
29 | val undoneSize: Int
30 | get() = undoneDrawPath.size
31 |
32 | val isUndoAvailable
33 | get() = (prevSize > 0)
34 |
35 | val isRedoAvailable
36 | get() = (undoneSize > 0)
37 |
38 | /**
39 | * This callback is triggered in three cases in which we may want to notify other components:
40 | *
41 | * 1. New paths and paints are added to the corresponding "prevList", which means the user
42 | * drew something on the [DrawView].
43 | *
44 | * 2. User performs undo action.
45 | *
46 | * 3. User performs redo action.
47 | */
48 | var stateActionCallback: (() -> Unit)? = null
49 |
50 | fun addToPrev(path: Path, paint: Paint) {
51 | prevDrawPath.add(path)
52 | prevDrawPaint.add(paint)
53 |
54 | // Clear undo list to make redo unavailable
55 | clearUndoList()
56 |
57 | stateActionCallback?.invoke()
58 | }
59 |
60 | fun getPrevPathWithIndexed(index: Int): Path =
61 | prevDrawPath[index]
62 |
63 | fun getPrevPaintWithIndexed(index: Int): Paint =
64 | prevDrawPaint[index]
65 |
66 | /**
67 | * Performs undo actions by removing the last [Path] and [Paint] objects in the [prevDrawPath]
68 | * list and the [prevDrawPaint] list and adding them to the corresponding undone lists. Thus,
69 | * when calling the [DrawView.invalidate] function, the canvas should draw all the previous paths
70 | * except for the certain path we recalled before.
71 | */
72 | fun undo() {
73 | prevDrawPath.removeLastOrNull()?.let {
74 | undoneDrawPath.add(it)
75 | }
76 | prevDrawPaint.removeLastOrNull()?.let {
77 | undoneDrawPaint.add(it)
78 | }
79 |
80 | stateActionCallback?.invoke()
81 | }
82 |
83 | fun redo() {
84 | undoneDrawPath.removeLastOrNull()?.let {
85 | prevDrawPath.add(it)
86 | }
87 | undoneDrawPaint.removeLastOrNull()?.let {
88 | prevDrawPaint.add(it)
89 | }
90 |
91 | stateActionCallback?.invoke()
92 | }
93 |
94 | /**
95 | * TODO: the paths cleared should have ability of being retrieved at once simultaneously.
96 | * Clears canvas and stores all the [prevDrawPath] and [prevDrawPaint] to the corresponding
97 | * undo list.
98 | */
99 | fun clearCanvasWithSaving() {
100 | with(undoneDrawPath) {
101 | clear()
102 | addAll(prevDrawPath)
103 | prevDrawPath.clear()
104 | }
105 |
106 | with(undoneDrawPaint) {
107 | clear()
108 | addAll(prevDrawPaint)
109 | prevDrawPaint.clear()
110 | }
111 |
112 | stateActionCallback?.invoke()
113 | }
114 |
115 | fun clearCanvas() {
116 | prevDrawPath.clear()
117 | prevDrawPaint.clear()
118 | undoneDrawPath.clear()
119 | undoneDrawPaint.clear()
120 |
121 | stateActionCallback?.invoke()
122 | }
123 |
124 | fun clearCallback() {
125 | stateActionCallback = null
126 | }
127 |
128 | private fun clearUndoList() {
129 | undoneDrawPath.clear()
130 | undoneDrawPaint.clear()
131 | }
132 |
133 | }
--------------------------------------------------------------------------------
/draw/src/main/java/com/redhoodhan/draw/draw_option/BaseDrawOptionStrategy.kt:
--------------------------------------------------------------------------------
1 | package com.redhoodhan.draw.draw_option
2 |
3 | import android.graphics.Paint
4 | import com.redhoodhan.draw.draw_option.data.DrawConst
5 | import com.redhoodhan.draw.draw_option.data.PaintOption
6 | import com.redhoodhan.draw.draw_option.data.LineType
7 |
8 | /**
9 | * This base strategy class implements the default actions of updating the property in the
10 | * [PaintOption], following the actions of [LineType.SOLID]
11 | */
12 | class BaseDrawOptionStrategy : DrawOptionStrategy {
13 | override fun updateAlpha(paintOption: PaintOption) {
14 | paintOption.paint.alpha = DrawConst.MAX_ALPHA
15 | }
16 |
17 | override fun updateBrushColor(paintOption: PaintOption, color: Int) {
18 | paintOption.paint.color = color
19 | }
20 |
21 | override fun updateStrokeStyle(paintOption: PaintOption) {
22 | paintOption.paint.apply {
23 | strokeCap = Paint.Cap.ROUND
24 | strokeJoin = Paint.Join.ROUND
25 | }
26 | }
27 |
28 | override fun updateBrushSize(paintOption: PaintOption, brushSize: Float) {
29 | paintOption.paint.strokeWidth = brushSize
30 | }
31 |
32 | override fun updatePathEffect(paintOption: PaintOption, brushSize: Float?) {
33 | paintOption.paint.pathEffect = null
34 | }
35 |
36 | override fun updateXferMode(paintOption: PaintOption, brushSize: Float) {
37 | paintOption.paint.xfermode = null
38 | updateBrushSize(paintOption, brushSize)
39 | }
40 | }
--------------------------------------------------------------------------------
/draw/src/main/java/com/redhoodhan/draw/draw_option/ChiselTipLineStrategy.kt:
--------------------------------------------------------------------------------
1 | package com.redhoodhan.draw.draw_option
2 |
3 | import android.graphics.Paint
4 | import com.redhoodhan.draw.draw_option.data.DrawConst
5 | import com.redhoodhan.draw.draw_option.data.PaintOption
6 |
7 | class ChiselTipLineStrategy(private val baseStrategy: BaseDrawOptionStrategy) :
8 | DrawOptionStrategy by baseStrategy {
9 |
10 | override fun updateAlpha(paintOption: PaintOption) {
11 | paintOption.paint.alpha = DrawConst.CHISEL_ALPHA
12 | }
13 |
14 | override fun updateStrokeStyle(paintOption: PaintOption) {
15 | paintOption.paint.apply {
16 | strokeCap = Paint.Cap.SQUARE
17 | strokeJoin = Paint.Join.BEVEL
18 | }
19 | }
20 |
21 | override fun updateBrushColor(paintOption: PaintOption, color: Int) {
22 | baseStrategy.updateBrushColor(paintOption, color)
23 | // Retrieves the alpha value overridden by setting color
24 | updateAlpha(paintOption)
25 | }
26 | }
--------------------------------------------------------------------------------
/draw/src/main/java/com/redhoodhan/draw/draw_option/DashedLineStrategy.kt:
--------------------------------------------------------------------------------
1 | package com.redhoodhan.draw.draw_option
2 |
3 | import com.redhoodhan.draw.draw_option.data.PaintOption
4 | import com.redhoodhan.draw.draw_option.data.LineType
5 | import com.redhoodhan.draw.path_effect.LineDashPathEffect
6 |
7 |
8 | class DashedLineStrategy(private val baseStrategy: BaseDrawOptionStrategy) :
9 | DrawOptionStrategy by baseStrategy {
10 |
11 | /**
12 | * Note that we will need the [brushSize] to specify the intervals and phase of the dash path
13 | * when we switch to the [LineType.DASH].
14 | */
15 | override fun updatePathEffect(paintOption: PaintOption, brushSize: Float?) {
16 | requireNotNull(brushSize)
17 |
18 | paintOption.paint.pathEffect = LineDashPathEffect(brushSize)
19 | }
20 |
21 | override fun updateBrushSize(paintOption: PaintOption, brushSize: Float) {
22 | baseStrategy.updateBrushSize(paintOption, brushSize)
23 |
24 | updatePathEffect(paintOption, brushSize)
25 | }
26 | }
--------------------------------------------------------------------------------
/draw/src/main/java/com/redhoodhan/draw/draw_option/DrawOptionContext.kt:
--------------------------------------------------------------------------------
1 | package com.redhoodhan.draw.draw_option
2 |
3 | import android.util.Log
4 | import com.redhoodhan.draw.draw_option.data.LineType
5 | import com.redhoodhan.draw.draw_option.data.PaintOption
6 |
7 | class DrawOptionContext() {
8 |
9 | private var baseStrategy = BaseDrawOptionStrategy()
10 | private var strategy: DrawOptionStrategy
11 |
12 | init {
13 | strategy = baseStrategy
14 | }
15 |
16 | fun changeStrategyByLineType(lineType: LineType) {
17 | strategy = when (lineType) {
18 | LineType.DASH -> DashedLineStrategy(baseStrategy)
19 | LineType.CHISEL -> ChiselTipLineStrategy(baseStrategy)
20 | LineType.ERASER -> EraserLineStrategy(baseStrategy)
21 | else -> baseStrategy
22 | }
23 | }
24 |
25 | fun updateAlpha(paintOption: PaintOption) {
26 | strategy.updateAlpha(paintOption)
27 | }
28 |
29 | fun updateBrushColor(paintOption: PaintOption, color: Int) {
30 | strategy.updateBrushColor(paintOption, color)
31 | }
32 |
33 | fun updateStrokeStyle(paintOption: PaintOption) {
34 | strategy.updateStrokeStyle(paintOption)
35 | }
36 |
37 | fun updatePathEffect(paintOption: PaintOption, brushSize: Float? = null) {
38 | strategy.updatePathEffect(paintOption, brushSize)
39 | }
40 |
41 | fun updateBrushSize(paintOption: PaintOption, brushSize: Float) {
42 | strategy.updateBrushSize(paintOption, brushSize)
43 | }
44 |
45 | fun updateXferMode(paintOption: PaintOption, brushSize: Float) {
46 | strategy.updateXferMode(paintOption, brushSize)
47 | }
48 |
49 | fun updateOptionWhenSwitchingLineType(paintOption: PaintOption, brushSize: Float) {
50 | updateXferMode(paintOption, brushSize)
51 |
52 | updatePathEffect(paintOption, brushSize)
53 |
54 | updateAlpha(paintOption)
55 |
56 | updateStrokeStyle(paintOption)
57 | }
58 |
59 | }
--------------------------------------------------------------------------------
/draw/src/main/java/com/redhoodhan/draw/draw_option/DrawOptionStrategy.kt:
--------------------------------------------------------------------------------
1 | package com.redhoodhan.draw.draw_option
2 |
3 | import android.graphics.Paint
4 | import com.redhoodhan.draw.draw_option.data.PaintOption
5 |
6 | interface DrawOptionStrategy {
7 |
8 | fun updateAlpha(paintOption: PaintOption)
9 |
10 | fun updateBrushColor(paintOption: PaintOption, color: Int)
11 |
12 | fun updateStrokeStyle(paintOption: PaintOption)
13 |
14 | fun updatePathEffect(paintOption: PaintOption, brushSize: Float? = null)
15 |
16 | fun updateBrushSize(paintOption: PaintOption, brushSize: Float)
17 |
18 | fun updateXferMode(paintOption: PaintOption, brushSize: Float)
19 | }
--------------------------------------------------------------------------------
/draw/src/main/java/com/redhoodhan/draw/draw_option/EraserLineStrategy.kt:
--------------------------------------------------------------------------------
1 | package com.redhoodhan.draw.draw_option
2 |
3 | import android.graphics.PorterDuff
4 | import android.graphics.PorterDuffXfermode
5 | import com.redhoodhan.draw.DrawViewState
6 | import com.redhoodhan.draw.draw_option.data.PaintOption
7 |
8 | /**
9 | * Note that the eraser mode is really just a NORMAL and SOLID brush with its XferMode set to
10 | * [PorterDuff.Mode.CLEAR], so that we can keep track of the drawing paths of the eraser without
11 | * introducing extra lists rather than the previous lists we defined in the [DrawViewState].
12 | */
13 | class EraserLineStrategy(private val baseStrategy: BaseDrawOptionStrategy) :
14 | DrawOptionStrategy by baseStrategy {
15 |
16 | override fun updateXferMode(paintOption: PaintOption, brushSize: Float) {
17 | baseStrategy.updateBrushSize(paintOption, brushSize)
18 |
19 | paintOption.paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
20 | }
21 | }
--------------------------------------------------------------------------------
/draw/src/main/java/com/redhoodhan/draw/draw_option/data/BrushType.kt:
--------------------------------------------------------------------------------
1 | package com.redhoodhan.draw.draw_option.data
2 |
3 |
4 | enum class BrushType {
5 | // Note that the NORMAL brush type includes "Eraser", "Solid" and "Dashed"
6 | NORMAL,
7 | IMAGE
8 | }
--------------------------------------------------------------------------------
/draw/src/main/java/com/redhoodhan/draw/draw_option/data/DrawConst.kt:
--------------------------------------------------------------------------------
1 | package com.redhoodhan.draw.draw_option.data
2 |
3 | import android.graphics.Color
4 |
5 | class DrawConst {
6 | companion object {
7 | const val DEFAULT_MIN_STROKE_WIDTH = 3F
8 | const val DEFAULT_STROKE_WIDTH = 15F
9 | const val DEFAULT_PAINT_COLOR = Color.BLACK
10 | const val DEFAULT_CANVAS_COLOR = Color.WHITE
11 | const val MAX_STROKE_WIDTH_BIAS = 5F
12 | const val CHISEL_ALPHA = 80
13 | const val MAX_ALPHA = 255
14 |
15 | const val BACKGROUND_ATTR_NOT_SET = -1
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/draw/src/main/java/com/redhoodhan/draw/draw_option/data/DrawPath.kt:
--------------------------------------------------------------------------------
1 | package com.redhoodhan.draw.draw_option.data
2 |
3 | import android.graphics.Path
4 |
5 | typealias CoordPair = Pair
6 |
7 | data class DrawPath(
8 | val brushType: BrushType = BrushType.NORMAL,
9 | var coordPair: CoordPair? = null
10 | ): Path()
--------------------------------------------------------------------------------
/draw/src/main/java/com/redhoodhan/draw/draw_option/data/LineType.kt:
--------------------------------------------------------------------------------
1 | package com.redhoodhan.draw.draw_option.data
2 |
3 | import android.view.MotionEvent
4 |
5 | enum class LineType {
6 | SOLID {
7 | override fun getBrushType() = BrushType.NORMAL
8 |
9 | override fun isSizeVariant() = false
10 |
11 | override fun isVelocityVariant() = false
12 | },
13 | DASH {
14 | override fun getBrushType() = BrushType.NORMAL
15 |
16 | override fun isSizeVariant() = true
17 |
18 | override fun isVelocityVariant() = false
19 | },
20 | SIGNING {
21 | override fun getBrushType() = BrushType.NORMAL
22 |
23 | override fun isSizeVariant() = false
24 |
25 | override fun isVelocityVariant() = true
26 | },
27 | CHISEL {
28 | override fun getBrushType() = BrushType.NORMAL
29 |
30 | override fun isSizeVariant() = false
31 |
32 | override fun isVelocityVariant() = false
33 | },
34 | ERASER {
35 | override fun getBrushType() = BrushType.NORMAL
36 |
37 | override fun isSizeVariant() = false
38 |
39 | override fun isVelocityVariant() = false
40 | };
41 |
42 | abstract fun getBrushType(): BrushType
43 |
44 | /**
45 | * Determines whether the corresponding effect should be retrieved when we change the brushSize.
46 | */
47 | abstract fun isSizeVariant(): Boolean
48 |
49 | /**
50 | * Determines whether we need to keep track of the velocity of [MotionEvent.ACTION_MOVE].
51 | */
52 | abstract fun isVelocityVariant(): Boolean
53 |
54 | }
--------------------------------------------------------------------------------
/draw/src/main/java/com/redhoodhan/draw/draw_option/data/PaintOption.kt:
--------------------------------------------------------------------------------
1 | package com.redhoodhan.draw.draw_option.data
2 |
3 | import android.graphics.Paint
4 |
5 | data class PaintOption(val paint: Paint, var imageRes: Int? = 0, var strokeWidthBias: Float = 0f)
--------------------------------------------------------------------------------
/draw/src/main/java/com/redhoodhan/draw/extension/CanvasExtension.kt:
--------------------------------------------------------------------------------
1 | package com.redhoodhan.draw.extension
2 |
3 | import android.graphics.Canvas
4 |
5 | /**
6 | * This function is called to prevent the Canvas from being transparent when applying the
7 | * clear PorterDuff Mode, so that the background of the canvas will not be erased when using eraser.
8 | *
9 | * [performDraw] is the callback lambda function to perform the actual drawing process.
10 | */
11 | inline fun Canvas.drawWithBlendLayer(
12 | performDraw: (Canvas) -> Unit,
13 | ) {
14 | val count = saveLayer(
15 | 0F, 0F,
16 | width.toFloat(), height.toFloat(), null
17 | )
18 |
19 | performDraw.invoke(this)
20 |
21 | restoreToCount(count)
22 | }
--------------------------------------------------------------------------------
/draw/src/main/java/com/redhoodhan/draw/path_effect/LineDashPathEffect.kt:
--------------------------------------------------------------------------------
1 | package com.redhoodhan.draw.path_effect
2 |
3 | import android.graphics.DashPathEffect
4 | import java.util.*
5 |
6 | class LineDashPathEffect(val brushSize: Float) :
7 | DashPathEffect(floatArrayOf(brushSize, brushSize * 2, brushSize, brushSize * 3), 0f) {
8 |
9 | override fun equals(other: Any?): Boolean {
10 | return if (this === other) {
11 | true
12 | } else {
13 | (other is LineDashPathEffect) && (other.brushSize == brushSize)
14 | }
15 | }
16 |
17 | override fun hashCode(): Int = Objects.hash(brushSize)
18 |
19 | }
--------------------------------------------------------------------------------
/draw/src/main/res/values/attr.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/draw/src/test/java/com/redhoodhan/draw/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.redhoodhan.draw
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu Apr 14 18:37:19 PDT 2022
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/jitpack.yml:
--------------------------------------------------------------------------------
1 | jdk:
2 | - openjdk11
3 |
--------------------------------------------------------------------------------
/screenshots/demo_with_canvas_background.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DonghanX/DrawView/e905e3af60e9ac93fb3299702e9958b473ba7a9b/screenshots/demo_with_canvas_background.gif
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | dependencyResolutionManagement {
2 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
3 | repositories {
4 | google()
5 | mavenCentral()
6 | }
7 | }
8 | rootProject.name = "Drawing"
9 | include ':app'
10 | include ':draw'
11 |
--------------------------------------------------------------------------------