Google Calendar app has an action item that dynamically change according to the current day ("today"):
I'm required to do a very similar thing, but with a slightly different image that surrounds the text.
I did succeed to make it work, by creating a Drawable that has text and image in it (based on here).
However, I'm don't think I did it well enough :
Here's the full code (also available here in a project) :
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>
How can I overcome the different fonts issues? I already use "Lato" font globally (not in the sample app, but in the real app, using "downloaded fonts" API of the support library, but having them built into the app instead), but I don't think Paint
Object can use it, right?
How can I center the text well?
I've looked, via View-hierarchy tool, at how Google Calendar works for this part. To me it seems they just used TextView. How did they do it? Maybe using a 9-patch? But does it work well for Toolbar items?
EDIT:
For now, because I'm tight on schedule, I can't use the drawable solution. Would still be nice to know how to do it well.
My current solution doesn't involve it. I just use a special view that mimics a normal action item. It's not perfect (doesn't fully mimics a real action item), but it will be enough for now. Because it's not perfect, I wrote about it on a new thread, here.
EDIT: since this actually can work well, and still stay as a normal action item, I've decided to give it another try.
I've managed to center the text nicely, but the font is the issue now. It seems that if the OS uses a font of its own, even if I've set "Lato" to be the one of the app, it's not used in the drawable I've made:
I think it's the last issue I need to fix here.
Here's the code:
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:
I think I've found how to have a font for the text, by using :
mPaint.typeface=TypefaceCompat.createFromResourcesFamilyXml(...)
Not sure though how to fill the parameters. Still investigating...
OK, found the answer about how to have the same font for the TextPaint of the Drawable class I've made:
mPaint.typeface = ResourcesCompat.getFont(context, R.font.lato)
The result:
Here's the full implementation of this class:
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: this code is now complete and works well. It should work fine, and is partially based on Calendar app itself, as was recommended to me to look at (here and here) .
Reference is made to your other question "How to fully mimic Action item view in the toolbar, for a customized one?"
I have incorporated the approach in my answer to the above-referenced question into your implementation of a custom drawable in your answer to this question. Below is a new version of TextDrawable.java
that dynamically builds a boxed TextView
for display as the desired icon for a menu item. It avoids drawing caches and simply manages a TextView
internally for display.
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)
}
Invoke this custom Drawable
as follows (Kotlin):
mTextDrawable = TextDrawable(this, "1")
menu.add("goToToday").setIcon(mTextDrawable).setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
To change the displayed date (Kotlin):
mTextDrawable?.setText(i.toString())
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With