Initial push; upped AGP to 8.0.1, Kotlin plugin to 1.8.20, as well as updated all dependencies.

This commit is contained in:
denizk0461
2023-05-08 11:27:43 +02:00
commit 06ea1c88e4
113 changed files with 3055 additions and 0 deletions
@@ -0,0 +1,55 @@
package com.denizd.textbasic
import android.R
import java.util.*
import kotlin.math.roundToInt
/**
* Created by Hemant chand on 05/07/17.
*/
object ColorTransparentUtils {
// This default color int
const val defaultColorID = R.color.black
const val defaultColor = "000000"
const val TAG = "ColorTransparentUtils"
/**
* This method convert numver into hexa number or we can say transparent code
*
* @param trans number of transparency you want
* @return it return hex decimal number or transparency code
*/
fun convert(trans: Int): String {
val hexString = Integer.toHexString((255 * trans / 100.0).roundToInt())
return (if (hexString.length < 2) "0" else "") + hexString
}
fun transparentColor(colorCode: Int, trans: Int): String {
return convertIntoColor(colorCode, trans)
}
/**
* Convert color code into transparent color code
*
* @param colorCode color code
* @param transCode transparent number
* @return transparent color code
*/
fun convertIntoColor(colorCode: Int, transCode: Int): String {
// convert color code into hexa string and remove starting 2 digit
var color = defaultColor
try {
color = Integer.toHexString(colorCode).uppercase(Locale.getDefault()).substring(2)
} catch (ignored: Exception) {
}
return if (color.isNotEmpty() && transCode < 101) {
if (color.trim { it <= ' ' }.length == 6) {
"#" + convert(transCode) + color
} else {
// Log.d(TAG, "Color is already with transparency")
convert(transCode) + color
}
} else "#" + Integer.toHexString(defaultColorID).uppercase(Locale.getDefault()).substring(2)
// if color is empty or any other problem occur then we return deafult color;
}
}
@@ -0,0 +1,14 @@
package com.denizd.textbasic
import android.app.backup.*
class DriveAgent : BackupAgentHelper() {
override fun onCreate() {
super.onCreate()
SharedPreferencesBackupHelper(this, QuoteStorage.PREF_NAME).also {
addHelper(QuoteStorage.PREF_BACKUP_KEY, it)
}
}
}
@@ -0,0 +1,97 @@
package com.denizd.textbasic
import android.app.backup.BackupManager
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.FragmentTransaction
import androidx.lifecycle.Lifecycle
import com.denizd.textbasic.databinding.ActivityMainBinding
import com.denizd.textbasic.fragment.GuideFragment
import com.denizd.textbasic.fragment.QuoteFragment
import com.denizd.textbasic.fragment.SettingsFragment
import com.denizd.textbasic.widget.TextCanvasWidget
import com.google.android.material.snackbar.Snackbar
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(this.layoutInflater)
setContentView(binding.root)
// binding.buttonBackupDownload.setOnClickListener {
//
// Snackbar.make(binding.rootLayout, R.string.snack_backup_download, Snackbar.LENGTH_LONG).show()
// }
binding.buttonBackupUpload.setOnClickListener {
BackupManager(this).dataChanged()
Snackbar.make(binding.rootLayout, R.string.snack_backup_upload, Snackbar.LENGTH_LONG).show()
}
binding.bottomNav.setOnItemSelectedListener { item ->
loadFragment(item.itemId)
}
}
override fun onResume() {
super.onResume()
if (supportFragmentManager.fragments.size == 0) {
binding.bottomNav.selectedItemId = R.id.quotes
loadFragment(R.id.quotes)
}
}
private fun loadFragment(type: Int): Boolean {
val fragment = when (type) {
R.id.quotes -> QuoteFragment()
R.id.guide -> GuideFragment()
R.id.settings -> SettingsFragment()
else -> QuoteFragment()
}//supportFragmentManager.findFragmentByTag(type) ?: viewModel.getFragment(type)
if (supportFragmentManager.fragments.isNotEmpty() &&
supportFragmentManager.fragments[supportFragmentManager.fragments.size-1]
.tag?.equals(type.toString()) == true
) {
return false
}
if (fragment.lifecycle.currentState != Lifecycle.State.INITIALIZED) {
return false
}
supportFragmentManager.beginTransaction()
.replace(R.id.fragment_container, fragment, type.toString())
// .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.commit()
// with (supportFragmentManager) {
// beginTransaction()
// .hide(fragments[fragments.size - 1]) // should this be 0?
// .show(findFragmentByTag(type) ?: TODO())
// .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
// .commit()
// }
return true
}
override fun onPause() {
arrayOf(TextCanvasWidget::class.java).forEach { widget ->
sendBroadcast(Intent(this, widget).also { intent ->
intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, AppWidgetManager.getInstance(this)
.getAppWidgetIds(ComponentName(application, widget)))
})
}
super.onPause()
}
}
@@ -0,0 +1,3 @@
package com.denizd.textbasic
data class Quote(val id: Int, var text: String)
@@ -0,0 +1,71 @@
package com.denizd.textbasic
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.button.MaterialButton
class QuoteAdapter(quotes: List<String>) : RecyclerView.Adapter<QuoteAdapter.QuoteViewHolder>() {
private var mutableQuotes: MutableList<String> = quotes.toMutableList()
class QuoteViewHolder(view: View) : RecyclerView.ViewHolder(view)/*, View.OnClickListener, View.OnLongClickListener*/ {
val quote: TextView = view.findViewById(R.id.quote)
val delete: ImageButton = view.findViewById(R.id.delete_button)
}
override fun getItemViewType(position: Int): Int {
return /*if (position == itemCount - 1) 1 else*/ 0
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QuoteViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.quote_entry, parent, false)
return QuoteViewHolder(v)
}
override fun onBindViewHolder(holder: QuoteViewHolder, position: Int) {
val currentItem = mutableQuotes[position]
holder.quote.text = currentItem
holder.quote.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
override fun afterTextChanged(p0: Editable?) {}
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
mutableQuotes[holder.layoutPosition] = p0.toString()
}
})
holder.delete.setOnClickListener {
mutableQuotes.removeAt(holder.layoutPosition)
notifyItemRemoved(holder.layoutPosition)
}
}
override fun getItemCount(): Int = mutableQuotes.size
fun setQuotes(quotes: List<String>) {
this.mutableQuotes = quotes.toMutableList()
notifyDataSetChanged()
}
fun addNewQuote() {
mutableQuotes.add("")
notifyItemInserted(mutableQuotes.size + 1)
}
fun getAllQuotes(): Array<String> {
return mutableQuotes.filter { s -> s.isNotBlank() }.toTypedArray()
}
interface QuoteClickListener {
fun onQuoteClick(quotes: String)
fun onQuoteLongClick(quoteId: Int)
}
}
@@ -0,0 +1,134 @@
package com.denizd.textbasic
import android.content.Context
import android.content.SharedPreferences
import android.graphics.Color
import android.graphics.Typeface
import android.util.Log
import androidx.preference.PreferenceManager
class QuoteStorage(context: Context) {
companion object {
const val PREF_NAME = "textbasicprefs"
const val PREF_BACKUP_KEY = "textbasicprefskey"
const val SEPARATOR = '\uFFFF'
private const val KEY_QUOTES = "quotes"
private const val KEY_QUOTE_COUNTER = "quotecounter"
private const val KEY_TEXT_SIZE = "textsize"
private const val KEY_INVERTED = "inverted"
private const val KEY_HIGH_CONTRAST = "hicontrast"
private const val KEY_TRANSPARENCY = "transparency"
private const val KEY_RANDOM = "random"
private const val KEY_WIDGET_POSITION = "widgetposition"
private const val KEY_BACKGROUND_TYPE = "backgroundtype"
private const val KEY_TYPEFACE = "typeface"
private const val KEY_TYPEFACE_STYLE = "typefacestyle"
private const val KEY_AVG_WIDTH = "avgwidth"
private const val KEY_AVG_HEIGHT = "avgheight"
private const val KEY_OUTLINE_SIZE = "outlinesize"
val prefGroups = arrayOf(
KEY_QUOTES, KEY_QUOTE_COUNTER, KEY_TEXT_SIZE, KEY_INVERTED, KEY_HIGH_CONTRAST,
KEY_TRANSPARENCY, KEY_RANDOM, KEY_WIDGET_POSITION
)
// val transparencyValues = arrayOf(
// "0", "5", "10", "15", "20", "25", "30", "35", "40", "45", "50",
// "55", "60", "65", "70", "75", "80", "85", "90", "95", "100",
// )
}
private val prefs = context.getSharedPreferences(PREF_NAME, 0)
fun getAllQuotes(): List<String> = ((prefs.getString(KEY_QUOTES, "")) ?: "").split(SEPARATOR).toList()
fun getRandomQuote(): String {
val quotes = getAllQuotes()
return quotes.random()
}
fun getNextQuote(): String {
val currentQuoteCounter: Int = prefs.getInt(KEY_QUOTE_COUNTER, 0)
val quotes = getAllQuotes()
val nextQuoteCounter: Int = if (currentQuoteCounter >= quotes.size - 1) 0 else currentQuoteCounter + 1
prefs.edit().putInt(
KEY_QUOTE_COUNTER,
nextQuoteCounter
).apply()
return quotes[nextQuoteCounter]
}
fun getTextSize() = prefs.getInt(KEY_TEXT_SIZE, 18)
fun isInvertedEnabled(): Boolean = prefs.getBoolean(KEY_INVERTED, false)
fun isHighContrastEnabled(): Boolean = prefs.getBoolean(KEY_HIGH_CONTRAST, false)
fun getTransparency(): Int = prefs.getInt(KEY_TRANSPARENCY, 100)
fun isOrderRandom(): Boolean = prefs.getBoolean(KEY_RANDOM, false)
fun getWidgetGravity(): Int = prefs.getInt(KEY_WIDGET_POSITION, 1)
fun getBackgroundType(): Int = prefs.getInt(KEY_BACKGROUND_TYPE, 0)
fun getTypefaceIndex(): Int = prefs.getInt(KEY_TYPEFACE, 0)
fun getTypeface(): Typeface = Typeface.create(
when (getTypefaceIndex()) {
1 -> Typeface.SERIF
2 -> Typeface.MONOSPACE
else -> Typeface.SANS_SERIF
},
getTypefaceStyle()
)
fun getTypefaceStyle(): Int = prefs.getInt(KEY_TYPEFACE_STYLE, 0)
fun getOutlineSize(): Float = prefs.getFloat(KEY_OUTLINE_SIZE, 8f)
// Pair<text colour, background colour>
fun getColours(
isInverted: Boolean = isInvertedEnabled(),
transparency: Int = getTransparency(),
context: Context
): Pair<Int, Int> {
val colours = when (isInverted) {
true -> Pair(R.color.widget_dark, R.color.widget_light)
false -> Pair(R.color.widget_light, R.color.widget_dark)
}
return Pair(
context.getColor(colours.first),//Color.parseColor(ColorTransparentUtils.transparentColor(colours.first, 10)),
Color.parseColor(ColorTransparentUtils.transparentColor(context.getColor(colours.second), transparency * 5))
)
}
fun setSize(size: Pair<Int, Int>) {
prefs.edit().apply {
putInt(KEY_AVG_WIDTH, size.first)
putInt(KEY_AVG_HEIGHT, size.second)
}.apply()
}
fun getSize(): Pair<Int, Int> = Pair(prefs.getInt(KEY_AVG_WIDTH, 1), prefs.getInt(KEY_AVG_HEIGHT, 1))
fun saveSettings(
textSize: Int, inverted: Boolean, transparency: Int, random: Boolean,
widgetPosition: Int, backgroundType: Int, typefaceIndex: Int, typefaceStyleIndex: Int,
outlineSize: Float
) {
prefs.edit().apply {
putInt(KEY_TEXT_SIZE, textSize)
putBoolean(KEY_INVERTED, inverted)
putInt(KEY_TRANSPARENCY, transparency)
putBoolean(KEY_RANDOM, random)
putInt(KEY_WIDGET_POSITION, widgetPosition)
putInt(KEY_BACKGROUND_TYPE, backgroundType)
putInt(KEY_TYPEFACE, typefaceIndex)
putInt(KEY_TYPEFACE_STYLE, typefaceStyleIndex)
putFloat(KEY_OUTLINE_SIZE, outlineSize)
}.apply()
}
fun saveQuotes(quotes: Array<String>) {
prefs.edit()
.putString(KEY_QUOTES, quotes.joinToString(SEPARATOR.toString()))
.apply()
}
}
@@ -0,0 +1,71 @@
package com.denizd.textbasic
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.content.Intent
import android.graphics.Typeface
import android.util.TypedValue
import android.view.Gravity
import android.widget.RemoteViews
import android.widget.TextView
class TextWidget : AppWidgetProvider() {
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
val remoteViews = configureViews(context, appWidgetIds)
// Perform this loop procedure for each App Widget that belongs to this provider
appWidgetIds.forEach { appWidgetId ->
// Tell the AppWidgetManager to perform an update on the current app widget
appWidgetManager.updateAppWidget(appWidgetId, remoteViews)
}
}
private fun configureViews(context: Context, appWidgetIds: IntArray): RemoteViews =
RemoteViews(context.packageName, R.layout.widget_text).apply {
val storage = QuoteStorage(context)
val quote = if (storage.isOrderRandom()) {
storage.getRandomQuote()
} else {
storage.getNextQuote()
}
setTextViewText(R.id.widget_text, quote.ifBlank { context.getString(R.string.info_add_text) })
setTextViewTextSize(R.id.widget_text, TypedValue.COMPLEX_UNIT_SP, storage.getTextSize().toFloat())
val colours = storage.getColours(context = context)
setTextColor(R.id.widget_text, colours.first)
setInt(R.id.background, "setBackgroundColor", colours.second)
val gravity = when (storage.getWidgetGravity()) {
0 -> Gravity.START or Gravity.TOP
1 -> Gravity.CENTER_HORIZONTAL or Gravity.TOP
2 -> Gravity.END or Gravity.TOP
3 -> Gravity.START or Gravity.CENTER_VERTICAL
4 -> Gravity.CENTER
5 -> Gravity.END or Gravity.CENTER_VERTICAL
6 -> Gravity.START or Gravity.BOTTOM
7 -> Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM
8 -> Gravity.END or Gravity.BOTTOM
else -> Gravity.CENTER
}
setInt(R.id.root_layout, "setGravity", gravity)
setOnClickPendingIntent(
R.id.root_layout,
Intent(context, TextWidget::class.java).let { intent ->
intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds)
PendingIntent.getBroadcast(
context, 0, intent, PendingIntent.FLAG_IMMUTABLE
)
}
)
}
}
@@ -0,0 +1,41 @@
package com.denizd.textbasic
import android.content.Context
import android.graphics.Canvas
import android.graphics.Rect
import android.util.AttributeSet
class VerticalTextView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : androidx.appcompat.widget.AppCompatTextView(context, attrs, defStyle) {
private var _width = 0
private var _height = 0
private val _bounds: Rect = Rect()
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
// vise versa
_height = measuredWidth
_width = measuredHeight
setMeasuredDimension(_width, _height)
}
override fun onDraw(canvas: Canvas) {
canvas.save()
canvas.translate(_width.toFloat(), _height.toFloat())
canvas.rotate(-90f)
val paint = paint
paint.color = textColors.defaultColor
val text = text()
paint.getTextBounds(text, 0, text.length, _bounds)
canvas.drawText(text, compoundPaddingLeft.toFloat(), (_bounds.height() - _width) / 2f, paint)
canvas.restore()
}
private fun text(): String {
return super.getText().toString()
}
}
@@ -0,0 +1,39 @@
package com.denizd.textbasic.fragment
import android.content.Context
import androidx.annotation.LayoutRes
import androidx.core.widget.NestedScrollView
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.RecyclerView
open class BaseFragment(@LayoutRes layoutId: Int) : Fragment(layoutId) {
private lateinit var _context: Context
override fun onAttach(context: Context) {
super.onAttach(context)
_context = context
}
override fun getContext(): Context = _context
// protected fun RecyclerView.addFabScrollListener() {
// addOnScrollListener(object : RecyclerView.OnScrollListener() {
// override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
// when {
// dy > 0 -> fab.hide()
// dy < 0 -> fab.show()
// }
// }
// })
// }
//
// protected fun NestedScrollView.addFabScrollListener() {
// setOnScrollChangeListener { _, _, dy, _, doy ->
// when {
// dy > doy -> fab.hide()
// dy < doy -> fab.show()
// }
// }
// }
}
@@ -0,0 +1,18 @@
package com.denizd.textbasic.fragment
import android.os.Bundle
import com.denizd.textbasic.R
import com.google.android.material.transition.MaterialSharedAxis
class GuideFragment : BaseFragment(R.layout.fragment_guide) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false)
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true)
enterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true)
returnTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false)
}
}
@@ -0,0 +1,57 @@
package com.denizd.textbasic.fragment
import android.animation.LayoutTransition
import android.os.Bundle
import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager
import com.denizd.textbasic.QuoteAdapter
import com.denizd.textbasic.QuoteStorage
import com.denizd.textbasic.util.viewBinding
import com.denizd.textbasic.R
import com.denizd.textbasic.databinding.FragmentQuoteBinding
import com.google.android.material.transition.MaterialSharedAxis
class QuoteFragment : BaseFragment(R.layout.fragment_quote) {
private lateinit var storage: QuoteStorage
private lateinit var quoteAdapter: QuoteAdapter
private val binding: FragmentQuoteBinding by viewBinding(FragmentQuoteBinding::bind)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true)
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false)
enterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false)
returnTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.quoteConstraintLayout.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
storage = QuoteStorage(context)
quoteAdapter = QuoteAdapter(storage.getAllQuotes())
binding.quoteScroller.let { s ->
s.layoutManager = LinearLayoutManager(context)
s.adapter = quoteAdapter
}
binding.addFab.setOnClickListener {
quoteAdapter.addNewQuote()
}
}
private fun save() {
storage.saveQuotes(quoteAdapter.getAllQuotes())
}
override fun onPause() {
save()
super.onPause()
}
}
@@ -0,0 +1,319 @@
package com.denizd.textbasic.fragment
import android.graphics.Bitmap
import android.graphics.Color
import android.os.Bundle
import android.util.Log
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.annotation.ColorInt
import com.denizd.textbasic.ColorTransparentUtils
import com.denizd.textbasic.QuoteStorage
import com.denizd.textbasic.R
import com.denizd.textbasic.databinding.FragmentSettingsBinding
import com.denizd.textbasic.util.viewBinding
import com.denizd.textbasic.widget.CanvasText
import com.google.android.material.transition.MaterialSharedAxis
import java.lang.reflect.Field
import kotlin.math.abs
class SettingsFragment : BaseFragment(R.layout.fragment_settings) {
private lateinit var storage: QuoteStorage
private lateinit var gravityButtons: Array<ImageButton>
private var widgetGravity = 4
private var backgroundIndex = 0
private var typefaceIndex = 0
private var typefaceStyleIndex = 0
private val binding: FragmentSettingsBinding by viewBinding(FragmentSettingsBinding::bind)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false)
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true)
enterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true)
returnTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// binding.animationLl.animateLayoutChanges(true)
storage = QuoteStorage(context)
gravityButtons = arrayOf(
binding.buttonTopLeft, binding.buttonTopMiddle, binding.buttonTopRight,
binding.buttonMiddleLeft, binding.buttonMiddleMiddle, binding.buttonMiddleRight,
binding.buttonBottomLeft, binding.buttonBottomMiddle, binding.buttonBottomRight
)
widgetGravity = storage.getWidgetGravity()
binding.textSizePicker.apply {
minValue = 1
maxValue = 168
descendantFocusability = ViewGroup.FOCUS_BLOCK_DESCENDANTS
wrapSelectorWheel = false
value = storage.getTextSize()
setOnValueChangedListener { _, _, _ ->
updatePreview()
}
}
binding.transparencyPicker.apply {
minValue = 0
maxValue = 20
val formatter = NumberPicker.Formatter { value ->
val temp = value * 5
"" + temp
}
setFormatter(formatter)
descendantFocusability = ViewGroup.FOCUS_BLOCK_DESCENDANTS
wrapSelectorWheel = false
value = storage.getTransparency()
// Do not touch - fix for value on spinner not showing up until touched
val f: Field = NumberPicker::class.java.getDeclaredField("mInputText")
f.isAccessible = true
val inputText: EditText = f.get(binding.transparencyPicker) as EditText
inputText.filters = arrayOfNulls(0)
setOnValueChangedListener { _, _, _ ->
updatePreview()
}
}
binding.outlinePicker.apply {
minValue = 0
maxValue = 64
descendantFocusability = ViewGroup.FOCUS_BLOCK_DESCENDANTS
wrapSelectorWheel = false
value = storage.getOutlineSize().toInt()
setOnValueChangedListener { _, _, _ ->
updatePreview()
}
}
binding.switchInvert.apply {
isChecked = storage.isInvertedEnabled()
setOnCheckedChangeListener { _, _ ->
updatePreview()
}
}
binding.switchRandom.apply {
isChecked = storage.isOrderRandom()
setOnCheckedChangeListener { _, _ ->
updatePreview()
}
}
// binding.switchHighContrast.apply {
// isChecked = storage.isHighContrastEnabled()
//
// setOnCheckedChangeListener { _, _ ->
// updatePreview()
// }
// }
// binding.typefaceMenuTextview.apply {
// typefaceIndex = storage.getTypefaceIndex()
// val typefaces = context.resources.getStringArray(R.array.typefaces)
// val typefaceAdapter = ArrayAdapter(
// context,
// android.R.layout.simple_dropdown_item_1line,
// typefaces
// )
// setAdapter(typefaceAdapter)
// setText(typefaces[typefaceIndex], false)
//
// setOnItemClickListener { adapterView, view, index, l ->
// typefaceIndex = index
// updatePreview()
// }
// }
binding.typefaceToggleButton.apply {
typefaceIndex = storage.getTypefaceIndex()
check(when (typefaceStyleIndex) {
2 -> R.id.button3_monospace
1 -> R.id.button2_serif
else -> R.id.button1_sansserif
})
addOnButtonCheckedListener { group, checkedId, isChecked ->
typefaceIndex = when (checkedId) {
R.id.button3_monospace -> 2
R.id.button2_serif -> 1
else -> 0
}
updatePreview()
}
}
binding.typefaceStyleToggleButton.apply {
typefaceStyleIndex = storage.getTypefaceStyle()
check(when (typefaceStyleIndex) {
3 -> R.id.button4_bold_italics
2 -> R.id.button3_italics
1 -> R.id.button2_bold
else -> R.id.button1_normal
})
addOnButtonCheckedListener { group, checkedId, isChecked ->
typefaceStyleIndex = when (checkedId) {
R.id.button4_bold_italics -> 3
R.id.button3_italics -> 2
R.id.button2_bold -> 1
else -> 0
}
updatePreview()
}
}
binding.backgroundToggleButton.apply {
backgroundIndex = storage.getBackgroundType()
check(when (backgroundIndex) {
2 -> R.id.button3_shadow
1 -> R.id.button2_outline
else -> R.id.button1_background
})
addOnButtonCheckedListener { group, checkedId, isChecked ->
backgroundIndex = when (checkedId) {
R.id.button3_shadow -> 2
R.id.button2_outline -> 1
else -> 0
}
updatePreview()
}
}
gravityButtons.forEachIndexed { index, button ->
button.setOnClickListener {
widgetGravity = index
setGravityButtons()
updatePreview()
}
}
setGravityButtons()
updatePreview()
}
private fun setGravityButtons() {
val transparent = context.getColor(android.R.color.transparent)
gravityButtons.forEach { button ->
button.setBackgroundColor(transparent)
}
val typedValue = TypedValue()
context.theme.resolveAttribute(R.attr.colorSecContainer, typedValue, true)
@ColorInt val colorTertiary = typedValue.data
gravityButtons[widgetGravity].setBackgroundColor(colorTertiary)
}
private fun updatePreview() {
save()
if (backgroundIndex == 0) {
binding.outlineTextview.visibility = View.GONE
binding.outlinePicker.visibility = View.GONE
} else {
binding.outlineTextview.visibility = View.VISIBLE
binding.outlinePicker.visibility = View.VISIBLE
if (backgroundIndex == 1) {
binding.outlineTextview.text = getString(R.string.thickness)
} else {
binding.outlineTextview.text = getString(R.string.intensity)
}
}
// val isBackgroundDark = if (storage.isInvertedEnabled()) { // bg white
// (storage.getTransparency() * 5) <= 5
// } else { // bg black
// (storage.getTransparency() * 5) > 5
// }
// Log.d("ASDF", isBackgroundDark.toString())
// binding.textPreviewBackground.setBackgroundColor(
// Color.parseColor(ColorTransparentUtils.transparentColor(context.getColor(if (isBackgroundDark) {
// R.color.md_theme_light_background
// } else {
// R.color.md_theme_dark_background
// }), storage.getTransparency() * 5)))
// binding.textPreviewBackground.setBackgroundColor(
// storage.getColours(
// !storage.isInvertedEnabled(),
// abs(storage.getTransparency() - 100),
// context = context
// ).second
// )
binding.textPreviewBackgroundDark.setBackgroundColor(
Color.parseColor(ColorTransparentUtils.transparentColor(
context.getColor(R.color.md_theme_dark_background),
if (storage.isInvertedEnabled()) {
abs((storage.getTransparency() * 5))
} else {
abs((storage.getTransparency() * 5) - 100)
}
))
)
val bitmap = CanvasText.drawText(context, true)
binding.textPreview.setImageBitmap(bitmap)
// val colours = storage.getColours(
// binding.switchInvert.isChecked,
//// binding.switchHighContrast.isChecked,
// binding.transparencyPicker.value,
// context
// )
//// binding.exampleText.textSize = binding.textSizePicker.value.toFloat()
// binding.exampleText.setTextColor(colours.first)
// binding.background.setBackgroundColor(colours.second)
// Log.d("VALVALUE", "${(binding.transparencyPicker.value * 5).toString()} /// ${colours.second}")
}
private fun save() {
storage.saveSettings(
binding.textSizePicker.value,
binding.switchInvert.isChecked,
// binding.switchHighContrast.isChecked,
binding.transparencyPicker.value,
binding.switchRandom.isChecked,
widgetGravity,
backgroundIndex,
typefaceIndex,
typefaceStyleIndex,
binding.outlinePicker.value.toFloat()
)
}
override fun onPause() {
save()
super.onPause()
}
}
@@ -0,0 +1,12 @@
package com.denizd.textbasic.util
import android.view.LayoutInflater
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.FragmentActivity
import androidx.viewbinding.ViewBinding
// written by Zhuinden (props to them!)
inline fun <T : ViewBinding> FragmentActivity.viewBinding(crossinline bindingInflater: (LayoutInflater) -> T) =
lazy(LazyThreadSafetyMode.NONE) {
bindingInflater.invoke(layoutInflater)
}
@@ -0,0 +1,50 @@
package com.denizd.textbasic.util
import android.view.View
import androidx.fragment.app.Fragment
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.observe
import androidx.viewbinding.ViewBinding
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
// written by Zhuinden (props to them!)
class FragmentViewBindingDelegate<T : ViewBinding>(
val fragment: Fragment,
val viewBindingFactory: (View) -> T
) : ReadOnlyProperty<Fragment, T> {
private var _binding: T? = null
init {
fragment.lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
fragment.viewLifecycleOwnerLiveData.observe(fragment) { viewLifecycleOwner ->
viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
_binding = null
}
})
}
}
})
}
override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
val binding = _binding
if (binding != null) {
return binding
}
val lifecycle = fragment.viewLifecycleOwner.lifecycle
if (!lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) {
throw IllegalStateException("Should not attempt to get bindings when Fragment views are destroyed.")
}
return viewBindingFactory(thisRef.requireView()).also { _binding = it }
}
}
fun <T : ViewBinding> Fragment.viewBinding(viewBindingFactory: (View) -> T) =
FragmentViewBindingDelegate(this, viewBindingFactory)
@@ -0,0 +1,268 @@
package com.denizd.textbasic.widget
import android.content.Context
import android.graphics.*
import android.text.StaticLayout
import android.text.TextPaint
import android.util.Log
import com.denizd.textbasic.QuoteStorage
import com.denizd.textbasic.R
class CanvasText {
companion object {
fun drawText(context: Context, isPreview: Boolean = false, size: Pair<Int, Int>? = null): Bitmap {
val previewSize = 20f
val storage = QuoteStorage(context)
size?.let {
storage.setSize(size)
// Log.d("ASDF", "${size.first} + ${size.second}")
}
val widgetSize = storage.getSize()
val outlineSize = storage.getOutlineSize()
val bmp = if (isPreview) {
Bitmap.createBitmap(1024, 256, Bitmap.Config.ARGB_8888)
} else {
// Log.d("ASDFasdf", "${widgetSize.first} + ${widgetSize.second}")
Bitmap.createBitmap(widgetSize.first * 3, widgetSize.second * 3, Bitmap.Config.ARGB_8888)
}
val canvas = Canvas(bmp)
val quote = if (isPreview) {
context.getString(R.string.example_text)
} else if (storage.isOrderRandom()) {
storage.getRandomQuote()
} else {
storage.getNextQuote()
}.ifBlank { context.getString(R.string.info_add_text) }
val colours = storage.getColours(context = context)
val scaledTextSize =
context.resources.displayMetrics.scaledDensity * if (/*isPreview*/false) previewSize else {
storage.getTextSize().toFloat()
}
val savedTypeface = storage.getTypeface()
val widgetGravity = storage.getWidgetGravity()
val paint = TextPaint().apply {
style = Paint.Style.FILL
color = colours.first
textSize = scaledTextSize
textAlign = when (widgetGravity) {
0, 3, 6 -> Paint.Align.LEFT
2, 5, 8 -> Paint.Align.RIGHT
else -> Paint.Align.CENTER
}
isAntiAlias = true
isSubpixelText = false
typeface = savedTypeface
}
val staticLayout = StaticLayout.Builder
.obtain(quote, 0, quote.length, paint, if (isPreview) 1024 else widgetSize.first * 3).build()
val boundsText = Rect()
paint.getTextBounds(quote, 0, quote.length, boundsText)
val (x, y) = calculateWidgetGravity(storage.getWidgetGravity(), bmp, boundsText, isPreview)
// val x = canvas.width / 2
// val y = ((canvas.getHeight() / 2) - ((paint.descent() + paint.ascent()) / 2))
val padding = 16f
val alignment = when (widgetGravity) {
0 -> {
canvas.translate(boundsText.left.toFloat() + padding, 0f)
Paint.Align.LEFT
}
1 -> {
canvas.translate(canvas.width / 2f, 0f)
Paint.Align.CENTER
}
2 -> {
canvas.translate(canvas.width.toFloat() - padding, 0f)
Paint.Align.RIGHT
}
3 -> {
canvas.translate(boundsText.left.toFloat() + padding, 0f)
Paint.Align.LEFT
}
5 -> {
canvas.translate(canvas.width.toFloat() - padding, 0f)
Paint.Align.RIGHT
}
6 -> {
canvas.translate(boundsText.left.toFloat() + padding, bmp.height.toFloat() - staticLayout.height - padding)
Paint.Align.LEFT
}
7 -> {
canvas.translate(canvas.width / 2f, bmp.height.toFloat() - staticLayout.height - padding)
Paint.Align.CENTER
}
8 -> {
canvas.translate(canvas.width.toFloat() - padding, bmp.height.toFloat() - staticLayout.height - padding)
Paint.Align.RIGHT
}
else -> { // 4
canvas.translate(canvas.width / 2f, 0f)
Paint.Align.CENTER
}
}
when (val bgType = storage.getBackgroundType()) {
0 -> { // background
val bgPaint = Paint().apply {
style = Paint.Style.FILL
color = colours.second
}
val rounding = 24f
// val rect = roundedRect(
// boundsText.left + x - padding,
// boundsText.top + y - padding,
// boundsText.right + x + padding,
// boundsText.bottom + y + padding, rounding, rounding
// )
// val rect = roundedRect(
// 0f, 0f, boundsText.right.toFloat() / 2, staticLayout.height.toFloat(),
// rounding, rounding
// )
val rect = when (widgetGravity) { // right
2, 5, 8 -> roundedRect(
-boundsText.right.toFloat() - 4f, 0f, 0f, staticLayout.height.toFloat(),
rounding, rounding
)
1, 4, 7 -> roundedRect( // middle
-(boundsText.width() / 2f), 0f,
boundsText.right.toFloat() / 2f, staticLayout.height.toFloat(),
rounding, rounding
)
else -> roundedRect( // left
0f, 0f, boundsText.right.toFloat() + 4f, staticLayout.height.toFloat(),
rounding, rounding
)
}
canvas.drawPath(rect, bgPaint)
}
1, 2 -> { // outline or shadow
val strokePaint = TextPaint().apply {
style = Paint.Style.STROKE
strokeWidth = outlineSize //if (isPreview) previewSize / 1.5f else
color = colours.second
textSize = scaledTextSize
textAlign = alignment
isAntiAlias = true
isSubpixelText = false
typeface = savedTypeface
if (bgType == 2) { // blur outline for shadow
maskFilter = BlurMaskFilter(if (outlineSize == 0f) 1f else outlineSize, BlurMaskFilter.Blur.NORMAL)
}
}
val strokeStaticLayout = StaticLayout.Builder
.obtain(quote, 0, quote.length, strokePaint, if (isPreview) 1024 else widgetSize.first * 3).build()
strokeStaticLayout.draw(canvas)
// canvas.drawText(quote, x, y, strokePaint)
}
}
staticLayout.draw(canvas)
// canvas.drawText(quote, x, y, paint)
return bmp
}
private fun calculateWidgetGravity(widgetGravity: Int, bitmap: Bitmap, boundsText: Rect, isPreview: Boolean) : Pair<Float, Float> {
return if (isPreview) {
Pair((bitmap.width - boundsText.width()) / 2f, (bitmap.height + boundsText.height()) / 2f)
} else when (widgetGravity) {
0 -> { // top left
Pair(0f + boundsText.left, (boundsText.height() - boundsText.top).toFloat())
}
1 -> { // top center
Pair(
(bitmap.width - boundsText.width()) / 2f,
(boundsText.height() - boundsText.top).toFloat()
)
}
2 -> { // top right
Pair(
(bitmap.width - (boundsText.right)).toFloat(),
(boundsText.height() - boundsText.top).toFloat()
)
}
3 -> { // center left
Pair(0f + boundsText.left, (bitmap.height + boundsText.height()) / 2f)
}
// 4 == else branch
5 -> { // center right
Pair(
(bitmap.width - (boundsText.right)).toFloat(),
(bitmap.height + boundsText.height()) / 2f
)
}
6 -> { // bottom left
Pair(0f + boundsText.left, (bitmap.height - boundsText.bottom).toFloat())
}
7 -> { // bottom center
Pair(
(bitmap.width - boundsText.width()) / 2f,
(bitmap.height - boundsText.bottom).toFloat()
)
}
8 -> { // bottom right
Pair(
(bitmap.width - (boundsText.right)).toFloat(),
(bitmap.height - boundsText.bottom).toFloat()
)
}
else -> { // center center LIKE 4
Pair(
(bitmap.width - boundsText.width()) / 2f,
(bitmap.height + boundsText.height()) / 2f
)
}
}
}
fun roundedRect(
left: Float, top: Float, right: Float, bottom: Float,
roundingX: Float, roundingY: Float
): Path {
var rx = roundingX
var ry = roundingY
val path = Path()
if (rx < 0) rx = 0f
if (ry < 0) ry = 0f
val width = right - left
val height = bottom - top
if (rx > width / 2) rx = width / 2
if (ry > height / 2) ry = height / 2
val widthMinusCorners = width - 2 * rx
val heightMinusCorners = height - 2 * ry
path.moveTo(right, top + ry)
path.rQuadTo(0f, -ry, -rx, -ry) //top-right corner
path.rLineTo(-widthMinusCorners, 0f)
path.rQuadTo(-rx, 0f, -rx, ry) //top-left corner
path.rLineTo(0f, heightMinusCorners)
path.rQuadTo(0f, ry, rx, ry) //bottom-left corner
path.rLineTo(widthMinusCorners, 0f)
path.rQuadTo(rx, 0f, rx, -ry) //bottom-right corner
path.rLineTo(0f, -heightMinusCorners)
path.close() //Given close, last lineto can be removed.
return path
}
}
}
@@ -0,0 +1,68 @@
package com.denizd.textbasic.widget
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.graphics.*
import android.os.Bundle
import android.util.Log
import android.widget.RemoteViews
import com.denizd.textbasic.R
import kotlin.random.Random
class TextCanvasWidget : AppWidgetProvider() {
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
// Perform this loop procedure for each App Widget that belongs to this provider
appWidgetIds.forEach { appWidgetId ->
val remoteViews = configureViews(context, appWidgetIds)
// Tell the AppWidgetManager to perform an update on the current app widget
appWidgetManager.updateAppWidget(appWidgetId, remoteViews)
}
}
private fun configureViews(context: Context, appWidgetIds: IntArray, size: Pair<Int, Int>? = null): RemoteViews =
RemoteViews(context.packageName, R.layout.widget_bitmap).apply {
val bitmap = CanvasText.drawText(context, size = size)
setImageViewBitmap(R.id.bitmap_view, bitmap)
setOnClickPendingIntent(
R.id.bmp_root_layout,
Intent(context, TextCanvasWidget::class.java).let { intent ->
intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds)
PendingIntent.getBroadcast(
context, Random.nextInt(), intent, PendingIntent.FLAG_IMMUTABLE
)
}
)
}
override fun onAppWidgetOptionsChanged(
context: Context?,
appWidgetManager: AppWidgetManager?,
appWidgetId: Int,
newOptions: Bundle?
) {
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)
val width = (newOptions?.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH) ?: 0)
val height = (newOptions?.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT) ?: 0)
// Log.d("ASDFF", (newOptions?.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH)).toString())
context?.let {
val remoteViews = configureViews(context, intArrayOf(appWidgetId), Pair(width, height))
// Tell the AppWidgetManager to perform an update on the current app widget
appWidgetManager?.updateAppWidget(appWidgetId, remoteViews)
}
}
}