How to split a String text (the blue one) between two TextViews so that it starts in one TextView and continues in the other one when there is no more room left.
The maximum width of the two TextViews is not the same.
An example would be filling a form. The black text is static (two labels).
Another approche could be having only one TextView (for the blue text) that has padding left and right but only for the first line.
For each padding, the padding size would equal the label width.
you should do it programmatically
int textSize = 16;
textView2.setTextSize(textSize);
textView1.setTextSize(textSize);
final float scale = getResources().getDisplayMetrics().density;
int dpWidthInPx = (int) (100 * scale);
int countTv1Chars = dpWidthInPx / textSize;
String tv1String = string.substring(0, countTv1Chars);
String tv2String = string.substring(countTv1Chars, string.length() - 1);
textView1.setText(tv1String);
textView2.setText(tv2String);
in xml
<TextView
android:id="#+id/tv1"
android:lines="1"
android:layout_width="100dp"
android:layout_height="wrap_content" />
<TextView
android:id="#+id/tv2"
android:lines="1"
android:layout_width="100dp"
android:layout_height="wrap_content" />
I want to make the design like image and also display same in phone and 7 inch tab.
I am using Linear layout by dividing the view in 5 part of the screen with using Framlayout draw a line but not possible to achieve like this image.
What's the other option like using canvas or any other better option.
First Image is displing expected result.
and other two are getting result.
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<gradient
android:angle="360.0"
android:endColor="#A29AA4"
android:startColor="#A29AA4" />
</shape>
Below layout
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="20dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:weightSum="5">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center">
<View
android:id="#+id/mView_circle1"
android:layout_width="20dp"
android:layout_height="20dp"
android:background="#drawable/circleshape" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center">
<View
android:id="#+id/mView_circle2"
android:layout_width="20dp"
android:layout_height="20dp"
android:background="#drawable/circleshape" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center">
<View
android:id="#+id/mView_circle3"
android:layout_width="20dp"
android:layout_height="20dp"
android:background="#drawable/circleshape" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center">
<View
android:id="#+id/mView_circle4"
android:layout_width="20dp"
android:layout_height="20dp"
android:background="#drawable/circleshape" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center">
<View
android:id="#+id/mView_circle5"
android:layout_width="20dp"
android:layout_height="20dp"
android:background="#drawable/circleshape" />
</LinearLayout>
</LinearLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_gravity="center_vertical"
android:background="#A29AA4">
</RelativeLayout>
</FrameLayout>
This is easier and cleaner in canvas. Here is how you would do the first one.. You can replicate this with slight modifications for the other two.
Create a Canvas View:
public class CanvasView extends View {
Paint bPaint;
RectF coordbounds;
public CanvasView(Context context) {
super(context);
}
private void init()
{
bPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
bPaint.setStyle(Paint.Style.FILL_AND_STROKE);
bPaint.setColor(Color.BLACK);
}
#Override
public void onDraw(android.graphics.Canvas canvas)
{
super.onDraw(canvas);
canvas.drawLine(coordbounds.left,coordbounds.centerY(),
coordbounds.right,coordbounds.centerY(),bPaint);
int circledia=20;
//Divide the line into four segments and subtract 2 * half the radii
float actualspan = (coordbounds.right - coordbounds.left) - (2 * circledia/2);
//Segment the line into 3 parts
float interlinesegments = actualspan/(4-1);
for(int i=0; i<4;i++)
{
canvas.drawCircle(coordbounds.left + circledia/2 +
(i*interlinesegments),
coordbounds.centerY(),10,bPaint);
}
}
}
Create a layout to hold the view and call this view in your activity:
LinearLayout layout = (LinearLayout) findViewById(R.id.root);
CanvasView view = new CanvasView(this);
layout.addView(view);
oops, I forgot . :-) Please add this method in CanvasView class to declare the bounding box and set the layout:
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
float xpad = (float) (getPaddingLeft() + getPaddingRight());
float ypad = (float) (getPaddingTop() + getPaddingBottom());
float coww = 0.0f, cohh = 0.0f, coll = 0.0f;
init();
coww = (float) w - xpad;
cohh = (float) h - ypad;
// Create a bounding box
coordbounds = new RectF(0.0f,0.0f,
coww,cohh);
}
EDIT : Change the above methods for bitmap
private void init()
{
bPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
bPaint.setStyle(Paint.Style.FILL_AND_STROKE);
bPaint.setColor(Color.BLACK);
bitmap = BitmapFactory.decodeResource(getResources(),
R.drawable.button);
}
Change onDraw as follows:
#Override
public void onDraw(android.graphics.Canvas canvas)
{
super.onDraw(canvas);
canvas.drawLine(coordbounds.left,coordbounds.centerY(),
coordbounds.right,coordbounds.centerY(),bPaint);
int rectwidth=bitmap.getWidth();
int rectheight=bitmap.getHeight();
//Divide the line into four segments and subtract 2 * half the radii
float actualspan = (coordbounds.right - coordbounds.left) - (2 * rectwidth/2);
//Segment the line into 3 parts
float interlinesegments = actualspan/(4-1);
for(int i=0; i<4;i++)
{
float left= coordbounds.left + (i * interlinesegments);
float top= coordbounds.centerY()-rectheight/2;
float right = coordbounds.left+(i * interlinesegments)+rectwidth;
float bottom= coordbounds.centerY()+ rectheight/2;
canvas.drawBitmap(bitmap,null,new RectF(left,top,right,bottom),null);
}
}
With the help of above code and previous code I made this Combination of circle shape and bitmap.
#Override
public void onDraw(android.graphics.Canvas canvas)
{
super.onDraw(canvas);
canvas.drawLine(coordbounds.left, coordbounds.centerY(),
coordbounds.right, coordbounds.centerY(), bPaint);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_myprofile);
int rectwidth=bitmap.getWidth();
int rectheight=bitmap.getHeight();
//Divide the line into four segments and subtract 2 * half the radii
float actualspan_image = (coordbounds.right - coordbounds.left) - (2 * rectwidth/2);
//Segment the line into 3 parts
float interlinesegments_bitmap = actualspan_image / (4 - 1);
int circledia = 20;
//Divide the line into four segments and subtract 2 * half the radii
float actualspan = (coordbounds.right - coordbounds.left) - (2 * circledia/2);
//Segment the line into 3 parts
float interlinesegments = actualspan/(4-1);
for(int i=0; i<4;i++)
{
float left= coordbounds.left + (i * interlinesegments_bitmap);
float top= coordbounds.centerY()-rectheight/2;
float right = coordbounds.left+(i * interlinesegments_bitmap)+rectwidth;
float bottom= coordbounds.centerY()+ rectheight/2;
if(i==1){
canvas.drawBitmap(bitmap,null,new RectF(left,top,right,bottom),null);
}else{
canvas.drawCircle(coordbounds.left + circledia / 2 +
(i * interlinesegments),
coordbounds.centerY(), 10, bPaint);
}
}
}
A sprite on my android game is set to fall by 5 pixels every 100 milliseconds, this works fine the only problem is that the ImageView itself is only 53dp high, if I make it any bigger the image inside scales with it. Since the ImageView is only 53dp high the image disappears after 1100 milliseconds as it scrolls outside the boundaries of the imageview.
I need the layout height of the ImageView to fill_parent but I need the image to stay the same size instead of scaling with it, here's my current code:
<ImageView
android:id="#+id/blueman"
android:layout_width="0dip"
android:layout_height="53dp"
android:paddingRight="300dp"
android:layout_weight="0.03"
android:src="#drawable/fall" />
Thanks in advance :)
since you didn't give the full code of the layout, I'll make some assumptions...
you're talking about setting your sprite's height to the screen's height without scaling?
There should be a difference between your screen size (that is the root layout item) and the sprites in it. I guess you declared your layout as...:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
style="?gameBackground"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<Button
android:id="#+id/btTap"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginLeft="14dp"
android:layout_marginTop="350dp"
android:background="#drawable/tap"
android:visibility="visible" />
<Button
android:id="#+id/btCellR1C1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginLeft="490dp"
android:layout_marginTop="155dp"
android:background="#drawable/cell_red" />
The only thing I had to cope with, was the scaling of my sprites depending on the device's resolution with such a method:
public static void scaleView(View view, int top, int left, int width,
int height) {
top = Math.round(top * scaleY);
left = Math.round(left * scaleX);
width = Math.round(width * scaleX);
height = Math.round(height * scaleY);
LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT);
params.height = height;
params.width = width;
params.topMargin = top;
params.leftMargin = left;
view.setLayoutParams(params);
view.setSoundEffectsEnabled(false);
}
Please give us more details to help you
Best regards
Serge
Good evening! I'm trying to setPadding on a custom View i built and the native setPadding() did nothing so i wrote my own... After a while i realized that setPadding gets called several times after my original call and i have no idea why... Please help :) (I realize that my custom setPadding maybe quite excessive ^^)
Here is the XML containing the View. It's the PieChart.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/PieDialog_llParent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<TextView
android:id="#+id/PieDialog_tvHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="Header"
android:textAppearance="?android:attr/textAppearanceLarge" />
<TextView
android:id="#+id/PieDialog_tvDiv1"
android:layout_width="match_parent"
android:layout_height="2dp"
android:textSize="0sp"/>
<TextView
android:id="#+id/PieDialog_tvDiv2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="0sp" />
<com.SverkerSbrg.Spendo.Statistics.Piechart.PieChart
android:id="#+id/PieDialog_Pie"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:id="#+id/PieDialog_tvDiv3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="0sp" />
<FrameLayout
android:id="#+id/PieDialog_flClose"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<TextView
android:id="#+id/PieDialog_tvClose"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="Large Text" />
</FrameLayout>
</LinearLayout>
And here is the code where i use the xml:
package com.SverkerSbrg.Spendo.Transaction.TransactionList.PieDialog;
imports...
public class PieDialog extends SpendoDialog{
private TransactionSet transactionSet;
private TransactionGroup transactionGroup;
private GUI_attrs gui_attrs;
private PieData pieData;
private PieChart pie;
private TextView tvHeader;
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
LayoutInflater inflater = getActivity().getLayoutInflater();
View view = inflater.inflate(R.layout.transaction_list_pie_dialog, null);
LinearLayout llParent = (LinearLayout) view.findViewById(R.id.PieDialog_llParent);
llParent.setBackgroundColor(gui_attrs.color_Z0);
tvHeader = (TextView) view.findViewById(R.id.PieDialog_tvHeader);
tvHeader.setTextSize(gui_attrs.textSize_header);
TextView tvDiv1 = (TextView) view.findViewById(R.id.PieDialog_tvDiv1);
tvDiv1.setBackgroundColor(gui_attrs.color_Z2);
TextView tvDiv2 = (TextView) view.findViewById(R.id.PieDialog_tvDiv2);
tvDiv2.setPadding(0, gui_attrs.padding_Z0, 0, 0);
PieChart pie = (PieChart) view.findViewById(R.id.PieDialog_Pie);
pie.setPadding(40, 10, 40, 10);
builder.setView(view);
AlertDialog ad = builder.create();
return ad;
}
public void initialize(GUI_attrs gui_attrs, TransactionSet transactionSet, long groupIdentifier){
this.gui_attrs = gui_attrs;
this.transactionSet = transactionSet;
}
}
Just to extrapolate on my comment, it is your custom View object's responsibility to respect the padding that is set. You can do something like the following to make sure that you handle that case:
onMeasure()
int desiredWidth, desiredHeight;
desiredWidth = //Determine how much width you need
desiredWidth += getPaddingLeft() + getPaddingRight();
desiredHeight = //Determine how much height you need
desiredHeight += getPaddingTop() + getPaddingBottom();
int measuredHeight, measuredWidth;
//Check against the MeasureSpec -- if it's MeasureSpec.EXACTLY, or MeasureSpec.AT_MOST
//follow those restrictions to determine the measured dimension
setMeasuredDimension(measuredWidth, measuredHeight);
onLayout()
int leftOffset = getPaddingLeft();
int topOffset = getPaddingTop();
//layout your children (if any) according to the left and top offsets,
//rather than just 0, 0
onDraw()
canvas.translate (getPaddingLeft(), getPaddingTop());
//Now draw your stuff as normal
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" />