Подтвердить что ты не робот

Как иметь изображение с динамическим текстом в нем, все в растягиваемом, например, "текущем" элементе действия в приложении Календаря Google?

Фон

В приложении Google Calendar есть элемент действия, который динамически изменяется в соответствии с текущим днем ​​( "сегодня" ):

введите описание изображения здесь

Мне нужно делать очень похожие вещи, но с немного другим изображением, которое окружает текст.

Проблема

Мне удалось сделать это, создав Drawable, в котором есть текст и изображение (на основе здесь).

Однако, я не думаю, что я сделал это достаточно хорошо:

  • Текстовый шрифт может быть разным на разных устройствах, поэтому может не соответствовать тому, что я написал.
  • Не уверен, что это из-за VectorDrawable или текста, но я думаю, что текст не кажется таким центрированным. Кажется немного слева. Это особенно верно, если я использую 2 цифры:

введите описание изображения здесь

  1. Для центрирования по вертикали, я не думаю, что сделал правильный расчет. Я попробовал гораздо более логичные вещи, но они не были сосредоточены.

Что я пробовал

Здесь полный код (также доступен здесь в проекте):

TextDrawable.java

public class TextDrawable extends Drawable {
    private static final int DEFAULT_COLOR = Color.WHITE;
    private static final int DRAWABLE_SIZE = 24;
    private static final int DEFAULT_TEXT_SIZE = 8;
    private Paint mPaint;
    private CharSequence mText;
    private final int mIntrinstSize;
    private final Drawable mDrawable;

    public TextDrawable(Context context, CharSequence text) {
        mText = text;
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(DEFAULT_COLOR);
        mPaint.setTextAlign(Align.CENTER);
        float textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULT_TEXT_SIZE, context.getResources().getDisplayMetrics());
        mPaint.setTextSize(textSize);
        mIntrinstSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DRAWABLE_SIZE, context.getResources().getDisplayMetrics());
        mDrawable = AppCompatResources.getDrawable(context, R.drawable.ic_backtodate);
        mDrawable.setBounds(0, 0, mIntrinstSize, mIntrinstSize);
    }

    @Override
    public void draw(Canvas canvas) {
        Rect bounds = getBounds();
        mDrawable.draw(canvas);
        canvas.drawText(mText, 0, mText.length(),
                bounds.centerX(), bounds.centerY() + mPaint.getFontMetricsInt(null) / 3, mPaint); // this seems very wrong
    }

    @Override
    public int getOpacity() {
        return mPaint.getAlpha();
    }

    @Override
    public int getIntrinsicWidth() {
        return mIntrinstSize;
    }

    @Override
    public int getIntrinsicHeight() {
        return mIntrinstSize;
    }

    @Override
    public void setAlpha(int alpha) {
        mPaint.setAlpha(alpha);
    }

    @Override
    public void setColorFilter(ColorFilter filter) {
        mPaint.setColorFilter(filter);
    }
}

MainActivity.kt

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val drawable = TextDrawable(this, "1")
        imageView.setImageDrawable(drawable)
    }
}

ic_backtodate.xml

<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="25dp" android:height="25dp"
        android:viewportHeight="76.0" android:viewportWidth="76.0">
    <path
        android:fillColor="#ffffff" android:fillType="evenOdd"
        android:pathData="M47.294,60.997H28.704C21.148,60.997 15,54.755 15,47.083V28.905c0,-7.672 6.148,-13.913 13.704,-13.913h18.59C54.852,14.992 61,21.233 61,28.905v18.178c0,7.672 -6.148,13.914 -13.706,13.914zM57.592,28.905c0,-5.763 -4.62,-10.453 -10.298,-10.453h-18.59c-5.676,0 -10.296,4.69 -10.296,10.453v18.178c0,5.765 4.62,10.454 10.296,10.454h18.59c5.678,0 10.298,-4.689 10.298,-10.454z"/>
</vector>

Вопросы

  • Как я могу преодолеть различные проблемы с шрифтами? Я уже использую шрифт "Lato" глобально (не в примере приложения, а в реальном приложении, используя "загруженные шрифты" API библиотеки поддержки, но вместо этого они встроены в приложение), но я не думаю, что Paint Object может использовать его, правильно?

  • Как я могу централизовать текст?

  • Я посмотрел, используя инструмент View-hierarchy, как работает Календарь Google для этой части. Мне кажется, они просто использовали TextView. Как они это делают? Возможно, используя 9-патч? Но хорошо ли это работает для элементов панели инструментов?


EDIT:

В настоящее время, поскольку я напряжен по графику, я не могу использовать выталкиваемое решение. Было бы неплохо знать, как это сделать хорошо.

Мое текущее решение не связано с этим. Я просто использую специальное представление, которое имитирует обычный элемент действия. Это не идеально (не полностью имитирует реальный элемент действия), но этого пока достаточно. Поскольку он не идеален, я написал об этом в новом потоке, здесь.


EDIT: так как это действительно может работать хорошо и по-прежнему оставаться как обычный элемент действия, я решил попробовать еще раз.

Мне удалось красиво центрировать текст, но теперь этот шрифт является проблемой. Похоже, что если ОС использует собственный шрифт, даже если я установил "Lato" в качестве одного из приложений, он не будет использоваться в drawable, который я сделал:

введите описание изображения здесь

Я думаю, что это последняя проблема, которую мне нужно исправить здесь.

Здесь код:

styles.xml

    <item name="android:fontFamily" tools:targetApi="jelly_bean">@font/lato</item>
    <item name="fontFamily">@font/lato</item>

MainActivity.kt

class MainActivity : AppCompatActivity() {
    lateinit var textDrawable: TextDrawable

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        textDrawable = TextDrawable(this, "1")
        setSupportActionBar(toolbar)
        val handler = Handler()
        val runnable = object : Runnable {
            var i = 1
            override fun run() {
                if (isFinishing||isDestroyed)
                    return
                textDrawable.text = (i + 1).toString()
                i = (i + 1) % 31
                handler.postDelayed(this, 1000)
            }
        }
        runnable.run()
    }

    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        menu.add("goToToday").setIcon(textDrawable).setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
        menu.add("asd").setIcon(R.drawable.abc_ic_menu_copy_mtrl_am_alpha).setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
        return super.onCreateOptionsMenu(menu)
    }
}

TextDrawable.kt

class TextDrawable(context: Context, text: CharSequence) : Drawable() {
    companion object {
        private val DEFAULT_COLOR = Color.WHITE
        private val DEFAULT_TEXT_SIZE = 12
    }

    var text: CharSequence = text
        set (value) {
            field = value
            invalidateSelf()
        }

    private val mPaint: TextPaint = TextPaint(Paint.ANTI_ALIAS_FLAG)
    private val mDrawable: Drawable?

    init {
        mPaint.color = DEFAULT_COLOR
        mPaint.textAlign = Align.CENTER
        val textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULT_TEXT_SIZE.toFloat(), context.resources.displayMetrics)
        mPaint.textSize = textSize
        mDrawable = AppCompatResources.getDrawable(context, R.drawable.ic_backtodate)
        mDrawable!!.setBounds(0, 0, mDrawable.intrinsicWidth, mDrawable.intrinsicHeight)
    }

    override fun draw(canvas: Canvas) {
        val bounds = bounds
        mDrawable!!.draw(canvas)
        canvas.drawText(text, 0, text.length,
                bounds.centerX().toFloat(), (bounds.centerY() + mPaint.getFontMetricsInt(null) / 3).toFloat(), mPaint) // this seems very wrong, but seems to work fine
    }

    override fun getOpacity(): Int = mPaint.alpha

    override fun getIntrinsicWidth(): Int = mDrawable!!.intrinsicWidth

    override fun getIntrinsicHeight(): Int = mDrawable!!.intrinsicHeight

    override fun setAlpha(alpha: Int) {
        mPaint.alpha = alpha
        invalidateSelf()
    }

    override fun setColorFilter(filter: ColorFilter?) {
        mPaint.colorFilter = filter
        invalidateSelf()
    }

}

EDIT:

Я думаю, что нашел способ шрифта для текста, используя:

mPaint.typeface=TypefaceCompat.createFromResourcesFamilyXml(...)

Не уверен, хотя как заполнить параметры. Продолжая расследование...

4b9b3361

Ответ 1

ОК, нашел ответ о том, как иметь тот же шрифт для TextPaint класса Drawable, который я сделал:

mPaint.typeface = ResourcesCompat.getFont(context, R.font.lato)

Результат:

введите описание изображения здесь

Здесь полная реализация этого класса:

class TextDrawable(context: Context, text: CharSequence) : Drawable() {
    companion object {
        private val DEFAULT_COLOR = Color.WHITE
        private val DEFAULT_TEXT_SIZE_IN_DP = 12
    }

    private val mTextBounds = Rect()
    private val mPaint: TextPaint = TextPaint(Paint.ANTI_ALIAS_FLAG)
    private val mDrawable: Drawable?

    var text: CharSequence = text
        set (value) {
            field = value
            invalidateSelf()
        }

    init {
        mPaint.typeface = ResourcesCompat.getFont(context, R.font.lato)
        mPaint.color = DEFAULT_COLOR
        mPaint.textAlign = Align.CENTER
        val textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULT_TEXT_SIZE_IN_DP.toFloat(), context.resources.displayMetrics)
        mPaint.textSize = textSize
        mDrawable = AppCompatResources.getDrawable(context, R.drawable.ic_backtodate)
        mDrawable!!.setBounds(0, 0, mDrawable.intrinsicWidth, mDrawable.intrinsicHeight)
    }

    override fun draw(canvas: Canvas) {
        val bounds = bounds
        mDrawable!!.draw(canvas)
        mPaint.getTextBounds(text.toString(), 0, text.length, mTextBounds);
        val textHeight = mTextBounds.bottom - mTextBounds.top
        canvas.drawText(text as String?, (bounds.right / 2).toFloat(), (bounds.bottom.toFloat() + textHeight + 1) / 2, mPaint)
    }

    override fun getOpacity(): Int = mPaint.alpha
    override fun getIntrinsicWidth(): Int = mDrawable!!.intrinsicWidth
    override fun getIntrinsicHeight(): Int = mDrawable!!.intrinsicHeight

    override fun setAlpha(alpha: Int) {
        mPaint.alpha = alpha
        invalidateSelf()
    }

    override fun setColorFilter(filter: ColorFilter?) {
        mPaint.colorFilter = filter
        invalidateSelf()
    }

}

EDIT: этот код завершен и работает хорошо. Он должен работать нормально и частично основан на приложении Calendar, так как мне было рекомендовано посмотреть ( здесь и здесь).

Ответ 2

Ссылка делается на ваш другой вопрос Как полностью имитировать вид элемента действия на панели инструментов для персонализированного?

Я включил этот подход в свой ответ на вышеупомянутый вопрос в вашу реализацию пользовательского чертежа, который можно получить в ответ на этот вопрос. Ниже приведена новая версия TextDrawable.java, которая динамически создает коробку TextView для отображения в качестве нужного значка для элемента меню. Он избегает рисования кэшей и просто управляет TextView внутренне для отображения.

TextDrawable.java

public class TextDrawable extends Drawable {
    private final int mIntrinsicSize;
    private final TextView mTextView;

    public TextDrawable(Context context, CharSequence text) {
        mIntrinsicSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DRAWABLE_SIZE,
                                                         context.getResources().getDisplayMetrics());
        mTextView = createTextView(context, text);
        mTextView.setWidth(mIntrinsicSize);
        mTextView.setHeight(mIntrinsicSize);
        mTextView.measure(mIntrinsicSize, mIntrinsicSize);
        mTextView.layout(0, 0, mIntrinsicSize, mIntrinsicSize);
    }

    private TextView createTextView(Context context, CharSequence text) {
        TextView textView = new TextView(context);
//        textView.setId(View.generateViewId()); // API 17+
        LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
            LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
        lp.gravity = Gravity.CENTER;
        textView.setLayoutParams(lp);
        textView.setGravity(Gravity.CENTER);
        textView.setBackgroundResource(R.drawable.ic_backtodate);
        textView.setTextColor(Color.WHITE);
        textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, DEFAULT_TEXT_SIZE);
        textView.setText(text);
        return textView;
    }

    public void setText(CharSequence text) {
        mTextView.setText(text);
        invalidateSelf();
    }

    @Override
    public void draw(@NonNull Canvas canvas) {
        mTextView.draw(canvas);
    }

    @Override
    public int getOpacity() {
        return PixelFormat.OPAQUE;
    }

    @Override
    public int getIntrinsicWidth() {
        return mIntrinsicSize;
    }

    @Override
    public int getIntrinsicHeight() {
        return mIntrinsicSize;
    }

    @Override
    public void setAlpha(int alpha) {
    }

    @Override
    public void setColorFilter(ColorFilter filter) {
    }

    private static final int DRAWABLE_SIZE = 32; // device-independent pixels (DP)
    private static final int DEFAULT_TEXT_SIZE = 12; // device-independent pixels (DP)
}

Вызовите этот пользовательский Drawable следующим образом (Kotlin):

mTextDrawable = TextDrawable(this, "1")
menu.add("goToToday").setIcon(mTextDrawable).setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)

Чтобы изменить отображаемую дату (Kotlin):

mTextDrawable?.setText(i.toString())