Skip to content

Commit

Permalink
feat: Allow the user to choose a different font
Browse files Browse the repository at this point in the history
Android's choices for font customisation can be limited, depending on
the vendor. Allow users to choose from a small collection of embedded
fonts, chosen by asking users for recommendations.

The font choice is implemented as a preference. Provide a custom dialog
that shows the fonts (in that font) so the user can see what they're
choosing between.

Ensure the font's license information is displayed in the "About"
section.
  • Loading branch information
nikclayton committed Sep 4, 2023
1 parent dfc16c0 commit 6539146
Show file tree
Hide file tree
Showing 66 changed files with 616 additions and 68 deletions.
116 changes: 58 additions & 58 deletions app/lint-baseline.xml

Large diffs are not rendered by default.

11 changes: 9 additions & 2 deletions app/src/main/java/com/keylesspalace/tusky/BaseActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.annotation.StyleRes;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
Expand All @@ -48,6 +49,7 @@
import com.keylesspalace.tusky.interfaces.AccountSelectionListener;
import com.keylesspalace.tusky.interfaces.PermissionRequester;
import com.keylesspalace.tusky.settings.PrefKeys;
import com.keylesspalace.tusky.util.EmbeddedFontFamily;
import com.keylesspalace.tusky.util.ThemeUtils;

import java.util.ArrayList;
Expand Down Expand Up @@ -89,9 +91,14 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {

setTaskDescription(new ActivityManager.TaskDescription(appName, appIcon, recentsBackgroundColor));

int style = textStyle(preferences.getString("statusTextSize", "medium"));
int style = textStyle(preferences.getString(PrefKeys.STATUS_TEXT_SIZE, "medium"));
getTheme().applyStyle(style, true);

EmbeddedFontFamily fontFamily = EmbeddedFontFamily.Companion.from(preferences.getString(PrefKeys.FONT_FAMILY, "default"));
if (fontFamily != EmbeddedFontFamily.DEFAULT) {
getTheme().applyStyle(fontFamily.getStyle(), true);
}

if(requiresLogin()) {
redirectIfNotLoggedIn();
}
Expand Down Expand Up @@ -141,7 +148,7 @@ protected boolean requiresLogin() {
return true;
}

private static int textStyle(String name) {
private static @StyleRes int textStyle(String name) {
int style;
switch (name) {
case "smallest":
Expand Down
22 changes: 22 additions & 0 deletions app/src/main/java/com/keylesspalace/tusky/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import android.app.NotificationManager
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.Color
Expand All @@ -46,6 +47,7 @@ import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.IntentCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.GravityCompat
import androidx.core.view.MenuProvider
import androidx.core.view.forEach
Expand Down Expand Up @@ -97,6 +99,7 @@ import com.keylesspalace.tusky.pager.MainPagerAdapter
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.usecase.DeveloperToolsUseCase
import com.keylesspalace.tusky.usecase.LogoutUsecase
import com.keylesspalace.tusky.util.EmbeddedFontFamily
import com.keylesspalace.tusky.util.deleteStaleCachedMedia
import com.keylesspalace.tusky.util.emojify
import com.keylesspalace.tusky.util.getDimension
Expand All @@ -121,6 +124,7 @@ import com.mikepenz.materialdrawer.model.ProfileDrawerItem
import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem
import com.mikepenz.materialdrawer.model.SecondaryDrawerItem
import com.mikepenz.materialdrawer.model.interfaces.IProfile
import com.mikepenz.materialdrawer.model.interfaces.Typefaceable
import com.mikepenz.materialdrawer.model.interfaces.descriptionRes
import com.mikepenz.materialdrawer.model.interfaces.descriptionText
import com.mikepenz.materialdrawer.model.interfaces.iconRes
Expand Down Expand Up @@ -685,6 +689,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
)
}

updateMainDrawerTypeface(preferences)
}

private fun buildDeveloperToolsDialog(): AlertDialog {
Expand All @@ -710,6 +716,22 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
.create()
}

/**
* The drawer library forces the `android:fontFamily` attribute, overriding the value in the
* theme. Force-ably set the typeface for everything in the drawer if using a non-default font.
*/
private fun updateMainDrawerTypeface(preferences: SharedPreferences) {
val fontFamily = EmbeddedFontFamily.from(preferences.getString(PrefKeys.FONT_FAMILY, "default"))
if (fontFamily == EmbeddedFontFamily.DEFAULT) return

val typeface = ResourcesCompat.getFont(this, fontFamily.font) ?: return
for (i in 0..binding.mainDrawer.adapter.itemCount) {
val item = binding.mainDrawer.adapter.getItem(i)
if (item !is Typefaceable) continue
item.typeface = typeface
}
}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(binding.mainDrawer.saveInstanceState(outState))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ class PreferencesActivity :
restartActivitiesOnBackPressedCallback.isEnabled = true
this.restartCurrentActivity()
}
PrefKeys.UI_TEXT_SCALE_RATIO -> {
PrefKeys.FONT_FAMILY, PrefKeys.UI_TEXT_SCALE_RATIO -> {
restartActivitiesOnBackPressedCallback.isEnabled = true
this.restartCurrentActivity()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import com.keylesspalace.tusky.util.deserialize
import com.keylesspalace.tusky.util.makeIcon
import com.keylesspalace.tusky.util.serialize
import com.keylesspalace.tusky.util.unsafeLazy
import com.keylesspalace.tusky.view.FontFamilyDialogFragment
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import de.c1710.filemojicompat_ui.views.picker.preference.EmojiPickerPreference
Expand Down Expand Up @@ -113,6 +114,16 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
icon = makeIcon(GoogleMaterial.Icon.gmd_format_size)
}

listPreference {
setDefaultValue("default")
setEntries(R.array.pref_font_family_names)
setEntryValues(R.array.pref_font_family_values)
key = PrefKeys.FONT_FAMILY
setSummaryProvider { entry }
setTitle(R.string.pref_title_font_family)
icon = makeIcon(GoogleMaterial.Icon.gmd_font_download)
}

listPreference {
setDefaultValue("medium")
setEntries(R.array.post_text_size_names)
Expand Down Expand Up @@ -312,6 +323,12 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
}

override fun onDisplayPreferenceDialog(preference: Preference) {
if (PrefKeys.FONT_FAMILY == preference.key) {
val fragment = FontFamilyDialogFragment.newInstance(PrefKeys.FONT_FAMILY)
fragment.setTargetFragment(this, 0)
fragment.show(parentFragmentManager, FontFamilyDialogFragment.TXN_TAG)
return
}
if (!EmojiPickerPreference.onDisplayPreferenceDialog(this, preference)) {
super.onDisplayPreferenceDialog(preference)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ object PrefKeys {
const val EMOJI = "selected_emoji_font"
const val FAB_HIDE = "fabHide"
const val LANGUAGE = "language"
const val FONT_FAMILY = "fontFamily"
const val STATUS_TEXT_SIZE = "statusTextSize"
const val READING_ORDER = "readingOrder"
const val MAIN_NAV_POSITION = "mainNavPosition"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright 2023 Pachli Association
*
* This file is a part of Pachli.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Pachli; if not,
* see <http://www.gnu.org/licenses>.
*/

package com.keylesspalace.tusky.util

import androidx.annotation.FontRes
import androidx.annotation.StyleRes
import com.keylesspalace.tusky.R

enum class EmbeddedFontFamily(@FontRes val font: Int, @StyleRes val style: Int) {
DEFAULT(-1, -1),
ATKINSON_HYPERLEGIBLE(R.font.atkinson_hyperlegible, R.style.FontAtkinsonHyperlegible),
COMICNEUE(R.font.comicneue, R.style.FontComicNeue),
ESTEDAD(R.font.estedad, R.style.FontEstedad),
LEXEND(R.font.lexend, R.style.FontLexend),
LUCIOLE(R.font.luciole, R.style.FontLuciole),
OPENDYSLEXIC(R.font.opendyslexic, R.style.FontOpenDyslexic);

companion object {
fun from(s: String?): EmbeddedFontFamily {
s ?: return DEFAULT

return try {
valueOf(s.uppercase())
} catch (_: Throwable) {
DEFAULT
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* Copyright 2023 Pachli Association
*
* This file is a part of Pachli.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Pachli; if not,
* see <http://www.gnu.org/licenses>.
*/

package com.keylesspalace.tusky.view

import android.content.DialogInterface.BUTTON_POSITIVE
import android.graphics.Typeface
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.TextView
import androidx.appcompat.R
import androidx.appcompat.app.AlertDialog
import androidx.preference.ListPreference
import androidx.preference.ListPreferenceDialogFragmentCompat
import com.keylesspalace.tusky.util.EmbeddedFontFamily

/**
* Dialog fragment for choosing a font family. Displays the list of font families with each
* entry in its font.
*/
class FontFamilyDialogFragment : ListPreferenceDialogFragmentCompat() {
private var clickedDialogEntryIndex = 0
private lateinit var entries: Array<CharSequence>
private lateinit var entryValues: Array<CharSequence>

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
val preference = preference as ListPreference
check(!(preference.entries == null || preference.entryValues == null)) {
"ListPreference requires an entries array and an entryValues array."
}
clickedDialogEntryIndex = preference.findIndexOfValue(preference.value)
entries = preference.entries
entryValues = preference.entryValues
} else {
clickedDialogEntryIndex = savedInstanceState.getInt(SAVE_STATE_INDEX, 0)
entries = savedInstanceState.getCharSequenceArray(SAVE_STATE_ENTRIES) as Array<CharSequence>
entryValues = savedInstanceState.getCharSequenceArray(SAVE_STATE_ENTRY_VALUES) as Array<CharSequence>
}
}

override fun onPrepareDialogBuilder(builder: AlertDialog.Builder) {
super.onPrepareDialogBuilder(builder)

val context = requireContext()

// Use the same layout AlertDialog uses, as android.R.layout.simple_list_item_single_choice
// puts the radio button at the end of the line, but the default dialog style puts it at
// the start.
val a = context.obtainStyledAttributes(
null,
R.styleable.AlertDialog,
R.attr.alertDialogStyle,
0
)
val layout = a.getResourceId(R.styleable.AlertDialog_singleChoiceItemLayout, 0)
a.recycle()

val adapter = object : ArrayAdapter<CharSequence>(context, layout, entries) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = super.getView(position, convertView, parent)

val fontFamily = EmbeddedFontFamily.from(entryValues[position].toString())
if (fontFamily == EmbeddedFontFamily.DEFAULT) {
(view as TextView).typeface = Typeface.DEFAULT
} else {
(view as TextView).setTextAppearance(fontFamily.style)
}
return view
}
}

builder.setSingleChoiceItems(adapter, clickedDialogEntryIndex) { dialog, which ->
clickedDialogEntryIndex = which
this@FontFamilyDialogFragment.onClick(dialog, BUTTON_POSITIVE)
dialog.dismiss()
}

// The typical interaction for list-based dialogs is to have click-on-an-item dismiss the
// dialog instead of the user having to press 'Ok'.
builder.setPositiveButton(null, null)
}

override fun onDialogClosed(positiveResult: Boolean) {
if (positiveResult && clickedDialogEntryIndex >= 0) {
val value = entryValues[clickedDialogEntryIndex].toString()
val preference = preference as ListPreference
if (preference.callChangeListener(value)) {
preference.value = value
}
}
}

companion object {
const val TXN_TAG = "com.keylesspalace.tusky.view.FontFamilyDialogFragment"
const val SAVE_STATE_INDEX = "FontFamilyDialogFragment.index"
const val SAVE_STATE_ENTRIES = "FontFamilyDialogFragment.entries"
const val SAVE_STATE_ENTRY_VALUES = "FontFamilyDialogFragment.entryValues"

fun newInstance(key: String): FontFamilyDialogFragment {
val fragment = FontFamilyDialogFragment()
val b = Bundle(1)
b.putString(ARG_KEY, key)
fragment.arguments = b
return fragment
}
}
}
20 changes: 20 additions & 0 deletions app/src/main/res/font/atkinson_hyperlegible.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<font-family xmlns:app="http://schemas.android.com/apk/res-auto">
<font
app:fontStyle="normal"
app:fontWeight="400"
app:font="@font/atkinson_hyperlegible_regular_102" />
<font
app:fontStyle="italic"
app:fontWeight="400"
app:font="@font/atkinson_hyperlegible_italic_102" />
<font
app:fontStyle="normal"
app:fontWeight="700"
app:font="@font/atkinson_hyperlegible_bold_102" />
<font
app:fontStyle="italic"
app:fontWeight="700"
app:font="@font/atkinson_hyperlegible_bolditalic_102" />

</font-family>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
27 changes: 27 additions & 0 deletions app/src/main/res/font/comicneue.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<font-family xmlns:app="http://schemas.android.com/apk/res-auto">
<font
app:fontStyle="normal"
app:fontWeight="300"
app:font="@font/comicneue_light" />
<font
app:fontStyle="italic"
app:fontWeight="300"
app:font="@font/comicneue_lightitalic" />
<font
app:fontStyle="normal"
app:fontWeight="400"
app:font="@font/comicneue_regular" />
<font
app:fontStyle="italic"
app:fontWeight="400"
app:font="@font/comicneue_light" />
<font
app:fontStyle="normal"
app:fontWeight="700"
app:font="@font/comicneue_bold" />
<font
app:fontStyle="italic"
app:fontWeight="700"
app:font="@font/comicneue_bolditalic" />
</font-family>
Binary file added app/src/main/res/font/comicneue_bold.otf
Binary file not shown.
Binary file added app/src/main/res/font/comicneue_bolditalic.otf
Binary file not shown.
Binary file added app/src/main/res/font/comicneue_italic.otf
Binary file not shown.
Binary file added app/src/main/res/font/comicneue_light.otf
Binary file not shown.
Binary file added app/src/main/res/font/comicneue_lightitalic.otf
Binary file not shown.
Binary file added app/src/main/res/font/comicneue_regular.otf
Binary file not shown.
Loading

0 comments on commit 6539146

Please sign in to comment.