Have only one size for multiple auto-size TextViews
Asked Answered
Z

5

11

I have a layout with different boxes, each of them contains a bunch of TextViews in a similar layout.

I wish to use the auto-size feature of TextView, but each TextView only takes into account its own boundaries, and there is no way to enforce the same size on multiple auto-size TextViews that represent a similar element in a layout.

Ideally, I would like to be able to "chain" multiple TextView objects (located in completely different places), so the auto-size mechanism knows that they should all have the same text size (stick to minimum, since one text can be longer than the others).

Zaller answered 21/9, 2018 at 12:31 Comment(0)
W
17

Updated:

I have developed a size aware TextView for your requirement. It notifies a listener when text size has changed. I have tested it and it works well. I hope it helps you.

SizeAwareTextView.java:

package com.aminography.textapp;

import android.content.Context;
import android.support.v7.widget.AppCompatTextView;
import android.util.AttributeSet;

public class SizeAwareTextView extends AppCompatTextView {

    private OnTextSizeChangedListener mOnTextSizeChangedListener;
    private float mLastTextSize;

    public SizeAwareTextView(Context context) {
        super(context);
        mLastTextSize = getTextSize();
    }

    public SizeAwareTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mLastTextSize = getTextSize();
    }

    public SizeAwareTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mLastTextSize = getTextSize();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mLastTextSize != getTextSize()) {
            mLastTextSize = getTextSize();
            if (mOnTextSizeChangedListener != null) {
                mOnTextSizeChangedListener.onTextSizeChanged(this, mLastTextSize);
            }
        }
    }

    public void setOnTextSizeChangedListener(OnTextSizeChangedListener onTextSizeChangedListener) {
        mOnTextSizeChangedListener = onTextSizeChangedListener;
    }

    public interface OnTextSizeChangedListener {

        void onTextSizeChanged(SizeAwareTextView view, float textSize);
    }
}

MainActivity.java

package com.aminography.textapp;

import android.annotation.SuppressLint;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.TypedValue;
import android.widget.EditText;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final SizeAwareTextView textView1 = findViewById(R.id.textView1);
        final SizeAwareTextView textView2 = findViewById(R.id.textView2);
        final SizeAwareTextView textView3 = findViewById(R.id.textView3);

        final List<SizeAwareTextView> textViewList = new ArrayList<>();
        textViewList.add(textView1);
        textViewList.add(textView2);
        textViewList.add(textView3);

        SizeAwareTextView.OnTextSizeChangedListener onTextSizeChangedListener = new SizeAwareTextView.OnTextSizeChangedListener() {
            @SuppressLint("RestrictedApi")
            @Override
            public void onTextSizeChanged(SizeAwareTextView view, float textSize) {
                for (SizeAwareTextView textView : textViewList) {
                    if (!textView.equals(view) && textView.getTextSize() != view.getTextSize()) {
                        textView.setAutoSizeTextTypeUniformWithPresetSizes(new int[]{(int) textSize}, TypedValue.COMPLEX_UNIT_PX);
                    }
                }
            }
        };

        for (SizeAwareTextView textView : textViewList) {
            textView.setOnTextSizeChangedListener(onTextSizeChangedListener);
        }

        ((EditText) findViewById(R.id.editText)).addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
            }

            @Override
            public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
            }

            @Override
            public void afterTextChanged(Editable editable) {
                textView1.setText(editable.toString());
            }
        });
    }

}

activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="top"
    android:orientation="vertical"
    android:padding="16dp"
    tools:context=".MainActivity">

    <com.aminography.textapp.SizeAwareTextView
        android:id="@+id/textView1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#DEDEDE"
        android:text="Here is the first TextView"
        android:textSize="26sp"
        app:autoSizeMinTextSize="10sp"
        app:autoSizeStepGranularity="0.5sp"
        app:autoSizeTextType="uniform" />

    <com.aminography.textapp.SizeAwareTextView
        android:id="@+id/textView2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:background="#DEDEDE"
        android:text="Here is the second TextView"
        android:textSize="26sp"
        app:autoSizeMinTextSize="10sp"
        app:autoSizeStepGranularity="0.5sp"
        app:autoSizeTextType="uniform" />

    <com.aminography.textapp.SizeAwareTextView
        android:id="@+id/textView3"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:background="#DEDEDE"
        android:text="Here is the third TextView"
        android:textSize="26sp"
        app:autoSizeMinTextSize="10sp"
        app:autoSizeStepGranularity="0.5sp"
        app:autoSizeTextType="uniform" />

    <android.support.v7.widget.AppCompatEditText
        android:id="@+id/editText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="32dp"
        android:text="Here is the first TextView" />

</LinearLayout>

Final result:

enter image description here

Whiteman answered 21/9, 2018 at 14:26 Comment(11)
@SirKnigget: I have changed it a bit, and the result is shown also. Changing in EditText, changes the text of first TextView and consequently text size of other TextViews.Whiteman
Added again :-)Whiteman
Again, I don&#39;t need a callback that happens when text changed. I need to monitor text SIZE changes that happen internally in TextView - not while setting it from the outside.Zaller
What do you mean about SIZE? You mean text length? Because above solution works even if text was changed internally. According to TextView class codes, I think there is no way for listening to text changes without customization like that.Whiteman
Text size. Not text length.Zaller
All text size changes is done via 'setRawTextSize' method which is private and you can't listen to its calling.Whiteman
How about another solution? Like checking size after each onDraw call.Zaller
It's ok, I put it in onDraw method with some changes in MainActivity.Whiteman
I would appreciate if I receive feedback from you.Whiteman
It's closer to what I was looking for - still refining my solution. Notice that I need to maintain a minimum, not a maximum (in case one TextView in the list has to shrink - all should shrink. in case one grows above others - then this should be prevented.)Zaller
Note for others that setOnTextSizeChangedListener have to be called before the view to render. Using this method after the first render will not trigger with the current size.Sidelight
W
5

I modified this so that you can use it straight from XML without any code in Fragment or Activity

You need to add into values/attrs.xml the following snippet:

<declare-styleable name="SizeAwareTextView">
    <attr name="group" format="reference"/>
</declare-styleable>

Into values/arrays.xml declare the label ids which belong into the same group

<array name="labels">
    <item>@id/label1</item>
    <item>@id/label2</item>
    <item>@id/label3</item>
</array>

Then when declaring the view use group attribute to reference the labels:

        <SizeAwareTextView
            android:id="@+id/label1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:textColor="@color/white"
            android:textSize="14sp"
            android:textAllCaps="true"
            android:maxLines="1"
            app:autoSizeTextType="uniform"
            app:autoSizeMaxTextSize="15sp"
            app:group="@array/labels"
            android:text="@string/very_long_string"/>

Below is the modified SizeAwareTextView

class SizeAwareTextView: AppCompatTextView {

    private var lastTextSize: Float = 0F
    private var viewRefs: TypedArray? = null
    private var views = mutableListOf<SizeAwareTextView>()
    var onTextSizeChangedListener: OnTextSizeChangedListener? = object : OnTextSizeChangedListener {
        @SuppressLint("RestrictedApi")
        override fun onTextSizeChanged(view: SizeAwareTextView, textSize: Float) {
            resolveViews()
            views.forEach {
                if (view != it && view.textSize != it.textSize) {
                    it.setAutoSizeTextTypeUniformWithPresetSizes(intArrayOf(textSize.toInt()), TypedValue.COMPLEX_UNIT_PX)
                }
            }
        }
    }

    constructor(context: Context) : super(context) {
        lastTextSize = textSize
    }

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
        lastTextSize = textSize
        val a = context.obtainStyledAttributes(attrs, R.styleable.SizeAwareTextView)
        a.getResourceId(R.styleable.SizeAwareTextView_group, 0).let {
            if (it > 0) {
                viewRefs = resources.obtainTypedArray(it)
            }
        }
        a.recycle()
    }

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
        lastTextSize = textSize
        val a = context.obtainStyledAttributes(attrs, R.styleable.SizeAwareTextView)
        a.getResourceId(R.styleable.SizeAwareTextView_group, 0).let {
            if (it > 0) {
                viewRefs = resources.obtainTypedArray(it)
            }
        }
        a.recycle()
    }

    fun resolveViews() {
        viewRefs?.let {
            var root = parent
            while (root.parent is View) {
                root = root.parent
            }
            for (i in 0 until it.length()) {
                val resId = it.getResourceId(i, 0)
                val v = (root as View).findViewById<SizeAwareTextView>(resId)
                if (v != null) {
                    views.add(v)
                } else {
                    Log.w(TAG, "Resource: $resId not found at idx: $i")
                }
            }
            it.recycle()
            viewRefs = null
        }
    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        if (lastTextSize != textSize) {
            lastTextSize = textSize
            onTextSizeChangedListener?.onTextSizeChanged(this, lastTextSize)
        }
    }

    interface OnTextSizeChangedListener {
        fun onTextSizeChanged(view: SizeAwareTextView, textSize: Float)
    }

    companion object {
        val TAG = SizeAwareTextView::class.java.simpleName
    }
}
Wayward answered 14/7, 2020 at 15:10 Comment(0)
S
2

This is a little bit different (I think) than what the OP may have been looking for, but what I needed was for a particular view that contained multiple TextView objects, once the layout had been determined to have the size of the smallest TextView become the size for all the TextViews. So I did this and put it in in the OnViewCreated() method of the fragment where my TextViews live:

@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);

    view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {

        @Override
        public void onGlobalLayout() {
            Timber.d("Lifecycle: In onViewCreated() of WelcomeFragment adjusting text fields");
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
            } else {
                //noinspection deprecation
                view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
            }

            // Get all text views and find the smallest size and set them all to that
            int childCount = ((ViewGroup)view).getChildCount();
            float f = -1;
            ArrayList<AppCompatTextView> textViewArrayList = new ArrayList();
            for (int x = 0; x < childCount; x++) {
                View v = ((ViewGroup) view).getChildAt(x);
                if ( v instanceof androidx.appcompat.widget.AppCompatTextView) {
                    textViewArrayList.add((androidx.appcompat.widget.AppCompatTextView)v);
                    if ( f == -1) {
                        // Handle edge case - first TextView found initializes f
                        f = Math.max(f, ((androidx.appcompat.widget.AppCompatTextView) v).getTextSize());
                    } else {
                        f = Math.min(f, ((androidx.appcompat.widget.AppCompatTextView) v).getTextSize());
                    }
                }
            }
            int[] uniformSize = new int[]{(int) f};
            for (int x = 0; x < textViewArrayList.size(); x++) {
                TextViewCompat.setAutoSizeTextTypeUniformWithPresetSizes(textViewArrayList.get(x), uniformSize, TypedValue.COMPLEX_UNIT_PX);
            }
        }
    });
}
Suppository answered 21/2, 2019 at 23:47 Comment(0)
S
1

Here's a solution in kotlin based on aminography's answer:

SizeAwareTextView.kt

package com.example.onesizemultipleauto_sizetextviews

import android.content.Context
import android.graphics.Canvas
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView

class SizeAwareTextView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : AppCompatTextView(context, attrs, defStyleAttr) {

    var onTextSizeChangedListener: OnTextSizeChangedListener? = null
    private var lastTextSize: Float = textSize

    @Override
    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        if (lastTextSize != textSize) {
            lastTextSize = textSize
            if (onTextSizeChangedListener != null) {
                onTextSizeChangedListener?.onTextSizeChanged(this, lastTextSize)
            }
        }
    }

    interface OnTextSizeChangedListener {
        fun onTextSizeChanged(view: SizeAwareTextView, textSize: Float)
    }
}

MainActivity.kt

package com.example.onesizemultipleauto_sizetextviews

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.TypedValue

import android.annotation.SuppressLint
import com.example.onesizemultipleauto_sizetextviews.SizeAwareTextView.OnTextSizeChangedListener


class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val walletUnlocksDescription: SizeAwareTextView = findViewById(R.id.walletUnlocksDescription)
        val walletMinutesDescription: SizeAwareTextView = findViewById(R.id.walletMinutesDescription)

        val textViewList = listOf(walletUnlocksDescription, walletMinutesDescription)

        val onTextSizeChangedListener: OnTextSizeChangedListener =
            object : OnTextSizeChangedListener {
                @SuppressLint("RestrictedApi")
                override fun onTextSizeChanged(view: SizeAwareTextView, textSize: Float) {
                    for (textView in textViewList) {
                        if (textView != view && textView.textSize != view.textSize) {
                            textView.setAutoSizeTextTypeUniformWithPresetSizes(
                                intArrayOf(textSize.toInt()),
                                TypedValue.COMPLEX_UNIT_PX
                            )
                        }
                    }
                }
            }

        textViewList.forEach { sizeAwareTextView ->
            sizeAwareTextView.onTextSizeChangedListener = onTextSizeChangedListener
        }
    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.example.onesizemultipleauto_sizetextviews.SizeAwareTextView
        android:id="@+id/walletUnlocksDescription"
        android:layout_width="0dp"
        android:layout_height="18dp"
        android:text="This is a long text that autosizes ok S"
        android:autoSizeTextType="uniform"
        android:gravity="start|top"
        android:textAlignment="viewStart"
        android:textSize="18sp"
        app:autoSizeTextType="uniform"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintEnd_toEndOf="@id/minutesGuideline"
        app:layout_constraintHorizontal_bias="0"
        app:layout_constraintStart_toStartOf="@id/unlocksGuideline"
        app:layout_constraintBottom_toBottomOf="parent" />

    <com.example.onesizemultipleauto_sizetextviews.SizeAwareTextView
        android:id="@+id/walletMinutesDescription"
        android:layout_width="0dp"
        android:layout_height="18dp"
        android:layout_marginEnd="44dp"
        android:gravity="start|top"
        android:textAlignment="viewStart"
        android:textSize="18sp"
        android:autoSizeTextType="uniform"
        android:text="Short text"
        app:layout_constraintBaseline_toBaselineOf="@id/walletUnlocksDescription"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="@id/minutesGuideline"
        app:layout_constraintTop_toTopOf="parent" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/unlocksGuideline"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintGuide_begin="110dp" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/minutesGuideline"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintGuide_end="100dp" />

</androidx.constraintlayout.widget.ConstraintLayout>
Soracco answered 7/7, 2021 at 11:20 Comment(0)
A
1

I wanted to say thank you @aminography, @hohteri, and @molundb because I made a different solution based on yours.

Because all texts came from backend, I made a custom view that reacts when its text size is smaller than the first time it was drawn. In that case, it fetches the other views of the same type to reduce their text size too.

If you have the same scenario, firstly register the attribute to store the id references of the same view type in the attr.xml file.

<declare-styleable name="SizeAwareTextView">
    <attr name="textViewGroup" format="reference"/>
</declare-styleable>

In the arrays.xml file, register the ids of the type SizeAwareTextView.

<array name="myScreen">
        <item>@id/first_text</item>
        <item>@id/second_text</item>
</array>

Then, create the SizeAwareTextView class and remove the comments :)

class SizeAwareTextView @JvmOverloads constructor(
    context: Context,
    attributeSet: AttributeSet? = null
) : AppCompatTextView(context, attributeSet) {

    // It will register the initial text size and the smaller size the custom view could have
    private var minTextSize: Float
    private var viewRefs: TypedArray? = null

    init {
        minTextSize = textSize
        val typedArray = context.obtainStyledAttributes(attributeSet, R.styleable.SizeAwareTextView)
        try {
            typedArray.getResourceId(R.styleable.SizeAwareTextView_textViewGroup, 0).let {
                if (it > 0) {
                    viewRefs = resources.obtainTypedArray(it)
                }
            }
        } finally {
            typedArray.recycle()
        }
    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        if (minTextSize != textSize) {

            // If the text size is smaller than the minTextSize, update the value and retrieve the other custom views of the same type
            // Otherwise, if the textSize is bigger than the minTextSize, it means the text changed, so pass the same view
            val sizeAwareTextViewList = if (minTextSize > textSize) {
                minTextSize = textSize
                getSizeAwareViewList()
            } else {
                mutableSetOf(this)
            }
            resizeSizeAwareTextViews(sizeAwareTextViewList)
        }
    }

    private fun getSizeAwareViewList(): MutableSet<SizeAwareTextView> {
        val sizeAwareTextViewList = mutableSetOf<SizeAwareTextView>()
        viewRefs?.let { typedArray ->
            val rootView = getRootView(typedArray, sizeAwareTextViewList)
            setSizeAwareViewsFromRootView(typedArray, rootView as View, sizeAwareTextViewList)
            typedArray.recycle()
            viewRefs = null
        }
        return sizeAwareTextViewList
    }

    private fun getRootView(
        typedArray: TypedArray,
        sizeAwareTextViewList: MutableSet<SizeAwareTextView>
    ): ViewParent {
        var rootView = parent
        while (rootView is View) {

            // We need the root view to locate the other custom views, but we could have any element inside a recycler view. So, it is necessary to analyze every view inside it because it always retrieves the first element from the root view
            verifyRootViewIsRecyclerView(typedArray, rootView as ViewGroup, sizeAwareTextViewList)
            if (rootView.parent is View) {
                rootView = rootView.parent
            } else {
                break
            }
        }
        return rootView
    }

    private fun verifyRootViewIsRecyclerView(
        typedArray: TypedArray,
        rootView: ViewGroup,
        sizeAwareTextViewList: MutableSet<SizeAwareTextView>
    ) {
        rootView.children.forEach {
            if (it is RecyclerView) {
                findSizeAwareViewsInRecyclerView(typedArray, it, sizeAwareTextViewList)
            }
        }
    }

    private fun findSizeAwareViewsInRecyclerView(
        typedArray: TypedArray,
        rootView: RecyclerView,
        sizeAwareTextViewList: MutableSet<SizeAwareTextView>
    ) {
        rootView.children.forEach {
            setSizeAwareViewsFromRootView(typedArray, it, sizeAwareTextViewList)
        }
    }

    private fun setSizeAwareViewsFromRootView(
        typedArray: TypedArray,
        rootView: View,
        sizeAwareTextViewList: MutableSet<SizeAwareTextView>
    ) {
        for (resourceElement in 0 until typedArray.length()) {
            val resId = typedArray.getResourceId(resourceElement, 0)
            rootView.findViewById<SizeAwareTextView>(resId).takeIf { this != it }?.let {
                sizeAwareTextViewList.add(it)
            }
        }
    }

    private fun resizeSizeAwareTextViews(sizeAwareTextViewList: MutableSet<SizeAwareTextView>) {
        sizeAwareTextViewList.forEach { sizeAwareTextView ->
            if (minTextSize < sizeAwareTextView.textSize) {
                TextViewCompat.setAutoSizeTextTypeUniformWithPresetSizes(
                    sizeAwareTextView,
                    intArrayOf(minTextSize.toInt()),
                    TypedValue.COMPLEX_UNIT_PX
                )
            }
            invalidate()
            requestLayout()
        }
    }
}

When you create the SizeAwareTextView custom view in your layout, remember to pass myScreen array to the textViewGroup attribute.

Are you curious why I did not override the third attribute defStyleAttr with @JvmOverloads? We could run into trouble if that attribute is not empty. Check this article out for more!

And that's it. If you have any questions, don't hesitate to ask!

Arresting answered 16/5, 2022 at 8:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.