Skip to content

Instantly share code, notes, and snippets.

@kibotu
Forked from alphamu/TextDrawable.java
Created October 8, 2018 15:34
Show Gist options
  • Save kibotu/180b497de458edff1e1d4b09fd7fe469 to your computer and use it in GitHub Desktop.
Save kibotu/180b497de458edff1e1d4b09fd7fe469 to your computer and use it in GitHub Desktop.
An TextDrawable that can be used to draw a String. This can be used anywhere you would normally use a drawable. You can watch the demo video here: https://youtu.be/l1HgpcJhIi0
/**
* Copyright 2016 Ali Muzaffar
* <p/>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p/>
* http://www.apache.org/licenses/LICENSE-2.0
* <p/>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.text.Editable;
import android.text.TextWatcher;
import android.widget.TextView;
import java.lang.ref.WeakReference;
public class TextDrawable extends Drawable implements TextWatcher {
private WeakReference<TextView> ref;
private String mText;
private Paint mPaint;
private Rect mHeightBounds;
private boolean mBindToViewPaint = false;
private float mPrevTextSize = 0;
private boolean mInitFitText = false;
private boolean mFitTextEnabled = false;
/**
* Create a TextDrawable using the given paint object and string
*
* @param paint
* @param s
*/
public TextDrawable(Paint paint, String s) {
mText = s;
mPaint = new Paint(paint);
mHeightBounds = new Rect();
init();
}
/**
* Create a TextDrawable. This uses the given TextView to initialize paint and has initial text
* that will be drawn. Initial text can also be useful for reserving space that may otherwise
* not be available when setting compound drawables.
*
* @param tv The TextView / EditText using to initialize this drawable
* @param initialText Optional initial text to display
* @param bindToViewsText Should this drawable mirror the text in the TextView
* @param bindToViewsPaint Should this drawable mirror changes to Paint in the TextView, like textColor, typeface, alpha etc.
* Note, this will override any changes made using setColorFilter or setAlpha.
*/
public TextDrawable(TextView tv, String initialText, boolean bindToViewsText, boolean bindToViewsPaint) {
this(tv.getPaint(), initialText);
ref = new WeakReference<>(tv);
if (bindToViewsText || bindToViewsPaint) {
if (bindToViewsText) {
tv.addTextChangedListener(this);
}
mBindToViewPaint = bindToViewsPaint;
}
}
/**
* Create a TextDrawable. This uses the given TextView to initialize paint and the text that
* will be drawn.
*
* @param tv The TextView / EditText using to initialize this drawable
* @param bindToViewsText Should this drawable mirror the text in the TextView
* @param bindToViewsPaint Should this drawable mirror changes to Paint in the TextView, like textColor, typeface, alpha etc.
* Note, this will override any changes made using setColorFilter or setAlpha.
*/
public TextDrawable(TextView tv, boolean bindToViewsText, boolean bindToViewsPaint) {
this(tv, tv.getText().toString(), false, false);
}
/**
* Use the provided TextView/EditText to initialize the drawable.
* The Drawable will copy the Text and the Paint properties, however it will from that
* point on be independant of the TextView.
*
* @param tv a TextView or EditText or any of their children.
*/
public TextDrawable(TextView tv) {
this(tv, false, false);
}
/**
* Use the provided TextView/EditText to initialize the drawable.
* The Drawable will copy the Paint properties, and use the provided text to initialise itself.
*
* @param tv a TextView or EditText or any of their children.
* @param s The String to draw
*/
public TextDrawable(TextView tv, String s) {
this(tv, s, false, false);
}
@Override
public void draw(Canvas canvas) {
if (mBindToViewPaint && ref.get() != null) {
Paint p = ref.get().getPaint();
canvas.drawText(mText, 0, getBounds().height(), p);
} else {
if (mInitFitText) {
fitTextAndInit();
}
canvas.drawText(mText, 0, getBounds().height(), mPaint);
}
}
@Override
public void setAlpha(int alpha) {
mPaint.setAlpha(alpha);
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
mPaint.setColorFilter(colorFilter);
}
@Override
public int getOpacity() {
int alpha = mPaint.getAlpha();
if (alpha == 0) {
return PixelFormat.TRANSPARENT;
} else if (alpha == 255) {
return PixelFormat.OPAQUE;
} else {
return PixelFormat.TRANSLUCENT;
}
}
private void init() {
Rect bounds = getBounds();
//We want to use some character to determine the max height of the text.
//Otherwise if we draw something like "..." they will appear centered
//Here I'm just going to use the entire alphabet to determine max height.
mPaint.getTextBounds("1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 0, 1, mHeightBounds);
//This doesn't account for leading or training white spaces.
//mPaint.getTextBounds(mText, 0, mText.length(), bounds);
float width = mPaint.measureText(mText);
bounds.top = mHeightBounds.top;
bounds.bottom = mHeightBounds.bottom;
bounds.right = (int) width;
bounds.left = 0;
setBounds(bounds);
}
public void setPaint(Paint paint) {
mPaint = new Paint(paint);
//Since this can change the font used, we need to recalculate bounds.
if (mFitTextEnabled && !mInitFitText) {
fitTextAndInit();
} else {
init();
}
invalidateSelf();
}
public Paint getPaint() {
return mPaint;
}
public void setText(String text) {
mText = text;
//Since this can change the bounds of the text, we need to recalculate.
if (mFitTextEnabled && !mInitFitText) {
fitTextAndInit();
} else {
init();
}
invalidateSelf();
}
public String getText() {
return mText;
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
setText(s.toString());
}
/**
* Make the TextDrawable match the width of the View it's associated with.
* <p/>
* Note: While this option will not work if bindToViewPaint is true.
*
* @param fitText
*/
public void setFillText(boolean fitText) {
mFitTextEnabled = fitText;
if (fitText) {
mPrevTextSize = mPaint.getTextSize();
if (ref.get() != null) {
if (ref.get().getWidth() > 0) {
fitTextAndInit();
} else {
mInitFitText = true;
}
}
} else {
if (mPrevTextSize > 0) {
mPaint.setTextSize(mPrevTextSize);
}
init();
}
}
private void fitTextAndInit() {
float fitWidth = ref.get().getWidth();
float textWidth = mPaint.measureText(mText);
float multi = fitWidth / textWidth;
mPaint.setTextSize(mPaint.getTextSize() * multi);
mInitFitText = false;
init();
}
}
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false"
android:orientation="vertical"
android:padding="@dimen/activity_horizontal_margin">
<EditText
android:id="@+id/txt_regular"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:hint="Regular EditText" />
<TextView
android:id="@+id/drawable_test"
android:layout_width="200dp"
android:layout_height="200dp"/>
</LinearLayout>
public class UsageExampleActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.usage_layout);
EditText regular = (EditText) findViewById(R.id.txt_regular);
regular.setCompoundDrawables(new TextDrawable(regular, "+61 "), null, new TextDrawable(regular, "\u2605"), null);
TextView view = (TextView) findViewById(R.id.drawable_test);
final TextDrawable textDrawable = new TextDrawable(view, "\u263A");
textDrawable.setFillText(true);
textDrawable.getPaint().setColor(Color.RED);
view.setBackgroundDrawable(textDrawable);
CountDownTimer c = new CountDownTimer(10000, 1000) {
@Override
public void onTick(long millisUntilFinished) {
textDrawable.setText(textDrawable.getText()+"\u263A");
}
@Override
public void onFinish() {
}
};
c.start();
}
}
@kibotu
Copy link
Author

kibotu commented Oct 8, 2018

xamarin version

using System;
using Android.Graphics;
using Android.Graphics.Drawables;
using Android.Text;
using Android.Widget;
using Java.Lang;

namespace @base.text
{
    public class TextDrawable : Drawable, ITextWatcher
    {
        private readonly WeakReference<TextView> _reference;

        private TextView TextView
        {
            get
            {
                _reference.TryGetTarget(out var textView);
                return textView;
            }
        }

        #region text

        private string _text;

        private string Text
        {
            get => _text;
            set
            {
                _text = value;
                //Since this can change the bounds of the text, we need to recalculate.
                if (_fitTextEnabled && !_initFitText)
                {
                    FitTextAndInit();
                }
                else
                {
                    Init();
                }

                InvalidateSelf();
            }
        }

        #endregion

        #region paint

        private Paint _paint;

        private Paint Paint
        {
            get => _paint;

            set
            {
                _paint = new Paint(value);
                //Since this can change the font used, we need to recalculate bounds.
                if (_fitTextEnabled && !_initFitText)
                {
                    FitTextAndInit();
                }
                else
                {
                    Init();
                }

                InvalidateSelf();
            }
        }

        #endregion

        private readonly Rect _heightBounds;

        private readonly bool _bindToViewPaint;

        private float _previousTextSize;

        private bool _initFitText;

        private bool _fitTextEnabled;

        /// <summary>
        /// Create a TextDrawable using the given paint object and string
        /// </summary>
        /// <param name="paint"></param>
        /// <param name="s"></param>
        public TextDrawable(Paint paint, string s)
        {
            Text = s;
            Paint = paint;
            _heightBounds = new Rect();
            Init();
        }

        /// <summary>
        /// Create a TextDrawable. This uses the given TextView to initialize paint and has initial text
        /// that will be drawn. Initial text can also be useful for reserving space that may otherwise
        /// not be available when setting compound drawables.
        /// </summary>
        /// <param name="tv">The TextView / EditText using to initialize this drawable</param>
        /// <param name="initialText">Optional initial text to display</param>
        /// <param name="bindToViewsText">Should this drawable mirror the text in the TextView</param>
        /// <param name="bindToViewsPaint">Should this drawable mirror changes to Paint in the TextView, like textColor, typeface, alpha etc.
        /// Note, this will override any changes made using setColorFilter or setAlpha.
        /// </param>
        public TextDrawable(TextView tv, string initialText, bool bindToViewsText = false, bool bindToViewsPaint = false) : this(tv.Paint, initialText)
        {
            _reference = new WeakReference<TextView>(tv);
            if (!bindToViewsText && !bindToViewsPaint) return;
            if (bindToViewsText)
            {
                tv.AddTextChangedListener(this);
            }

            _bindToViewPaint = bindToViewsPaint;
        }

        public override void Draw(Canvas canvas)
        {
            if (_bindToViewPaint && TextView != null)
            {
                Paint p = TextView?.Paint;
                canvas.DrawText(Text, 0, Bounds.Height(), p);
            }
            else
            {
                if (_initFitText)
                {
                    FitTextAndInit();
                }

                canvas.DrawText(Text, 0, Bounds.Height(), Paint);
            }
        }

        public override void SetAlpha(int alpha)
        {
            Paint.Alpha = alpha;
        }

        public override void SetColorFilter(ColorFilter colorFilter) => Paint.SetColorFilter(colorFilter);

        public override int Opacity
        {
            get
            {
                switch (Paint.Alpha)
                {
                    case 0:
                        return (int) Format.Transparent;
                    case 255:
                        return (int) Format.Opaque;
                    default:
                        return (int) Format.Translucent;
                }
            }
        }

        private void Init()
        {
            var bounds = Bounds;
            // We want to use some character to determine the max height of the text.
            // Otherwise if we draw something like "..." they will appear centered
            // Here I'm just going to use the entire alphabet to determine max height.
            Paint.GetTextBounds("1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 0, 1, _heightBounds);
            // This doesn't account for leading or training white spaces.
            // Paint.getTextBounds(mText, 0, mText.length(), bounds);
            var width = Paint.MeasureText(Text);
            SetBounds(0, _heightBounds.Top, (int) width, _heightBounds.Bottom);
        }

        /**
         * Make the TextDrawable match the width of the View it's associated with.
         * <p/>
         * Note: While this option will not work if bindToViewPaint is true.
         *
         * @param fitText
         */
        public void SetFillText(bool fitText)
        {
            _fitTextEnabled = fitText;
            if (fitText)
            {
                _previousTextSize = Paint.TextSize;
                if (TextView == null) return;
                if (TextView?.Width > 0)
                {
                    FitTextAndInit();
                }
                else
                {
                    _initFitText = true;
                }
            }
            else
            {
                if (_previousTextSize > 0)
                {
                    Paint.TextSize = _previousTextSize;
                }

                Init();
            }
        }

        private void FitTextAndInit()
        {
            var fitWidth = TextView?.Width ?? 0;
            var textWidth = Paint.MeasureText(Text);
            var multi = fitWidth / textWidth;
            Paint.TextSize = Paint.TextSize * multi;
            _initFitText = false;
            Init();
        }

        #region ITextWatcher

        public void AfterTextChanged(IEditable s)
        {
            Text = s.ToString();
        }

        public void BeforeTextChanged(ICharSequence s, int start, int count, int after)
        {
        }

        public void OnTextChanged(ICharSequence s, int start, int before, int count)
        {
        }

        #endregion
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment