Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide a preference to scale all UI text #3248

Merged
merged 7 commits into from
Jun 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions app/src/main/java/com/keylesspalace/tusky/BaseActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
package com.keylesspalace.tusky;

import android.app.ActivityManager;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
Expand All @@ -45,6 +47,7 @@
import com.keylesspalace.tusky.di.Injectable;
import com.keylesspalace.tusky.interfaces.AccountSelectionListener;
import com.keylesspalace.tusky.interfaces.PermissionRequester;
import com.keylesspalace.tusky.settings.PrefKeys;
import com.keylesspalace.tusky.util.ThemeUtils;

import java.util.ArrayList;
Expand All @@ -54,6 +57,7 @@
import javax.inject.Inject;

public abstract class BaseActivity extends AppCompatActivity implements Injectable {
private static final String TAG = "BaseActivity";

@Inject
public AccountManager accountManager;
Expand Down Expand Up @@ -93,6 +97,44 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
requesters = new HashMap<>();
}

@Override
protected void attachBaseContext(Context newBase) {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(newBase);

// Scale text in the UI from PrefKeys.UI_TEXT_SCALE_RATIO
float uiScaleRatio = preferences.getFloat(PrefKeys.UI_TEXT_SCALE_RATIO, 100F);

Configuration configuration = newBase.getResources().getConfiguration();

// Adjust `fontScale` in the configuration.
//
// You can't repeatedly adjust the `fontScale` in `newBase` because that will contain the
// result of previous adjustments. E.g., going from 100% to 80% to 100% does not return
// you to the original 100%, it leaves it at 80%.
//
// Instead, calculate the new scale from the application context. This is unaffected by
// changes to the base context. It does contain contain any changes to the font scale from
// "Settings > Display > Font size" in the device settings, so scaling performed here
// is in addition to any scaling in the device settings.
Configuration appConfiguration = newBase.getApplicationContext().getResources().getConfiguration();

// This only adjusts the fonts, anything measured in `dp` is unaffected by this.
// You can try to adjust `densityDpi` as shown in the commented out code below. This
// works, to a point. However, dialogs do not react well to this. Beyond a certain
// scale (~ 120%) the right hand edge of the dialog will clip off the right of the
// screen.
//
// So for now, just adjust the font scale
//
// val displayMetrics = appContext.resources.displayMetrics
// configuration.densityDpi = ((displayMetrics.densityDpi * uiScaleRatio).toInt())
configuration.fontScale = appConfiguration.fontScale * uiScaleRatio / 100F;

Context fontScaleContext = newBase.createConfigurationContext(configuration);

super.attachBaseContext(fontScaleContext);
}

protected boolean requiresLogin() {
return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,9 @@ class PreferencesActivity :
}

onBackPressedDispatcher.addCallback(this, restartActivitiesOnBackPressedCallback)
restartActivitiesOnBackPressedCallback.isEnabled = savedInstanceState?.getBoolean(EXTRA_RESTART_ON_BACK, false) ?: false
restartActivitiesOnBackPressedCallback.isEnabled = intent.extras?.getBoolean(
EXTRA_RESTART_ON_BACK
) ?: savedInstanceState?.getBoolean(EXTRA_RESTART_ON_BACK, false) ?: false
}

override fun onPreferenceStartFragment(
Expand Down Expand Up @@ -151,6 +153,10 @@ class PreferencesActivity :
restartActivitiesOnBackPressedCallback.isEnabled = true
this.restartCurrentActivity()
}
PrefKeys.UI_TEXT_SCALE_RATIO -> {
restartActivitiesOnBackPressedCallback.isEnabled = true
this.restartCurrentActivity()
}
"statusTextSize", "absoluteTimeView", "showBotOverlay", "animateGifAvatars", "useBlurhash",
"showSelfUsername", "showCardsInTimelines", "confirmReblogs", "confirmFavourites",
"enableSwipeForTabs", "mainNavPosition", PrefKeys.HIDE_TOP_TOOLBAR, PrefKeys.SHOW_STATS_INLINE -> {
Expand All @@ -175,7 +181,8 @@ class PreferencesActivity :
override fun androidInjector() = androidInjector

companion object {

@Suppress("unused")
private const val TAG = "PreferencesActivity"
const val GENERAL_PREFERENCES = 0
const val ACCOUNT_PREFERENCES = 1
const val NOTIFICATION_PREFERENCES = 2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import com.keylesspalace.tusky.settings.listPreference
import com.keylesspalace.tusky.settings.makePreferenceScreen
import com.keylesspalace.tusky.settings.preference
import com.keylesspalace.tusky.settings.preferenceCategory
import com.keylesspalace.tusky.settings.sliderPreference
import com.keylesspalace.tusky.settings.switchPreference
import com.keylesspalace.tusky.util.LocaleManager
import com.keylesspalace.tusky.util.deserialize
Expand Down Expand Up @@ -99,6 +100,19 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
preferenceDataStore = localeManager
}

sliderPreference {
key = PrefKeys.UI_TEXT_SCALE_RATIO
setDefaultValue(100F)
valueTo = 150F
valueFrom = 50F
stepSize = 5F
setTitle(R.string.pref_ui_text_size)
format = "%.0f%%"
decrementIcon = makeIcon(GoogleMaterial.Icon.gmd_zoom_out)
incrementIcon = makeIcon(GoogleMaterial.Icon.gmd_zoom_in)
icon = makeIcon(GoogleMaterial.Icon.gmd_format_size)
}

listPreference {
setDefaultValue("medium")
setEntries(R.array.post_text_size_names)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,7 @@ object PrefKeys {

const val TAB_FILTER_HOME_REPLIES = "tabFilterHomeReplies_v2" // This was changed once to reset an unintentionally set default.
const val TAB_FILTER_HOME_BOOSTS = "tabFilterHomeBoosts"

/** UI text scaling factor, stored as float, 100 = 100% = no scaling */
const val UI_TEXT_SCALE_RATIO = "uiTextScaleRatio"
}
10 changes: 10 additions & 0 deletions app/src/main/java/com/keylesspalace/tusky/settings/SettingsDSL.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreference
import com.keylesspalace.tusky.view.SliderPreference
import de.c1710.filemojicompat_ui.views.picker.preference.EmojiPickerPreference

class PreferenceParent(
Expand Down Expand Up @@ -43,6 +44,15 @@ inline fun <A> PreferenceParent.emojiPreference(activity: A, builder: EmojiPicke
return pref
}

inline fun PreferenceParent.sliderPreference(
builder: SliderPreference.() -> Unit
): SliderPreference {
val pref = SliderPreference(context)
builder(pref)
addPref(pref)
return pref
}

inline fun PreferenceParent.switchPreference(
builder: SwitchPreference.() -> Unit
): SwitchPreference {
Expand Down
185 changes: 185 additions & 0 deletions app/src/main/java/com/keylesspalace/tusky/view/SliderPreference.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package com.keylesspalace.tusky.view

import android.content.Context
import android.content.res.TypedArray
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.View.VISIBLE
import androidx.appcompat.content.res.AppCompatResources
import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder
import com.google.android.material.slider.LabelFormatter.LABEL_GONE
import com.google.android.material.slider.Slider
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.PrefSliderBinding
import java.lang.Float.max
import java.lang.Float.min

/**
* Slider preference
*
* Similar to [androidx.preference.SeekBarPreference], but better because:
*
* - Uses a [Slider] instead of a [android.widget.SeekBar]. Slider supports float values, and step sizes
* other than 1.
* - Displays the currently selected value in the Preference's summary, for consistency
* with platform norms.
* - Icon buttons can be displayed at the start/end of the slider. Pressing them will
* increment/decrement the slider by `stepSize`.
* - User can supply a custom formatter to format the summary value
*/
class SliderPreference @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = androidx.preference.R.attr.preferenceStyle,
defStyleRes: Int = 0
) : Preference(context, attrs, defStyleAttr, defStyleRes),
Slider.OnChangeListener,
Slider.OnSliderTouchListener {

/** Backing property for `value` */
private var _value = 0F

/**
* @see Slider.getValue
* @see Slider.setValue
*/
var value: Float = defaultValue
get() = _value
set(v) {
val clamped = max(max(v, valueFrom), min(v, valueTo))
if (clamped == field) return
_value = clamped
persistFloat(v)
notifyChanged()
}

/** @see Slider.setValueFrom */
var valueFrom: Float

/** @see Slider.setValueTo */
var valueTo: Float

/** @see Slider.setStepSize */
var stepSize: Float

/**
* Format string to be applied to values before setting the summary. For more control set
* [SliderPreference.formatter]
*/
var format: String = defaultFormat

/**
* Function that will be used to format the summary. The default formatter formats using the
* value of the [SliderPreference.format] property.
*/
var formatter: (Float) -> String = { format.format(it) }

/**
* Optional icon to show in a button at the start of the slide. If non-null the button is
* shown. Clicking the button decrements the value by one step.
*/
var decrementIcon: Drawable? = null
nikclayton marked this conversation as resolved.
Show resolved Hide resolved

/**
* Optional icon to show in a button at the end of the slider. If non-null the button is
* shown. Clicking the button increments the value by one step.
*/
var incrementIcon: Drawable? = null
nikclayton marked this conversation as resolved.
Show resolved Hide resolved

/** View binding */
private lateinit var binding: PrefSliderBinding

init {
// Using `widgetLayoutResource` here would be incorrect, as that tries to put the entire
// preference layout to the right of the title and summary.
layoutResource = R.layout.pref_slider

val a = context.obtainStyledAttributes(attrs, R.styleable.SliderPreference, defStyleAttr, defStyleRes)

value = a.getFloat(R.styleable.SliderPreference_android_value, defaultValue)
valueFrom = a.getFloat(R.styleable.SliderPreference_android_valueFrom, defaultValueFrom)
valueTo = a.getFloat(R.styleable.SliderPreference_android_valueTo, defaultValueTo)
stepSize = a.getFloat(R.styleable.SliderPreference_android_stepSize, defaultStepSize)
format = a.getString(R.styleable.SliderPreference_format) ?: defaultFormat

val decrementIconResource = a.getResourceId(R.styleable.SliderPreference_iconStart, -1)
if (decrementIconResource != -1) {
decrementIcon = AppCompatResources.getDrawable(context, decrementIconResource)
}

val incrementIconResource = a.getResourceId(R.styleable.SliderPreference_iconEnd, -1)
if (incrementIconResource != -1) {
incrementIcon = AppCompatResources.getDrawable(context, incrementIconResource)
}

a.recycle()
}

override fun onGetDefaultValue(a: TypedArray, i: Int): Any {
return a.getFloat(i, defaultValue)
}

override fun onSetInitialValue(defaultValue: Any?) {
value = getPersistedFloat((defaultValue ?: Companion.defaultValue) as Float)
}

override fun onBindViewHolder(holder: PreferenceViewHolder) {
super.onBindViewHolder(holder)
binding = PrefSliderBinding.bind(holder.itemView)

binding.root.isClickable = false

binding.slider.addOnChangeListener(this)
binding.slider.addOnSliderTouchListener(this)
binding.slider.value = value // sliderValue
binding.slider.valueTo = valueTo
binding.slider.valueFrom = valueFrom
binding.slider.stepSize = stepSize

// Disable the label, the value is shown in the preference summary
binding.slider.labelBehavior = LABEL_GONE
binding.slider.isEnabled = isEnabled

binding.summary.visibility = VISIBLE
binding.summary.text = formatter(value)

decrementIcon?.let { icon ->
binding.decrement.icon = icon
binding.decrement.visibility = VISIBLE
binding.decrement.setOnClickListener {
value -= stepSize
}
}

incrementIcon?.let { icon ->
binding.increment.icon = icon
binding.increment.visibility = VISIBLE
binding.increment.setOnClickListener {
value += stepSize
}
}
}

override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) {
if (!fromUser) return
binding.summary.text = formatter(value)
}

override fun onStartTrackingTouch(slider: Slider) {
// Deliberately empty
}

override fun onStopTrackingTouch(slider: Slider) {
value = slider.value
}

companion object {
private const val TAG = "SliderPreference"
private const val defaultValueFrom = 0F
private const val defaultValueTo = 1F
private const val defaultValue = 0.5F
private const val defaultStepSize = 0.1F
private const val defaultFormat = "%3.1f"
}
}
Loading