This commit is contained in:
Kieran BW 2021-08-10 20:50:24 +01:00
parent a0f84c3b0c
commit 8e93b09e05
17 changed files with 129 additions and 88 deletions

View File

@ -12,6 +12,6 @@
</deviceKey> </deviceKey>
</Target> </Target>
</targetSelectedWithDropDown> </targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2021-08-09T19:37:01.560785100Z" /> <timeTargetWasSelectedWithDropDown value="2021-08-10T19:32:45.536947900Z" />
</component> </component>
</project> </project>

3
.idea/misc.xml generated
View File

@ -5,6 +5,9 @@
<map> <map>
<entry key="..\:/Users/Dell/Documents/GitHub/Android.EweSticker/app/src/main/res/drawable/ic_chevron_left.xml" value="0.14635416666666667" /> <entry key="..\:/Users/Dell/Documents/GitHub/Android.EweSticker/app/src/main/res/drawable/ic_chevron_left.xml" value="0.14635416666666667" />
<entry key="..\:/Users/Dell/Documents/GitHub/Android.EweSticker/app/src/main/res/drawable/ic_clock.xml" value="0.14635416666666667" /> <entry key="..\:/Users/Dell/Documents/GitHub/Android.EweSticker/app/src/main/res/drawable/ic_clock.xml" value="0.14635416666666667" />
<entry key="..\:/Users/Dell/Documents/GitHub/Android.EweSticker/app/src/main/res/layout/image_container.xml" value="0.13020833333333334" />
<entry key="..\:/Users/Dell/Documents/GitHub/Android.EweSticker/app/src/main/res/layout/image_container_column.xml" value="0.13020833333333334" />
<entry key="..\:/Users/Dell/Documents/GitHub/Android.EweSticker/app/src/main/res/layout/keyboard_layout.xml" value="0.13020833333333334" />
</map> </map>
</option> </option>
</component> </component>

View File

@ -3,15 +3,17 @@ All major and minor version changes will be documented in this file. Details of
patch-level version changes can be found in [commit messages](../../commits/master). patch-level version changes can be found in [commit messages](../../commits/master).
## 20210810 - 2021/08/10
## (no version) - 2021/08/09
- Code optimisations - Code optimisations
- Code clean-up
- Removed APNG animation due to memory leak
- Linting fixes
- Added caching functionality - Added caching functionality
- to improve performance of fallback stickers - to improve performance of fallback stickers
- to enable addition of recent list - to enable addition of recent list
- Updated gradle - to improve switching packs performance
- Updated gradle and deps
- Add recent icon - Add recent icon
- (In progress ...) testing, cleanup
## 20210723 - 2021/07/23 ## 20210723 - 2021/07/23

View File

@ -11,8 +11,8 @@ android {
applicationId "com.fredhappyface.ewesticker" applicationId "com.fredhappyface.ewesticker"
minSdkVersion 28 minSdkVersion 28
targetSdkVersion 30 targetSdkVersion 30
versionCode 20210723 versionCode 20210810
versionName "2021.07.23" versionName "2021.08.10"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }
@ -39,14 +39,15 @@ android {
dependencies { dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib:1.5.21"
implementation 'androidx.core:core-ktx:1.5.0' implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.appcompat:appcompat:1.3.0' implementation "androidx.fragment:fragment-ktx"
implementation 'com.google.android.material:material:1.3.0' implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.preference:preference-ktx:1.1.1' implementation 'androidx.preference:preference-ktx:1.1.1'
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
implementation "com.github.penfeizhou.android.animation:apng:2.10.0" //implementation "com.github.penfeizhou.android.animation:apng:2.11.0"
} }

View File

@ -8,10 +8,9 @@ import java.util.*
* an element is removed from the start * an element is removed from the start
* *
*/ */
class Cache(size: Int = 30) { class Cache(private val size: Int = 30) {
private var data: LinkedList<String> = LinkedList() private var data: LinkedList<String> = LinkedList()
private val size = size
/** /**
* Logic to add an element * Logic to add an element
@ -31,14 +30,6 @@ class Cache(size: Int = 30) {
} }
/**
* Remove an element
*
* @param elem
*/
fun remove(elem: String) {
data.remove(elem)
}
/** /**
* Get an element * Get an element

View File

@ -7,13 +7,11 @@ import android.graphics.ImageDecoder
import android.graphics.drawable.AnimatedImageDrawable import android.graphics.drawable.AnimatedImageDrawable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.inputmethodservice.InputMethodService import android.inputmethodservice.InputMethodService
import android.util.Log
import android.view.View import android.view.View
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.RelativeLayout
import android.widget.Toast import android.widget.Toast
import androidx.cardview.widget.CardView import androidx.cardview.widget.CardView
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
@ -22,23 +20,24 @@ import androidx.core.view.inputmethod.EditorInfoCompat
import androidx.core.view.inputmethod.InputConnectionCompat import androidx.core.view.inputmethod.InputConnectionCompat
import androidx.core.view.inputmethod.InputContentInfoCompat import androidx.core.view.inputmethod.InputContentInfoCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.github.penfeizhou.animation.apng.APNGDrawable //import com.github.penfeizhou.animation.apng.APNGDrawable
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
import java.util.* import java.util.*
import kotlin.collections.HashMap
class ImageKeyboard : InputMethodService() { class ImageKeyboard : InputMethodService() {
// Attributes // Attributes
private lateinit var supportedMimes: MutableMap<String, String> private lateinit var supportedMimes: MutableMap<String, String>
private var loadedPacks = HashMap<String, StickerPack>() private var loadedPacks = HashMap<String, StickerPack>()
private var imageContainer: LinearLayout? = null private lateinit var imageContainer: LinearLayout
private var packContainer: LinearLayout? = null private lateinit var packContainer: LinearLayout
private lateinit var internalDir: File private lateinit var internalDir: File
// SharedPref // SharedPref
private lateinit var sharedPreferences: SharedPreferences private lateinit var sharedPreferences: SharedPreferences
private var iconsPerRow = 0 private var iconsPerColumn = 0
private var iconSize = 0 private var iconSize = 0
private var disableAnimations = false private var disableAnimations = false
@ -46,6 +45,9 @@ class ImageKeyboard : InputMethodService() {
private var compatCache = Cache() private var compatCache = Cache()
private var recentCache = Cache() private var recentCache = Cache()
// Cache for image container
private var imageContainerCache = HashMap<Int, LinearLayout>()
/** /**
* Adds a back button as a PackCard to keyboard that shows the InputMethodPicker * Adds a back button as a PackCard to keyboard that shows the InputMethodPicker
*/ */
@ -59,7 +61,7 @@ class ImageKeyboard : InputMethodService() {
.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager .getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.showInputMethodPicker() inputMethodManager.showInputMethodPicker()
} }
packContainer!!.addView(packCard) packContainer.addView(packCard)
} }
/** /**
@ -70,11 +72,11 @@ class ImageKeyboard : InputMethodService() {
val recentButton = packCard.findViewById<ImageButton>(R.id.ib3) val recentButton = packCard.findViewById<ImageButton>(R.id.ib3)
val icon = ResourcesCompat.getDrawable(resources, R.drawable.ic_clock, null) val icon = ResourcesCompat.getDrawable(resources, R.drawable.ic_clock, null)
recentButton.setImageDrawable(icon) recentButton.setImageDrawable(icon)
recentButton.setOnClickListener { view: View -> recentButton.setOnClickListener {
imageContainer!!.removeAllViewsInLayout() imageContainer.removeAllViewsInLayout()
recreateImageContainer(recentCache.toFiles()) imageContainer.addView(createImageContainer(recentCache.toFiles()))
} }
packContainer!!.addView(packCard) packContainer.addView(packCard)
} }
@ -89,10 +91,9 @@ class ImageKeyboard : InputMethodService() {
setStickerButtonImage(pack.thumbSticker, packButton) setStickerButtonImage(pack.thumbSticker, packButton)
packButton.tag = pack packButton.tag = pack
packButton.setOnClickListener { view: View -> packButton.setOnClickListener { view: View ->
imageContainer!!.removeAllViewsInLayout() switchImageContainer((view.tag as StickerPack).stickerList)
recreateImageContainer((view.tag as StickerPack).stickerList)
} }
packContainer!!.addView(packCard) packContainer.addView(packCard)
} }
/** /**
@ -170,19 +171,19 @@ class ImageKeyboard : InputMethodService() {
drawable = ImageDecoder.decodeDrawable(ImageDecoder.createSource(sticker)) drawable = ImageDecoder.decodeDrawable(ImageDecoder.createSource(sticker))
} catch (ignore: IOException) { } catch (ignore: IOException) {
} }
if (sName.contains(".png") || sName.contains(".apng")) { //if (sName.contains(".png") || sName.contains(".apng")) {
drawable = APNGDrawable.fromFile(sticker.absolutePath) // drawable = APNGDrawable.fromFile(sticker.absolutePath)
drawable!!.setAutoPlay(false) // drawable!!.setAutoPlay(false)
drawable.start() // drawable.start()
} //}
// Disable animations? // Disable animations?
if (drawable is AnimatedImageDrawable && !disableAnimations if (drawable is AnimatedImageDrawable && !disableAnimations
) { ) {
drawable.start() drawable.start()
} }
if (drawable is APNGDrawable && disableAnimations) { //if (drawable is APNGDrawable && disableAnimations) {
drawable.stop() // drawable.stop()
} //}
// Apply // Apply
btn.setImageDrawable(drawable) btn.setImageDrawable(drawable)
} }
@ -215,14 +216,14 @@ class ImageKeyboard : InputMethodService() {
} }
/** /**
* When the activity is crated, grab the number of icons per row and the configured icon size * When the activity is crated, grab the number of icons per column and the configured icon size
* before reloading the packs * before reloading the packs
* *
*/ */
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(baseContext) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(baseContext)
iconsPerRow = sharedPreferences.getInt("iconsPerRow", 3) iconsPerColumn = sharedPreferences.getInt("iconsPerColumn", 3)
iconSize = sharedPreferences.getInt("iconSize", 160) iconSize = sharedPreferences.getInt("iconSize", 160)
disableAnimations = sharedPreferences.getBoolean( disableAnimations = sharedPreferences.getBoolean(
"disableAnimations", "disableAnimations",
@ -234,19 +235,28 @@ class ImageKeyboard : InputMethodService() {
reloadPacks() reloadPacks()
} }
/**
* Called when the keyboard is first drawn
*
* @return
*/
override fun onCreateInputView(): View { override fun onCreateInputView(): View {
val keyboardLayout = val keyboardLayout =
layoutInflater.inflate(R.layout.keyboard_layout, null) as RelativeLayout View.inflate(applicationContext, R.layout.keyboard_layout, null)
packContainer = keyboardLayout.findViewById(R.id.packContainer) packContainer = keyboardLayout.findViewById(R.id.packContainer)
imageContainer = keyboardLayout.findViewById(R.id.imageContainer) imageContainer = keyboardLayout.findViewById(R.id.imageContainer)
imageContainer?.layoutParams?.height = (iconSize * iconsPerRow * 1.4).toInt() imageContainer.layoutParams?.height = (iconSize * iconsPerColumn * 1.4).toInt()
recreatePackContainer() recreatePackContainer()
return keyboardLayout return keyboardLayout
} }
/**
* In full-screen mode the inserted content is likely to be hidden by the IME. Hence in this
* sample we simply disable full-screen mode.
*
* @return
*/
override fun onEvaluateFullscreenMode(): Boolean { override fun onEvaluateFullscreenMode(): Boolean {
// In full-screen mode the inserted content is likely to be hidden by the IME. Hence in this
// sample we simply disable full-screen mode.
return false return false
} }
@ -267,27 +277,45 @@ class ImageKeyboard : InputMethodService() {
} }
} }
/**
* Swap the image container every time a new pack is selected. If already cached use that otherwise create
*
* @param stickers
*/
private fun switchImageContainer(stickers: Array<File>) {
// Check the cache
val imageContainerHash = stickers.hashCode()
lateinit var imageContainerLayout: LinearLayout
if (imageContainerHash !in imageContainerCache.keys) {
imageContainerLayout = createImageContainer(stickers)
imageContainerCache[imageContainerHash] = createImageContainer(stickers)
} else {
imageContainerLayout = imageContainerCache[imageContainerHash]!!
}
// Swap the image container
imageContainer.removeAllViews()
imageContainer.addView(imageContainerLayout)
}
/** /**
* Recreate the image container every time a new pack is selected * Recreate the image container every time a new pack is selected
* *
* @param pack * @param stickers
*/ */
private fun recreateImageContainer(stickers: Array<File>) { private fun createImageContainer(stickers: Array<File>): LinearLayout {
// Clear the image view val tempImageContainer =
imageContainer!!.removeAllViewsInLayout() View.inflate(applicationContext, R.layout.image_container, null) as LinearLayout
// And rebuild... lateinit var imageContainerColumn: LinearLayout
var imageContainerColumn = layoutInflater.inflate(
R.layout.image_container_column,
imageContainer,
false
) as LinearLayout
for (i in stickers.indices) { for (i in stickers.indices) {
if (i % iconsPerRow == 0) { // Add a new column
if (i % iconsPerColumn == 0) {
imageContainerColumn = layoutInflater.inflate( imageContainerColumn = layoutInflater.inflate(
R.layout.image_container_column, R.layout.image_container_column,
imageContainer, tempImageContainer,
false false
) as LinearLayout ) as LinearLayout
tempImageContainer.addView(imageContainerColumn)
} }
val imageCard = layoutInflater.inflate( val imageCard = layoutInflater.inflate(
R.layout.sticker_card, R.layout.sticker_card,
@ -302,7 +330,6 @@ class ImageKeyboard : InputMethodService() {
imgButton.setOnClickListener { view: View -> imgButton.setOnClickListener { view: View ->
val file = view.tag as File val file = view.tag as File
recentCache.add(file.absolutePath) recentCache.add(file.absolutePath)
Log.e("qwerty", recentCache.toSharedPref() )
val stickerType = supportedMimes[Utils.getFileExtension(file.name)] val stickerType = supportedMimes[Utils.getFileExtension(file.name)]
if (stickerType == null) { if (stickerType == null) {
// Sticker is unsupported by input // Sticker is unsupported by input
@ -312,10 +339,8 @@ class ImageKeyboard : InputMethodService() {
doCommitContent(file.name, stickerType, file) doCommitContent(file.name, stickerType, file)
} }
imageContainerColumn.addView(imageCard) imageContainerColumn.addView(imageCard)
if (i % iconsPerRow == 0) {
imageContainer!!.addView(imageContainerColumn)
}
} }
return tempImageContainer
} }
/** /**
@ -323,7 +348,7 @@ class ImageKeyboard : InputMethodService() {
* *
*/ */
private fun recreatePackContainer() { private fun recreatePackContainer() {
packContainer!!.removeAllViewsInLayout() packContainer.removeAllViewsInLayout()
// Back button // Back button
if (sharedPreferences.getBoolean("showBackButton", false)) { if (sharedPreferences.getBoolean("showBackButton", false)) {
addBackButtonToContainer() addBackButtonToContainer()
@ -338,7 +363,7 @@ class ImageKeyboard : InputMethodService() {
addPackToContainer(loadedPacks[sortedPackName]!!) addPackToContainer(loadedPacks[sortedPackName]!!)
} }
if (sortedPackNames.isNotEmpty()) { if (sortedPackNames.isNotEmpty()) {
recreateImageContainer(loadedPacks[sortedPackNames[0]]!!.stickerList) switchImageContainer(loadedPacks[sortedPackNames[0]]!!.stickerList)
} }
} }
@ -386,4 +411,4 @@ class ImageKeyboard : InputMethodService() {
// Constants // Constants
private const val AUTHORITY = "com.fredhappyface.ewesticker.inputcontent" private const val AUTHORITY = "com.fredhappyface.ewesticker.inputcontent"
} }
} }

View File

@ -189,18 +189,18 @@ class MainActivity : AppCompatActivity() {
editor.putBoolean("disableAnimations", isChecked) editor.putBoolean("disableAnimations", isChecked)
editor.apply() editor.apply()
} }
val iconsPerRowSeekBar = findViewById<SeekBar>(R.id.iconsPerRowSeekBar) val iconsPerColumnSeekBar = findViewById<SeekBar>(R.id.iconsPerColumnSeekBar)
iconsPerRowSeekBar.progress = sharedPreferences.getInt("iconsPerRow", 3) iconsPerColumnSeekBar.progress = sharedPreferences.getInt("iconsPerColumn", 3)
iconsPerRowSeekBar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener { iconsPerColumnSeekBar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
var iconsPerRow = 3 var iconsPerColumn = 3
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
iconsPerRow = progress iconsPerColumn = progress
} }
override fun onStartTrackingTouch(seekBar: SeekBar) {} override fun onStartTrackingTouch(seekBar: SeekBar) {}
override fun onStopTrackingTouch(seekBar: SeekBar) { override fun onStopTrackingTouch(seekBar: SeekBar) {
val editor = sharedPreferences.edit() val editor = sharedPreferences.edit()
editor.putInt("iconsPerRow", iconsPerRow) editor.putInt("iconsPerColumn", iconsPerColumn)
editor.apply() editor.apply()
refreshKeyboardConfig() refreshKeyboardConfig()
showChangedPrefText() showChangedPrefText()
@ -229,9 +229,9 @@ class MainActivity : AppCompatActivity() {
* Refreshes config from preferences * Refreshes config from preferences
*/ */
fun refreshKeyboardConfig() { fun refreshKeyboardConfig() {
val iconsPerRow = sharedPreferences.getInt("iconsPerRow", 3) val iconsPerColumn = sharedPreferences.getInt("iconsPerColumn", 3)
val iconsPerRowValue = findViewById<TextView>(R.id.iconsPerRowValue) val iconsPerColumnValue = findViewById<TextView>(R.id.iconsPerColumnValue)
iconsPerRowValue.text = iconsPerRow.toString() iconsPerColumnValue.text = iconsPerColumn.toString()
val iconSize = sharedPreferences.getInt("iconSize", 160) val iconSize = sharedPreferences.getInt("iconSize", 160)
val iconSizeValue = findViewById<TextView>(R.id.iconSizeValue) val iconSizeValue = findViewById<TextView>(R.id.iconSizeValue)
iconSizeValue.text = String.format("%dpx", iconSize) iconSizeValue.text = String.format("%dpx", iconSize)

View File

@ -102,14 +102,14 @@
android:text="@string/icons_per_row_status" /> android:text="@string/icons_per_row_status" />
<TextView <TextView
android:id="@+id/iconsPerRowValue" android:id="@+id/iconsPerColumnValue"
style="@style/body_text" style="@style/body_text"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textIsSelectable="false" /> android:textIsSelectable="false" />
<SeekBar <SeekBar
android:id="@+id/iconsPerRowSeekBar" android:id="@+id/iconsPerColumnSeekBar"
style="@style/body_text" style="@style/body_text"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -187,6 +187,3 @@
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:orientation="horizontal" />

View File

@ -13,7 +13,7 @@
<string name="sub_options">&#xf423; Options</string> <string name="sub_options">&#xf423; Options</string>
<string name="show_back_button">Show back button in navbar?</string> <string name="show_back_button">Show back button in navbar?</string>
<string name="disable_animations">Disable Animations?</string> <string name="disable_animations">Disable Animations?</string>
<string name="icons_per_row_status">"Icons per row: "</string> <string name="icons_per_row_status">"Icons per column: "</string>
<string name="icon_size_status">"Icon size: "</string> <string name="icon_size_status">"Icon size: "</string>
<!-- About --> <!-- About -->

View File

@ -1,13 +1,12 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext.kotlin_version = "1.5.10"
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.0.0' classpath 'com.android.tools.build:gradle:7.0.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.21"
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files

View File

@ -0,0 +1,16 @@
<ul>
<li>Code optimisations
<ul>
<li>Code clean-up</li>
<li>Removed APNG animation due to memory leak</li>
<li>Linting fixes</li>
</ul></li>
<li>Added caching functionality
<ul>
<li>to improve performance of fallback stickers</li>
<li>to enable addition of recent list</li>
<li>to improve switching packs performance</li>
</ul></li>
<li>Updated gradle and deps</li>
<li>Add recent icon</li>
</ul>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 191 KiB