Click here to Skip to main content
15,867,594 members
Articles / Mobile Apps / Android

An Efficient Way to Make Button Color Change on Android

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
5 Jul 2015BSD6 min read 59.3K   4   3   1
This is an efficient way to make button color change on Android

Buttons can change color when a state changed (e.g. pressed, disabled, highlighted). But, it requires some efforts to draw each state. If you read this article, you will find a method of how to easily make buttons that change color on state change. In the end of the article, you can find ready to use source code. It can be used even in commercial applications without a fee. If you are writing a custom view, the article will be useful for you too, as I will explain how to implement custom view with custom attributes.

How It Can Be Implemented

Android provides a flexible drawable selector mechanism. It can be used to change view appearance according to the view’s state. Each state is presented by a separate section. For example: button that has one background colors in normal, disabled, pressed, highlighted states. Please look at example code below:

button_1_background.xml
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!— pressed state -->
<item android:drawable="@drawable/button_1_selected" android:state_pressed="true"/>
<!-- focused state -->
<item android:drawable="@drawable/button_1_focused" android:state_focused="true"/>
<!-- default state -->
<item android:drawable="@drawable/button_1_normal"/>
</selector>

Drawables for each state (button_1_selected, button_1_focused, button_1_normal) must be defined in drawables folder:

button_1_normal.xml
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="@color/button_1_normal_background"/>
    <corners android:radius="10dip"/>
</shape>
button_1_focused.xml
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="@color/button_1_focused_background"/>
    <corners android:radius="10dip"/>
</shape>
button_1_selected.xml
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="@color/button_1_selected_background"/>
    <corners android:radius="10dip"/>
</shape>

And, then set button’s background:

android:background="@drawable/button_1_background" 

This approach is great and very flexible. But when your app has many buttons, and each button has different colors, it becomes difficult to maintain all these XML files for each button. If you change normal state button color, you have to change colors for other states. In the example above, it will require 4 XML files per one button. And what will be if you have 10 or more buttons in your app?

To make it clear about what I’m writing, please look at the screenshots below:

 main screen.

 send screen.

These screenshots are from our free product BMEX (It can be found here).

You can see main and send app’s screens. Both screens are made in Metro style. Each screen has 6 buttons with different colors. And button’s color is changed when the state of the button is changed. We have 12 buttons in total, and so, we require 12 drawable selector XML files and 24 drawable state XML files. When an app evolves, it’s possible that new screens with new buttons will be added. It’s not an easy task to maintain all this stuff.
To make this process more easy and efficient, we decided to find a more efficient solution - we have implemented our own custom button view. This is a button implementation with easy initialization. We called it RoundButton, because it supports rounded corners.

In another of our product, we required the highlight feature. We decided to not create a separate custom view for it. So we have added it to RoundButton. Please look at the screenshot below:

 highlighed.

As you can see, buttons on the screen can be selected or unselected (list icon at the top and plus icons for each element). When a button is selected, it’s highlighted state is set to true, when it’s unselected – the highlighted state is set to false. And the button's appearance changes appropriately. In the example above, highlight mode “image” is used. In this mode, visible pixels of the image are drawn in highlight color.
First, we define an attribute set for RoundButton. This is a set of attributes that can be set up through layout XML.

attrs_round_button.xml
<resources>

    <declare-styleable name="RoundButton">
        <attr name="image" format="reference"/>
        <attr name="bgcolor" format="color"/>
        <attr name="text" format="string"/>
        
        <attr name="radius" format="float"/>
        
        <attr name="highlightColor" format="color"/>
        <attr name="highlightMode" format="enum">
            <enum name="none" value="0"/>
            <enum name="image" value="1"/>
            <enum name="background" value="2"/>
        </attr>
    </declare-styleable>

</resources>

We added image, bgcolor, text, border round radius, highlightColor and highlightMode attributes. Pressed state color will be derived from bgcolor (it will be described later).

Button Implementation

First, we need to implement constructor and parse arguments. We create 3 different constructors:

public RoundButton(Context context, AttributeSet attrs, int defStyle) {
	super(context, attrs, defStyle);

	init(attrs, defStyle);
}

public RoundButton(Context context, AttributeSet attrs) {
	super(context, attrs);

	init(attrs, 0);
}

public RoundButton(Context context) {
	super(context);
	
	init(null, 0);
}

All of these constructors call the init method.

Now, we need to implement the init method. It gets attribute set and default style as input arguments. In the init method, we retrieve attribute values and initialize internal variables. If null is passed as attribute set, default values will be used.

private void init(AttributeSet attrs, int defStyle) {
	Drawable image;
	int bgcolor;
	String text;
		
	if (attrs != null) {
		final TypedArray a = getContext().obtainStyledAttributes(attrs,
				R.styleable.RoundButton, defStyle, 0);

		image = a.getDrawable(R.styleable.RoundButton_image);

		bgcolor = a.getColor(R.styleable.RoundButton_bgcolor, 0xffffffff);
			
		text = a.getString(R.styleable.RoundButton_text);
			
		radius = a.getFloat(R.styleable.RoundButton_radius, 12.0f);
			
		highlightMode = HighlightMode.getValue(a.getInt
		(R.styleable.RoundButton_highlightMode, HighlightMode.None.ordinal()));
			
		highlightColor = a.getColor(R.styleable.RoundButton_highlightColor, 0xff00b5ff);
			
		a.recycle();
	}
	else {
		image = null;
			
		text = "";
			
		bgcolor = 0xff808080;
			
		radius = 12.0f;
		
		highlightMode = HighlightMode.None;
			
		highlightColor = 0xff00b5ff;
	}
		
	init(image, bgcolor, text);
}

Then, we create another init method. This method will create objects, required for rendering button’s contents. This init method is made public, because it needs to be called when RoundButton is created from code. It creates background and pressed paint - objects used to draw background in normal and pressed states. Pressed color is made from bgcolor by making a brigher version of it. Method that makes brigher version of color is brigher. It will be described later. Highlight mode is initialized here. If background highlight is set, we create highlight paint, that will be used to draw button background when highlighted. If image highlight mode is set, we create highlight image. Image creation code is in createHighlightImage method, which will be shown later.

public void init(Drawable image, int bgcolor, String text) {
	this.image = image;
		
	bgpaint = new Paint(Paint.ANTI_ALIAS_FLAG);
	bgpaint.setColor(bgcolor);
		
	pressedBgpaint = new Paint(Paint.ANTI_ALIAS_FLAG);
	pressedBgpaint.setColor(brighter(bgcolor));
		
	if (text == null)
		text = "";

	this.text = text;
		
	textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
	textPaint.setColor(0xffffffff);
	textPaint.setTextAlign(Paint.Align.CENTER);
	textPaint.setTextSize(pixelsToSp(getContext(), textSize));
		
	if (highlightMode == HighlightMode.Background) {
		highlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
		highlightPaint.setColor(highlightColor);
	}
	else if (highlightMode == HighlightMode.Image) {
		highlightImage = createHighlightImage();
	}
		
	setClickable(true);
}

To get value of pressed state color, we create method brighter. It takes color as argument and returns a brighter version of this color. The method is simple:

public static int brighter(int color) {
	int d = 50;
	
	int r = Color.red(color);
	int g = Color.green(color);
	int b = Color.blue(color);
	
	int incr = maxIncr(r, g, b, d);
	
	r += incr;
	g += incr;
	b += incr;
	
	if (incr < d) {
        r = 0xbf;
        g = 0xbf;
        b = 0xbf;
    }
	
	return Color.rgb(r, g, b);
}

The next method is createHighlightImage. It’s called from method shown above when image highlight mode is set. There is some tricky code in its beginning. It’s required to get image’s pixels. Then we process pixels - if pixel is not transparent (alpha != 0), we replace it with highlight color value, but if pixel is not transparent, we don't change it. By this operation, we create highlighed version of the image. Then, we setup modified pixels back to bitmap. And in the end of the method, we create and return BitmapDrawable.

private Drawable createHighlightImage() {
	int width = image.getIntrinsicWidth();
	int height = image.getIntrinsicHeight();
	
	Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
	
	Canvas canvas = new Canvas(bitmap);
	image.setBounds(0, 0, width, height);
	image.draw(canvas);
	
	int count = bitmap.getWidth() * bitmap.getHeight();
	int pixels[] = new int[count];
	
	bitmap.getPixels(pixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
	for (int n = 0; n < count; n++) {
		boolean v = (Color.alpha(pixels[n])) != 0;
		
		if (v) {
			int pixel = pixels[n];
			
			int alpha = Color.alpha(pixel);
			int red = Color.red(highlightColor);
			int green = Color.green(highlightColor);
			int blue = Color.blue(highlightColor);
			
			int color = Color.argb(alpha, red, green, blue);
			
			pixels[n] = color;
		}
	}
	
	bitmap.setPixels(pixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
	
	return new BitmapDrawable(getResources(), bitmap);
}

In order to process state changes, we need to handle touch event. We implement touch handler. When button is touched, its state is changed to pressed and contents redrawn. When button is untouched, its pressed flag is set to false and contents redrawn.

@Override
public boolean onTouchEvent(MotionEvent event) {
	int action = event.getActionMasked();
	
	switch (action) {
	case MotionEvent.ACTION_DOWN:
		pressed = true;
		invalidate();
		break;			
	case MotionEvent.ACTION_UP:
		pressed = false;
		invalidate();
		break;
	case MotionEvent.ACTION_CANCEL:
	case MotionEvent.ACTION_OUTSIDE:
	case MotionEvent.ACTION_HOVER_EXIT:
		pressed = false;
		invalidate();
		break;
	}
	
	return super.onTouchEvent(event);
}

Then, we implement button onDraw method. This method draws button’s contents. It’s called when custom view is first shown and on each redraw.

Java
protected void onDraw(Canvas canvas) {
	RectF bounds = new RectF(0, 0, getWidth(), getHeight());
	
	Drawable image = null;
	Paint bgPaint = null;
	
	switch (highlightMode) {
	case None:
		image = this.image;
		bgPaint = pressed ? pressedBgpaint : this.bgpaint;
		break;
	case Background:
		image = this.image;
		if (pressed)
			bgPaint = pressedBgpaint;
		else 
			bgPaint = highlighted ? highlightPaint : this.bgpaint;
		break;
	case Image:
		image = highlighted ? highlightImage : this.image;
		bgPaint = pressed ? pressedBgpaint : this.bgpaint;
		break;
	}
	
	if (radius != 0.0f)
		canvas.drawRoundRect(bounds, radius, radius, bgPaint);
	else 
		canvas.drawRect(bounds, bgPaint);
	
	Rect textBounds = new Rect();
	if (text.length() > 0)
		textPaint.getTextBounds(text, 0, text.length(), textBounds);
	
	float h_dst = ((image != null) ? image.getMinimumHeight() + 
	((text.length() > 0) ? spacing : 0) : 0) + textBounds.height();
	
	float xd = (bounds.width() - ((image != null) ? image.getMinimumWidth() : 0)) / 2;
	float yd = (bounds.height() - h_dst) / 2; 
	
	if (image != null) {
		image.setBounds((int) xd, (int) yd, (int) 
		(xd + image.getMinimumWidth()), (int) (yd + image.getMinimumHeight()));
		image.draw(canvas);
	}
	
	float xt = (bounds.width() - 0 * textBounds.width()) / 2;
	float yt = yd + ((image != null) ? image.getMinimumHeight() + 
	((text.length() > 0) ? spacing : 0) : textBounds.height());// + textBounds.height();
	
	canvas.drawText(text, xt, yt, textPaint);
	
	if (checked && checkable && checkedImage != null) {
		checkedImage.setBounds((int) (bounds.width() - 
		checkedImage.getMinimumWidth()), (int) (bounds.height() - checkedImage.getMinimumHeight()),
				(int) bounds.width(), (int) bounds.height());
		checkedImage.draw(canvas);
	}
}

Usage

To integrate RoundButton to your code, you need to download source code archive from. In the archive, you will find Eclipse project, sources and res XML files. You can copy them to your app project. Or compile RoundButton project and add it to your project as library.

If you use visual editor, just select RoundButton from controls list and after adding it, set up its properties.
Except visual editor, RoundButton can be inserted both from layout XML and from code. To add it from layout XML, you can use. Example is shown below:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:focusable="false"
    android:focusableInTouchMode="false"
    android:descendantFocusability="blocksDescendants"
    android:orientation="horizontal" 
    xmlns:app="http://schemas.android.com/apk/res/com.bitgriff.bamp">
         
   	<com.bitgriff.bamp.helpers.RoundButton
        android:id="@+id/button"
        app:radius="0"
    	app:image="@drawable/ic_addtomedialibrary"
        app:bgcolor="@color/transparent"
        app:highlightMode="image"
        android:layout_width="40dip"
        android:layout_height="80dip"
        android:layout_centerVertical="true"
        android:layout_alignParentRight="true"/>
   	
</RelativeLayout>

To add RoundButton from code, create new RoundButton instance. Call its init method passing it image (can be null), bgcolor and text. And add RoundButton to your ViewGroup:

Java
roundButton = new RoundButton(context);
roundButton.init(image, bgcolor, text);

Further Ideas

It’s also possible to change RoundButton's shape. For example, make circle button as it can be found in many modern Android apps. It’s also possible to make image position configurable (left, right, top, bottom). You can easily make it with source code attached to the article.

Summary

In the article, I described how to implement custom button that changes its background color on state change. This simple component saved us many hours. And I hope that it will be useful for you too. Thank you for your interest and time! I hope the article was useful for you. Please post your suggestions. Enjoy coding!

Links

License

This article, along with any associated source code and files, is licensed under The BSD License


Written By
CEO BitGriff LLC
Russian Federation Russian Federation
My name is Andrey Moskvichev.

I'm a software developer with more than 14 years of programming experience.

I specialize in networking, Unix systems (Linux, FreeBSD), mobile programming, computer graphics, software architecture, reverse engineering, data processing systems, AI, computer vision.

I'm interested in all new cutting edge technologies.

Comments and Discussions

 
QuestiononClickEvent Pin
ademcan256-Jun-16 2:22
ademcan256-Jun-16 2:22 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.