I am trying to add a circular notification badge in navigation drawer here is my custom textview.
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
style="#style/Widget.Design.FloatingActionButton"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"
android:padding="2dp"
android:textAppearance="#style/Base.TextAppearance.AppCompat.Body2"
android:textColor="#color/colorWhite"
android:textStyle="bold" />
Result:
Required:
Note:
If I set the textview height to wrap_content then textview will be circular but it will stick to the top.
I just created a new View class to achieve this:
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.support.design.widget.NavigationView;
import android.support.v4.view.MenuItemCompat;
import android.support.v7.widget.AppCompatTextView;
import android.util.AttributeSet;
import android.view.Gravity;
import android.widget.FrameLayout;
public class DrawerBadge extends AppCompatTextView {
private float strokeWidth;
int strokeColor = Color.parseColor("#000000"); // black
int solidColor = Color.parseColor("#FF0000"); // red
// **** THIS IS THE FULL CONSTRUCTOR YOU HAVE TO CALL ****
public DrawerBadge(Context context, NavigationView navigationView, int idItem, String value, String letterColor, String strokeColor, String solidColor) {
super(context);
MenuItemCompat.setActionView(navigationView
.getMenu().findItem(idItem), this);
DrawerBadge badge = (DrawerBadge) MenuItemCompat
.getActionView(navigationView
.getMenu().findItem(idItem));
badge.setGravity(Gravity.CENTER);
badge.setTypeface(null, Typeface.BOLD);
badge.setTextColor(Color.parseColor(letterColor));
badge.setText(value);
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT,FrameLayout.LayoutParams.WRAP_CONTENT);
params.gravity = Gravity.CENTER;
badge.setLayoutParams(params);
badge.setPadding(3,3,3,3);
badge.setStrokeWidth(1);
badge.setStrokeColor(strokeColor);
badge.setSolidColor(solidColor);
badge.requestLayout();
}
public DrawerBadge(Context context) {
super(context);
}
public DrawerBadge(Context context, AttributeSet attrs) {
super(context, attrs);
}
public DrawerBadge(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
#Override
public void draw(Canvas canvas) {
Paint circlePaint = new Paint();
circlePaint.setColor(solidColor);
circlePaint.setFlags(Paint.ANTI_ALIAS_FLAG);
Paint strokePaint = new Paint();
strokePaint.setColor(strokeColor);
strokePaint.setFlags(Paint.ANTI_ALIAS_FLAG);
int h = this.getHeight();
int w = this.getWidth();
int diameter = ((h > w) ? h : w);
int radius = diameter/2;
this.setHeight(diameter);
this.setWidth(diameter);
canvas.drawCircle(diameter / 2 , diameter / 2, radius, strokePaint);
canvas.drawCircle(diameter / 2, diameter / 2, radius-strokeWidth, circlePaint);
super.draw(canvas);
}
public void setStrokeWidth(int dp)
{
float scale = getContext().getResources().getDisplayMetrics().density;
strokeWidth = dp*scale;
}
public void setStrokeColor(String color)
{
strokeColor = Color.parseColor(color);
}
public void setSolidColor(String color)
{
solidColor = Color.parseColor(color);
}
}
How to use? Just call the DrawerBadge FULL CONSTRUCTOR like this:
DrawerBadge badge = new DrawerBadge(this, findViewById(R.id.navigation_view),
R.id.menu_item_id,
String.valueOf(count > 99 ? "+" + 99 : count),
"#FFFFFF","#FF0000","#FF0000");
Enjoy!
Using Relative layout for every row then use property in XML of textView center_vertical="true" and alignParentRight="true
Its a bug in Android. You can only achieve this by Trial and error.
You have to fix text view size height & width in dp
This can be achieved in 3 steps using a custom drawable resource file.
First, create a drawable resource file with custom shape, circle.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<solid android:color="#37A667" />
<size android:width="12dp" android:height="12dp" />
</shape>
Second, create your Custom notification drawer layout file, we will inflate this in our drawer activity, notification_badge.xml.
These fields are important:
Surround TextView with RelativeLayout: RelativeLayout allows us to center TextView vertically
Specify TextView layout_width
Specify TextView layout_height
Specify background="#drawable/circle"
Set gravity="true": Centers the TextView nicely within the background #drawable/circle
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:id="#+id/counter"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_centerVertical="true"
android:background="#drawable/circle"
android:gravity="center"
android:textColor="#color/white"
android:textSize="12sp"
android:textStyle="bold" />
</RelativeLayout>
Finally, inflate the notification_badge view and setText to the TextView:
In Kotlin:
val customLayout = LayoutInflater.from(this#AdminMainActivity).inflate(R.layout.notification_badge, null) as RelativeLayout
(customLayout.findViewById<TextView>(R.id.counter)).text = "2"
drawerNavView.menu.findItem(R.id.nav_orders).actionView = customLayout
In Java:
RelativeLayout customLayout = (RelativeLayout) LayoutInflater.from(this).inflate(R.layout.notification_badge, null);
TextView badge = (customLayout.findViewById(R.id.counter));
badge.setText("2");
navigationView.getMenu().findItem(R.id.nav_orders).setActionView(customLayout);
Output:
Related
I am currently working on rewriting one of my iOS apps to release on Android. I'm making good progress but I am looking for some guidance regarding the best way to approach the list rows.
These are the cells/rows I am trying to recreate:
As you can see, I have a white background view with padding around each edge and rounded corners, so I guess for this I would need to embed everything within a view of some sort? The other part I am unsure about is how to create the coloured circle on the right. Would this be another view with rounded corners and a coloured background? I haven't managed to figure out how to get this to the right of the two textViews that I currently have. So if anyone could give a code example then that would be great.
This is my current XML:
<ImageView
android:layout_width="78dp"
android:layout_height="78dp"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:scaleType="centerCrop"
android:adjustViewBounds="false"
android:id="#+id/attractionImageView"
android:contentDescription="Attraction Image"
android:background="#color/colorPrimary" />
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginLeft="5dp"
android:layout_marginTop="5dp"
android:background="#drawable/fast_pass"
android:id="#+id/fastPassImageView" />
<LinearLayout android:orientation="vertical" android:layout_height="match_parent" android:layout_width="fill_parent" android:layout_toEndOf="#+id/attractionImageView" android:layout_toRightOf="#+id/attractionImageView">
<TextView
android:layout_width="match_parent"
android:layout_height="49dp"
android:id="#+id/attractionNameTextView"
android:text="Attraction Name"
android:layout_marginLeft="5dp"/>
<LinearLayout android:orientation="horizontal" android:layout_height="match_parent" android:layout_width="fill_parent" android:layout_toEndOf="#+id/attractionImageView" android:layout_toRightOf="#+id/attractionImageView">
<ImageView
android:layout_width="13dp"
android:layout_height="13dp"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:id="#+id/updatedImageView"
android:background="#drawable/updated"
android:layout_gravity="center_vertical"/>
<TextView
android:layout_width="match_parent"
android:layout_height="29dp"
android:id="#+id/updatedTextView"
android:text="Updated"
android:gravity="center_vertical"/>
</LinearLayout>
</LinearLayout>
This seems to work well for everything I've got so far, but I'm not sure where to go from here. This is how it looks:
Any suggestions?
For the colored circle- I'd suggest just a text view set to the proper size with a circle drawable set as the background of the view. Should get what you want.
For the background- I'd just stick the entire row in either a linear or relative layout, then set a RoundedBitmapDrawable as the background of the layout. That will give you the rounded background effect. If necessary add some margin to the top and bottom of each view to increase the gap between items.
I suppose that you're using custom views for your listview's items, if so, I would set the background of the custom view to something like this:
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
<solid android:color="#color/white" />
<stroke android:width="4dp" android:color="#color/beige" /> //If you want a stroke
<corners
android:topLeftRadius="20dp"
android:topRightRadius="20dp"
android:bottomLeftRadius="20dp"
android:bottomRightRadius="20dp" />
<padding
android:left="10dp"
android:top="10dp"
android:right="10dp"
android:bottom="10dp" />
As an alternate solution, I just got something working with CardViews and using Picasso with a CircleTransformation for the circle.
In this simple example, I used a RecyclerView of CardViews.
First, here is the CircleTransformation class that is used by Picasso:
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import com.squareup.picasso.Transformation;
public class CircleTransform implements Transformation {
#Override
public Bitmap transform(Bitmap source) {
int size = Math.min(source.getWidth(), source.getHeight());
int x = (source.getWidth() - size) / 2;
int y = (source.getHeight() - size) / 2;
Bitmap squaredBitmap = Bitmap.createBitmap(source, x, y, size, size);
if (squaredBitmap != source) {
source.recycle();
}
Bitmap bitmap = Bitmap.createBitmap(size, size, source.getConfig());
Canvas canvas = new Canvas(bitmap);
BitmapShader shader = new BitmapShader(squaredBitmap, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | Paint.FILTER_BITMAP_FLAG);
paint.setShader(shader);
float r = size/2f;
canvas.drawCircle(r, r, r-1, paint);
Paint paintBorder = new Paint();
paintBorder.setStyle(Style.STROKE);
paintBorder.setColor(Color.argb(84,0,0,0));
paintBorder.setAntiAlias(true);
paintBorder.setStrokeWidth(1);
canvas.drawCircle(r, r, r-1, paintBorder);
squaredBitmap.recycle();
return bitmap;
}
#Override
public String key() {
return "circle";
}
}
Here is the Fragment:
public class BlankFragment extends Fragment {
public BlankFragment() {
// Required empty public constructor
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View rootView = inflater.inflate(R.layout.fragment_blank, container, false);
RecyclerView rv = (RecyclerView) rootView.findViewById(R.id.rv_recycler_view);
rv.setHasFixedSize(true);
MyAdapter adapter = new MyAdapter(new String[]{"testone", "testtwo", "testthree", "testfour"}, getActivity());
rv.setAdapter(adapter);
LinearLayoutManager llm = new LinearLayoutManager(getActivity());
rv.setLayoutManager(llm);
return rootView;
}
}
fragment_blank.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="#+id/rv_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" >
</android.support.v7.widget.RecyclerView>
</RelativeLayout>
The adapter, which includes the defaultCircleWithText() method for drawing the circle and text. Here I'm just using one color for the circles, but you can extend this to set the correct circle color for each row:
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.RectF;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.support.v7.widget.CardView;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {
private String[] mDataset;
Context mContext;
// Provide a reference to the views for each data item
// Complex data items may need more than one view per item, and
// you provide access to all the views for a data item in a view holder
public static class MyViewHolder extends RecyclerView.ViewHolder {
public CardView mCardView;
public TextView mTextView;
public ImageView mImageView;
public MyViewHolder(View v) {
super(v);
mCardView = (CardView) v.findViewById(R.id.card_view);
mTextView = (TextView) v.findViewById(R.id.tv_text);
mImageView = (ImageView) v.findViewById(R.id.iv_image);
}
}
// Provide a suitable constructor (depends on the kind of dataset)
public MyAdapter(String[] myDataset, Context context) {
mDataset = myDataset;
mContext = context;
}
// Create new views (invoked by the layout manager)
#Override
public MyAdapter.MyViewHolder onCreateViewHolder(ViewGroup parent,
int viewType) {
// create a new view
View v = LayoutInflater.from(parent.getContext())
.inflate(R.layout.card_item, parent, false);
// set the view's size, margins, paddings and layout parameters
MyViewHolder vh = new MyViewHolder(v);
return vh;
}
#Override
public void onBindViewHolder(MyViewHolder holder, int position) {
holder.mTextView.setText(mDataset[position]);
Drawable drawable = new BitmapDrawable(mContext.getResources(), defaultCircleWithText("test"));
Picasso.with(mContext).load((String)null).fit().transform(new CircleTransform()).placeholder(drawable).into(holder.mImageView);
}
#Override
public int getItemCount() {
return mDataset.length;
}
public final static int BIG_IMAGE = 138;
public static Bitmap defaultCircleWithText(String text) {
int size = BIG_IMAGE;
Bitmap image = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(image);
int alpha = 165;
//hard-coded circle color:
int color = Color.argb(alpha,253,70,45);
Paint p_circle = new Paint();
p_circle.setAntiAlias(true);
p_circle.setColor(color);
c.drawCircle(size/2f, size/2f, size/2f-1, p_circle);
Paint p_text = new Paint();
p_text.setAntiAlias(true);
p_text.setColor(Color.WHITE);
p_text.setTextSize(58);
RectF bounds = new RectF(0, 0, c.getWidth(), c.getHeight());
// measure text width
bounds.right = p_text.measureText(text, 0, text.length());
// measure text height
bounds.bottom = p_text.descent() - p_text.ascent();
bounds.left += (c.getWidth() - bounds.right) / 2.0f;
bounds.top += (c.getHeight() - bounds.bottom) / 2.0f;
c.drawText(text, bounds.left, bounds.top - p_text.ascent(), p_text);
return image;
}
}
card_item.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="78dp" >
<android.support.v7.widget.CardView
android:id="#+id/card_view"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
card_view:cardCornerRadius="4dp"
android:layout_width="match_parent"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_marginTop="10dp"
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TextView
android:id="#+id/tv_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="test" >
</TextView>
<ImageView
android:id="#+id/iv_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginRight="10dp"/>
</RelativeLayout>
</android.support.v7.widget.CardView>
</RelativeLayout>
Result:
Here is my layout:
The issue I'm facing is with the drawable checkmark. How would I go about aligning it next to the text, both of them centered within the button? Here is the XML:
<RelativeLayout 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"
tools:context=".PostAssignmentActivity" >
<LinearLayout
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:orientation="horizontal" >
<Button
style="?android:attr/buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:drawableLeft="#drawable/ic_checkmark_holo_light"
android:text="Post" />
<Button
style="?android:attr/buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Cancel" />
</LinearLayout>
</RelativeLayout>
Applying android:gravity="center_vertical" pulls the text and drawable together, but then the text is no longer aligned in the center.
Solution 1
Set android:paddingLeft inside your first button. This will force the drawableLeft by paddingLeft amount to the right. This is the fast/hacky solution.
Solution 2
Instead of using a ButtonView, use a LinearLayout that contains both a textview and imageview. This is a better solution. It gives you more flexibility in the positioning of the checkmark.
Replace your ButtonView with the following code. You need the LinearLayout and TextView to use buttonBarButtonStyle so that the background colors are correct on selection and the text size is correct. You need to set android:background="#0000" for the children, so that only the LinearLayout handles the background coloring.
<LinearLayout
style="?android:attr/buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="horizontal" >
<ImageView
style="?android:attr/buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="false"
android:background="#0000"
android:src="#drawable/ic_checkmark_holo_light"/>
<TextView
style="?android:attr/buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="false"
android:background="#0000"
android:text="Done" />
</LinearLayout>
Here are some screenshots I took while trying this out.
None of these solutions worked correctly without presenting unacceptable trade-offs (create a layout with views in it? Not a good idea). So why not roll your own? This is what I got:
First create an attrs.xml with this:
<resources>
<declare-styleable name="IconButton">
<attr name="iconSrc" format="reference" />
<attr name="iconSize" format="dimension" />
<attr name="iconPadding" format="dimension" />
</declare-styleable>
</resources>
This allows to create an icon with specific size, padding from text, and image in our new view. The view code looks like this:
public class IconButton extends Button {
private Bitmap mIcon;
private Paint mPaint;
private Rect mSrcRect;
private int mIconPadding;
private int mIconSize;
public IconButton(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context, attrs);
}
public IconButton(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public IconButton(Context context) {
super(context);
}
#Override
protected void onDraw(Canvas canvas) {
int shift = (mIconSize + mIconPadding) / 2;
canvas.save();
canvas.translate(shift, 0);
super.onDraw(canvas);
if (mIcon != null) {
float textWidth = getPaint().measureText((String)getText());
int left = (int)((getWidth() / 2f) - (textWidth / 2f) - mIconSize - mIconPadding);
int top = getHeight()/2 - mIconSize/2;
Rect destRect = new Rect(left, top, left + mIconSize, top + mIconSize);
canvas.drawBitmap(mIcon, mSrcRect, destRect, mPaint);
}
canvas.restore();
}
private void init(Context context, AttributeSet attrs) {
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.IconButton);
for (int i = 0; i < array.getIndexCount(); ++i) {
int attr = array.getIndex(i);
switch (attr) {
case R.styleable.IconButton_iconSrc:
mIcon = drawableToBitmap(array.getDrawable(attr));
break;
case R.styleable.IconButton_iconPadding:
mIconPadding = array.getDimensionPixelSize(attr, 0);
break;
case R.styleable.IconButton_iconSize:
mIconSize = array.getDimensionPixelSize(attr, 0);
break;
default:
break;
}
}
array.recycle();
//If we didn't supply an icon in the XML
if(mIcon != null){
mPaint = new Paint();
mSrcRect = new Rect(0, 0, mIcon.getWidth(), mIcon.getHeight());
}
}
public static Bitmap drawableToBitmap (Drawable drawable) {
if (drawable instanceof BitmapDrawable) {
return ((BitmapDrawable)drawable).getBitmap();
}
Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
}
}
And then it can be used like this:
<com.example.grennis.myapplication.IconButton
android:layout_width="200dp"
android:layout_height="64dp"
android:text="Delete"
app:iconSrc="#android:drawable/ic_delete"
app:iconSize="32dp"
app:iconPadding="6dp" />
This works for me.
You can use
<com.google.android.material.button.MaterialButton/> .
https://material.io/develop/android/components/material-button/
It finally allows setting the icon gravity.
<com.google.android.material.button.MaterialButton
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:gravity="center"
android:text="Awesome button"
app:icon="#drawable/your_icon"
app:iconGravity="textStart" />
Here is a clean easy way, without doing anything fancy, to achieve the results of having a Button that is much wider than the content with Image and Text which are centered.
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:background="#drawable/button_background_selector">
<Button
android:layout_centerInParent="true"
android:gravity="center"
android:duplicateParentState="true"
android:layout_width="wrap_content"
android:text="New User"
android:textSize="15sp"
android:id="#android:id/button1"
android:textColor="#android:color/white"
android:drawablePadding="6dp"
android:drawableLeft="#drawable/add_round_border_32x32"
android:layout_height="64dp" />
</RelativeLayout>
In our case, we wanted to use the default Button class (to inherit its various styles and behaviors) and we needed to be able to create the button in code. Also, in our case we could have text, an icon (left drawable), or both.
The goal was to center the icon and/or text as a group when the button width was wider than wrap_content.
public class CenteredButton extends Button
{
public CenteredButton(Context context, AttributeSet attrs, int defStyleAttr)
{
super(context, attrs, defStyleAttr);
// We always want our icon and/or text grouped and centered. We have to left align the text to
// the (possible) left drawable in order to then be able to center them in our onDraw() below.
//
setGravity(Gravity.LEFT|Gravity.CENTER_VERTICAL);
}
#Override
protected void onDraw(Canvas canvas)
{
// We want the icon and/or text grouped together and centered as a group.
// We need to accommodate any existing padding
//
float buttonContentWidth = getWidth() - getPaddingLeft() - getPaddingRight();
// In later versions of Android, an "all caps" transform is applied to buttons. We need to get
// the transformed text in order to measure it.
//
TransformationMethod method = getTransformationMethod();
String buttonText = ((method != null) ? method.getTransformation(getText(), this) : getText()).toString();
float textWidth = getPaint().measureText(buttonText);
// Compute left drawable width, if any
//
Drawable[] drawables = getCompoundDrawables();
Drawable drawableLeft = drawables[0];
int drawableWidth = (drawableLeft != null) ? drawableLeft.getIntrinsicWidth() : 0;
// We only count the drawable padding if there is both an icon and text
//
int drawablePadding = ((textWidth > 0) && (drawableLeft != null)) ? getCompoundDrawablePadding() : 0;
// Adjust contents to center
//
float bodyWidth = textWidth + drawableWidth + drawablePadding;
canvas.translate((buttonContentWidth - bodyWidth) / 2, 0);
super.onDraw(canvas);
}
}
Here is my code and working perfect.
<Button
android:id="#+id/button"
android:layout_width="200dp"
android:layout_height="50dp"
android:layout_gravity="center"
android:background="#drawable/green_btn_selector"
android:gravity="left|center_vertical"
android:paddingLeft="50dp"
android:drawableLeft="#drawable/plus"
android:drawablePadding="5dp"
android:text="#string/create_iou"
android:textColor="#color/white" />
public class DrawableCenterTextView extends TextView {
public DrawableCenterTextView(Context context, AttributeSet attrs,
int defStyle) {
super(context, attrs, defStyle);
}
public DrawableCenterTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public DrawableCenterTextView(Context context) {
super(context);
}
#Override
protected void onDraw(Canvas canvas) {
Drawable[] drawables = getCompoundDrawables();
if (drawables != null) {
Drawable drawableLeft = drawables[0];
Drawable drawableRight = drawables[2];
if (drawableLeft != null || drawableRight != null) {
float textWidth = getPaint().measureText(getText().toString());
int drawablePadding = getCompoundDrawablePadding();
int drawableWidth = 0;
if (drawableLeft != null)
drawableWidth = drawableLeft.getIntrinsicWidth();
else if (drawableRight != null) {
drawableWidth = drawableRight.getIntrinsicWidth();
}
float bodyWidth = textWidth + drawableWidth + drawablePadding;
canvas.translate((getWidth() - bodyWidth) / 2, 0);
}
}
super.onDraw(canvas);
}
}
This is now available in the Material Button by default with the app:iconGravity property. However, the Material Button does not allow for setting the background to a drawable (RIP gradients).
I converted the answers by #BobDickinson and #David-Medenjak above to kotlin and it works great.
import android.content.Context
import android.graphics.Canvas
import android.util.AttributeSet
import android.view.Gravity
import androidx.appcompat.widget.AppCompatButton
import kotlin.math.max
class CenteredButton #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = R.attr.buttonStyle
) : AppCompatButton(context, attrs, defStyle) {
init {
gravity = Gravity.LEFT or Gravity.CENTER_VERTICAL
}
override fun onDraw(canvas: Canvas) {
val buttonContentWidth = (width - paddingLeft - paddingRight).toFloat()
var textWidth = 0f
layout?.let {
for (i in 0 until layout.lineCount) {
textWidth = max(textWidth, layout.getLineRight(i))
}
}
val drawableLeft = compoundDrawables[0]
val drawableWidth = drawableLeft?.intrinsicWidth ?: 0
val drawablePadding = if (textWidth > 0 && drawableLeft != null) compoundDrawablePadding else 0
val bodyWidth = textWidth + drawableWidth.toFloat() + drawablePadding.toFloat()
canvas.save()
canvas.translate((buttonContentWidth - bodyWidth) / 2, 0f)
super.onDraw(canvas)
canvas.restore()
}
}
I know it's a bit late, but if anyone looking for another answer, here is another way to add icon without the need to wrap button with a ViewGroup
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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">
<Button
android:id="#+id/btnCamera"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Click!"
android:textAllCaps="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
*need to set textAllCaps to false to make the spannable working
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val buttonLabelBuilder = SpannableStringBuilder(btnCamera.text)
val iconDrawable = AppCompatResources.getDrawable(this, R.drawable.ic_camera)
iconDrawable?.setBounds(0, 0, btnCamera.lineHeight, btnCamera.lineHeight)
val imageSpan = ImageSpan(iconDrawable, ImageSpan.ALIGN_BOTTOM)
buttonLabelBuilder.insert(0, "i ")
buttonLabelBuilder.setSpan(imageSpan, 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
btnCamera.text = buttonLabelBuilder
}
}
I started with #BobDickinson's answer, but it did not cope well with multiple lines. The approach is good, because you still end up with a Button that can properly be reused.
Here is an adapted solution that will also work if the button has multiple lines (Please don't ask why.)
Just extend Button and use the following in onDraw, the getLineRight() is used to look up the actual length of each line.
#Override
protected void onDraw(Canvas canvas) {
// We want the icon and/or text grouped together and centered as a group.
// We need to accommodate any existing padding
final float buttonContentWidth = getWidth() - getPaddingLeft() - getPaddingRight();
float textWidth = 0f;
final Layout layout = getLayout();
if (layout != null) {
for (int i = 0; i < layout.getLineCount(); i++) {
textWidth = Math.max(textWidth, layout.getLineRight(i));
}
}
// Compute left drawable width, if any
Drawable[] drawables = getCompoundDrawables();
Drawable drawableLeft = drawables[0];
int drawableWidth = (drawableLeft != null) ? drawableLeft.getIntrinsicWidth() : 0;
// We only count the drawable padding if there is both an icon and text
int drawablePadding = ((textWidth > 0) && (drawableLeft != null)) ? getCompoundDrawablePadding() : 0;
// Adjust contents to center
float bodyWidth = textWidth + drawableWidth + drawablePadding;
canvas.save();
canvas.translate((buttonContentWidth - bodyWidth) / 2, 0);
super.onDraw(canvas);
canvas.restore();
}
Here is a another solution:
<LinearLayout
android:id="#+id/llButton"
android:layout_width="match_parent"
style="#style/button_celeste"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
style="#style/button_celeste"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawablePadding="10dp"
android:clickable="false"
android:drawableLeft="#drawable/icon_phone"
android:text="#string/call_runid"/>
</LinearLayout>
and the event:
LinearLayout btnCall = (LinearLayout) findViewById(R.id.llButton);
btnCall.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
call(runid.Phone);
}
});
I had the same issue, and I've come up with a solution that doesn't require XML changes or custom Views.
This code snippet retrieves the width of the text and the left/right drawables, and sets the Button's left/right padding so there will only be enough space to draw the text and the drawables, and no more padding will be added.
This can be applied to Buttons as well as TextViews, their superclasses.
public class TextViewUtils {
private static final int[] LEFT_RIGHT_DRAWABLES = new int[]{0, 2};
public static void setPaddingForCompoundDrawableNextToText(final TextView textView) {
ViewTreeObserver vto = textView.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
shinkRoomForHorizontalSpace(textView);
}
});
}
private static void shinkRoomForHorizontalSpace(TextView textView) {
int textWidth = getTextWidth(textView);
int sideCompoundDrawablesWidth = getSideCompoundDrawablesWidth(textView);
int contentWidth = textWidth + sideCompoundDrawablesWidth;
int innerWidth = getInnerWidth(textView);
int totalPadding = innerWidth - contentWidth;
textView.setPadding(totalPadding / 2, 0, totalPadding / 2, 0);
}
private static int getTextWidth(TextView textView) {
String text = textView.getText().toString();
Paint textPaint = textView.getPaint();
Rect bounds = new Rect();
textPaint.getTextBounds(text, 0, text.length(), bounds);
return bounds.width();
}
private static int getSideCompoundDrawablesWidth(TextView textView) {
int sideCompoundDrawablesWidth = 0;
Drawable[] drawables = textView.getCompoundDrawables();
for (int drawableIndex : LEFT_RIGHT_DRAWABLES) {
Drawable drawable = drawables[drawableIndex];
if (drawable == null)
continue;
int width = drawable.getBounds().width();
sideCompoundDrawablesWidth += width;
}
return sideCompoundDrawablesWidth;
}
private static int getInnerWidth(TextView textView) {
Rect backgroundPadding = new Rect();
textView.getBackground().getPadding(backgroundPadding);
return textView.getWidth() - backgroundPadding.left - backgroundPadding.right;
}
}
Notice that:
It actually still leaves some more space than needed (good enough for my purposes, but you may look for the error)
It overwrites whatever left/right padding is there. I guess it's not difficult to fix that.
To use it, just call TextViewUtils.setPaddingForCompoundDrawableNextToText(button) on your onCreate or onViewCreated().
There are several solutions to this problem. Perhaps the easiest on some devices is to use paddingRight and paddingLeft to move the image and text next to each other as below.
Original button
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:layout_marginTop="16dp"
android:text="#string/scan_qr_code"
android:textColor="#color/colorPrimary"
android:drawableLeft="#drawable/ic_camera"
android:paddingRight="90dp"
android:paddingLeft="90dp"
android:gravity="center"
/>
The problem here is on smaller devices this padding can cause unfortunate problems such as this:
The other solutions are all some version of "build a button out of a layout an image and a textview". They work, but completely emulating a button can be tricky. I propose one more solution; "build a button out of a layout an image, a textview, and a button"
Here's the same button rendered as I propose:
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:layout_marginTop="16dp"
android:gravity="center"
>
<Button
android:id="#+id/scanQR"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#drawable/white_bg_button"
/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_centerInParent="true"
android:gravity="center"
android:elevation="10dp"
>
<ImageView
android:id="#+id/scanImage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="8dp"
android:src="#drawable/ic_camera"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="#style/Base.TextAppearance.AppCompat.Button"
android:text="#string/scan_qr_code"
android:textColor="#color/colorPrimary"
/>
</LinearLayout>
</RelativeLayout>
As you can see, the button is now within a relative layout, but it's text and drawableLeft are not part of the button, they are in a separate layout that's placed on top of the button. With this, the button still acts like a button. The gotchas are:
The inner layout needs an elevation for newer versions of Android. The button itself has an elevation greater than the ImageView and TextView, so even though they are defined after the Button, they will still be "below" it in elevation and be invisible. Setting 'android:elevation' to 10 solves this.
The textAppearance of the TextView must be set so that it has the same appearance as it would in a button.
Another quite hacky alternative is to add blank spacer views with weight="1" on each side of the buttons. I don't know how this would affect performance.
<View
android:layout_width="0dp"
android:layout_height="fill_parent"
android:layout_weight="1" />
My activity has a LinearLayout with a single child view. I want both to fill the screen, minus a 12 dp margin.
Unfortunately, the child view is drawn 12 dp too big and gets cut off. Apparently match_parent ignores the layout_margin attribute when calculating the size of the child view. What is the simplest way to fix this?
myActivity.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_margin="12dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.myapp.myView
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
</LinearLayout>
myActivity.java
package com.myapp;
import android.app.Activity;
import android.os.Bundle;
public class myActivity extends Activity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.myActivity);
}
}
myView.java
package com.myapp;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
public class myView extends View {
private Paint paint = new Paint();
public myView(Context context, AttributeSet attrs) {
super(context, attrs);
paint.setColor(0xFFFF0000); //red
paint.setStyle(Paint.Style.STROKE); // for unfilled rectangles
paint.setStrokeWidth(4);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int size = canvas.getWidth(); // width = height (see onMeasure())
canvas.drawRect(0, 0, size, size, paint);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, widthMeasureSpec);
// This gives us a square canvas!
}
}
Child views can have a margin around them, parent views (or view groups like layouts) can have a padding between their boundaries and their child views. In other words, margin is outside a view, padding is inside.
Also, see this excellent explanation: Difference between a View's Padding and Margin
Example with standard View and padding instead of margin:
I created a little example with a standard view instead of your custom one, and using padding for the LinearLayout as suggested above and it works perfectly (see screenshot):
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="12dp"
android:orientation="vertical" >
<View
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#ff0000"/>
</LinearLayout>
Solution
Turns out, the problem was your using canvas.getWidth() in your custom view's onDraw method. Using the view's getWidth() instead, solved the problem. Finally :-)
I want a Button like this:
+-----------------------+
| |
| +-----+ |
| |Image| |
| +-----+ |
| Text |
| |
| |
+-----------------------+
EDIT: Explanation to the picture: I want the COMBINATION of Image and text centered (text ALWAYS below the image)
I want the Button to stretch to a parent object (to make the whole area the button click area) and still align imgage AND text at center.
I achieve only top center alignment with folowing code, but I don't get the desired behaviour...
<Button
android:id="#+id/btInfo"
android:paddingTop="20dp"
android:layout_width="0dp"
android:layout_height="match_parent"
android:gravity="top|center_horizontal"
android:layout_weight="1"
android:background="#drawable/border_button_main_menu"
android:drawableTop="#drawable/bt_info"
android:onClick="onClick"
android:text="#string/info"
android:textColor="#drawable/bt_white_red_text"
android:textSize="15dp" />
changing android:gravity="top|center_horizontal" to android:gravity="center_vertical|center_horizontal" only leads to image centered at top and text centered at bottom...
---- EDIT2 -----
Wanted behaviour:
1) look as described (Image and text is a optical group and the group is centered in the button)
2) text should be part of the button (I want the onclick behaviour to work with selectors)
---- EDIT3 -----
added my own solution... but thanks to all that tried to help
Use the Following Code, your problem will be solve.
<Button
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignBottom="#+id/foreground"
android:layout_alignLeft="#+id/foreground"
android:layout_alignRight="#+id/foreground"
android:layout_alignTop="#+id/foreground"
android:background="#android:drawable/dialog_frame"
android:onClick="clickedMe" />
<RelativeLayout
android:id="#+id/foreground"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentTop="true" >
<TextView
android:id="#+id/button_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="112dp"
android:text="#string/hello_world" />
<ImageView
android:id="#+id/imageView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="#+id/button_text"
android:layout_centerHorizontal="true"
android:paddingBottom="10dip"
android:paddingTop="10dip"
android:src="#drawable/ic_launcher" />
</RelativeLayout>
</RelativeLayout>
------------- EDIT --------------------
oNclick Method:
final TextView text = (TextView) findViewById(R.id.button_text);
RelativeLayout foreground = (RelativeLayout) findViewById(R.id.foreground);
foreground.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
Log.d(TAG, "clicked");
Toast.makeText(getApplicationContext(), "Clicked...!!!",
Toast.LENGTH_LONG).show();
text.setTextColor(Color.RED);
}
});
Try this:
<LinearLayout
android:gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<Button
android:id="#+id/btInfo"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="#drawable/border_button_main_menu"
android:drawableTop="#drawable/bt_info"
android:onClick="onClick"
android:text="#string/info"
android:textColor="#drawable/bt_white_red_text"
android:textSize="15dp" />
</LinearLayout>
Call the setBackgroundDrawable() the text you will then add to the button will appear below the drawable!
I think the best way to achieve this kind of UI is using ImageButton instead of Button. But you can still achieve this by some hackish ways. One of them is here:
How to have Image and Text Center within a Button
You'll just have to mention inner RelativeLayout's orientation as "vertical" in the solution given in above link.
I hope it helps.
May be this can help you too:
How to center icon and text in a android button with width set to "fill parent"
The output you are trying to achieve can not be done in that way using drawableTop.
reason why? - View can set background or drawable and while setting drawable Android gives only 4 options to set bounds of the drawable either top,left,right or bottom and nothing for center.
Now in your XML you are having view's height and width as MATCH_PARENT so every time the drawable set using drawableTOP or LEFT etc. it will go to that edge of the view. which is happening right now with you.
From Comments :
Android OS is ultimately a Software.. and a software has always been developed within it's scope. The thing you are asking is out of scope so it's directly not supported by android using any default Form widget..
I demonstrated that how the android have written the Drawable class and how you can use it so please try to understand the answer and not only the Solution to your problem.. by the way to get click of whole area you can write click on that LinearLayout instead of the button.
Solution will be :
<LinearLayout height = MATCH_PARENT and width = 0dp>
<Button height and width = WRAP_CONTENT with drawableTop=image and gravity = center>
</LinearLayout>
My solution is now deriving from a button... that fullfills all my requirements... I don't know, if the measuring of the text is really exact that way, but it looks to be so... if not, everyone get's the idea behind and can adjust that...
Thanks for the help, though
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.StateListDrawable;
import android.util.AttributeSet;
import android.widget.Button;
public class CenterImageTextButton extends Button {
private Paint mPaint = new Paint();
private String mText = null;
private float mTextSize = 0;
public CenterImageTextButton(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public CenterImageTextButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CenterImageTextButton(Context context) {
super(context);
}
#Override
public void onDraw(Canvas canvas) {
mText = getText().toString();
mTextSize = getTextSize();
mPaint.setStyle(Style.FILL);
mPaint.setColor(getCurrentTextColor());
// get image top
Drawable drawable = getCompoundDrawables()[1];
Drawable curDrawable = null;
if (drawable instanceof StateListDrawable)
curDrawable = ((StateListDrawable)drawable).getCurrent();
else
curDrawable = ((BitmapDrawable)drawable).getCurrent();
Bitmap image = ((BitmapDrawable)curDrawable).getBitmap();
// call default drawing method without image/text
setText("");
setCompoundDrawables(null, null, null, null);
super.onDraw(canvas);
setText(mText);
setCompoundDrawables(null, drawable, null, null);
// get measurements of button and Image
int width = getMeasuredWidth();
int height = getMeasuredHeight();
int imgWidth = image.getWidth();
int imgHeight = image.getHeight();
// get measurements of text
//float densityMultiplier = getContext().getResources().getDisplayMetrics().density;
//float scaledPx = textSize * densityMultiplier;
//paint.setTextSize(scaledPx);
mPaint.setTextSize(mTextSize);
float textWidth = mPaint.measureText(mText);
// draw Image and text
float groupHeight = imgHeight + mTextSize;
canvas.drawBitmap(image, (width - imgWidth) / 2, (height - groupHeight) / 2, null);
canvas.drawText(mText, (width - textWidth) / 2, mTextSize + (height - groupHeight) / 2 + imgHeight, mPaint);
}
}
I want to display TEXT and Icon on a Button.
+----------------------------+
| Icon TEXT |
+----------------------------+
I tried with
<Button
android:id="#+id/Button01"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingLeft="40dip"
android:text="TEXT"
android:drawableLeft="#drawable/Icon" />
But Text and Icon is not in center.
My Text size varies, according to text size Icon and Text should get adjusted to center.
How should i do it?
You can fake it by making a more complex layout, but I'm not sure whether it's worth it. Here's something I hacked together:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<Button
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:layout_alignTop="#+id/foreground"
android:layout_alignBottom="#id/foreground"
android:layout_alignRight="#id/foreground"
android:layout_alignLeft="#id/foreground"
android:onClick="clickedMe" />
<RelativeLayout
android:id="#id/foreground"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<TextView
android:id="#+id/button_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="#string/hello" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toLeftOf="#id/button_text"
android:paddingTop="10dip"
android:paddingBottom="10dip"
android:src="#drawable/icon" />
</RelativeLayout>
</RelativeLayout>
There might be a more concise way to do it. I tend to struggle getting RelativeLayout to do what I want sometimes. Note that you need to pay attention to the z-order (Button needs to appear first in the top level RelativeLayout) and you might need to adjust padding to get it to look the way you want.
Similar to some other approaches, I think a good solution is to extend Button and add the missing functionality by overriding its onLayout method:
public class CenteredIconButton extends Button {
private static final int LEFT = 0, TOP = 1, RIGHT = 2, BOTTOM = 3;
// Pre-allocate objects for layout measuring
private Rect textBounds = new Rect();
private Rect drawableBounds = new Rect();
public CenteredIconButton(Context context) {
this(context, null);
}
public CenteredIconButton(Context context, AttributeSet attrs) {
this(context, attrs, android.R.attr.buttonStyle);
}
public CenteredIconButton(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (!changed) return;
final CharSequence text = getText();
if (!TextUtils.isEmpty(text)) {
TextPaint textPaint = getPaint();
textPaint.getTextBounds(text.toString(), 0, text.length(), textBounds);
} else {
textBounds.setEmpty();
}
final int width = getWidth() - (getPaddingLeft() + getPaddingRight());
final Drawable[] drawables = getCompoundDrawables();
if (drawables[LEFT] != null) {
drawables[LEFT].copyBounds(drawableBounds);
int leftOffset =
(width - (textBounds.width() + drawableBounds.width()) + getRightPaddingOffset()) / 2 - getCompoundDrawablePadding();
drawableBounds.offset(leftOffset, 0);
drawables[LEFT].setBounds(drawableBounds);
}
if (drawables[RIGHT] != null) {
drawables[RIGHT].copyBounds(drawableBounds);
int rightOffset =
((textBounds.width() + drawableBounds.width()) - width + getLeftPaddingOffset()) / 2 + getCompoundDrawablePadding();
drawableBounds.offset(rightOffset, 0);
drawables[RIGHT].setBounds(drawableBounds);
}
}
}
The sample only works for left and right drawables, but could be extended to adjust top and bottom drawables too.
How about this one?
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/lovely_color"
android:clickable="true"
android:onClick="clickHandler">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="no?"
android:textColor="#color/white"
android:layout_centerHorizontal="true"
android:drawableLeft="#drawable/lovely_icon"
android:drawablePadding="10dp"
android:padding="10dp"
android:gravity="center"
android:textSize="21sp"/>
</RelativeLayout>
This should work
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal">
<TextView
android:id="#+id/button_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="hello" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="10dip"
/>
</LinearLayout>
How about using a SpannableString as the text with an ImageSpan?
Button myButton = ...
SpannableString ss = new SpannableString(" " + getString(R.string.my_button_text));
Drawable d = getResources().getDrawable(R.drawable.myIcon);
d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
ImageSpan span = new ImageSpan(d, DynamicDrawableSpan.ALIGN_BOTTOM);
ss.setSpan(span, 0, 1, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
myButton.setText(ss);
You can just set a padding depending on button size and image size:
Button button1 = null;
//initialize button….
ViewGroup.LayoutParams params = button1.getLayoutParams();
int btn1Width = ((int) (0.33 * (double)ecranWidth));
params.width = btn1Width;
button1.setLayoutParams(params);
button1.setPadding((btn1Width/2-9), 0, 0, 0);
//where (btn1Width/2-9) = size of button divided on 2 minux half size of icon…
The easy way (albeit not perfect) is to set the paddingRight to the same width as your icon.
<com.google.android.material.button.MaterialButton
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#string/your_text"
app:icon="#drawable/ic_example"
app:iconGravity="textStart"/>
This is what I did... It can be improved. The text is centered and the icon is to the left. So they both aren't centered as a group.
public class CustomButton extends Button
{
Rect r = new Rect();
private Drawable buttonIcon = null;
private int textImageSeparation = 10;
public CustomButton(Context context, AttributeSet attrs, int defStyle)
{
super(context, attrs, defStyle);
}
public CustomButton(Context context, AttributeSet attrs)
{
super(context, attrs);
}
public CustomButton(Context context)
{
super(context);
}
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
Drawable icon = getButtonIcon();
if(icon != null)
{
int drawableHeight = icon.getIntrinsicHeight();
int drawableWidth = icon.getIntrinsicWidth();
if(icon instanceof BitmapDrawable)
{
Bitmap bitmap = ((BitmapDrawable)icon).getBitmap();
drawableWidth = (int) AndroidScreenUtils.dipToPixels(bitmap.getWidth());
drawableHeight = (int) AndroidScreenUtils.dipToPixels(bitmap.getHeight());
}
else
{
drawableWidth = (int) AndroidScreenUtils.dipToPixels(icon.getIntrinsicWidth());
drawableHeight = (int) AndroidScreenUtils.dipToPixels(icon.getIntrinsicHeight());
}
float textWidth = getLayout().getPaint().measureText(getText().toString());
float left = ((getWidth() - textWidth) / 2) - getTextImageSeparation() - drawableWidth;
int height = getHeight();
int top = (height - drawableHeight) /2;
int right = (int) (left + drawableWidth);
int bottom = top + drawableHeight;
r.set((int) left, top, right, bottom);
icon.setBounds(r);
icon.draw(canvas);
}
}
private Drawable getButtonIcon()
{
return buttonIcon;
}
public void setButtonIcon(Drawable buttonIcon)
{
this.buttonIcon = buttonIcon;
}
private int getTextImageSeparation()
{
return textImageSeparation;
}
public void setTextImageSeparation(int dips)
{
this.textImageSeparation = (int) AndroidScreenUtils.dipToPixels(dips);
}
}
<LinearLayout
style="#style/Sonnen.Raised.Button.Transparent.LightBlueBorder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="30dp"
android:orientation="horizontal"
android:padding="20dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:drawableLeft="#drawable/refresh"
android:drawablePadding="10dp"
android:drawableStart="#drawable/refresh"
android:gravity="center"
android:text="#string/generic_error_button_text"
android:textColor="#color/dark_sky_blue"
android:textSize="20sp"/>
</LinearLayout>
I made a custom component to solve this problem.
Component class:
class CustomImageButton #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0
) : RelativeLayout(context, attrs, defStyleAttr, defStyleRes) {
init {
inflate(context, R.layout.custom_image_button, this)
// Load the styled attributes and set their properties
val typedArray = context.obtainStyledAttributes(
attrs,
R.styleable.CustomImageButton, defStyleAttr, 0
)
val src = typedArray?.getDrawable(R.styleable.CustomImageButton_cib_src)
val text = typedArray?.getText(R.styleable.CustomImageButton_cib_text)
val contentDescription = typedArray?.getText(R.styleable.CustomImageButton_cib_contentDescription)
ivIcon.setImageDrawable(src)
tvText.text = text
ivIcon.contentDescription = contentDescription
typedArray?.recycle()
}
}
Component XML:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:toos="http://schemas.android.com/tools"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_width="fill_parent"
android:layout_height="#dimen/button_height">
<Button
android:id="#+id/bClick"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:layout_alignTop="#+id/foreground"
android:layout_alignBottom="#id/foreground"
android:layout_alignEnd="#id/foreground"
android:layout_alignStart="#id/foreground"/>
<RelativeLayout
android:id="#id/foreground"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<TextView
android:id="#+id/tvText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textColor="#color/textWhite"
toos:text="Some text to test"
toos:ignore="RelativeOverlap"/>
<ImageView
android:id="#+id/ivIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toStartOf="#id/tvText"
android:paddingTop="1dip"
android:paddingBottom="1dip"
android:src="#mipmap/some_image_to_test"
toos:ignore="ContentDescription"/>
</RelativeLayout>
</RelativeLayout>
The resources attributes, attrs.xml:
<declare-styleable name="CustomImageButton">
<attr name="cib_src" format="reference"/>
<attr name="cib_text" format="string"/>
<attr name="cib_contentDescription" format="string"/>
</declare-styleable>
Component use example:
<app.package.components.CustomImageButton
android:id="#+id/cibMyImageButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_height="wrap_content"
android:layout_width="match_parent"
app:cib_src="#mipmap/my_image_to_put_in_the_button"
app:cib_text="Some text to show in the button"
app:cib_contentDescription="icon description"/>
This is a hack, but worked for me with a negative margin:
<Button
android:id="#+id/some_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableStart="#drawable/some_drawable"
android:drawablePadding="-118dp"
android:paddingEnd="28dp"
android:text="#string/some_string" />
A better way would probably be doing this in a custom view
android:layout_gravity="center_vertical|center_horizontal|center" >