Monday, September 7, 2015

Create a Compound Widget for Android Lollipop: Part 4

Integrate a custom widget

This is the fourth in a four part article detailing the implementation of a compound widget for Android Lollipop. In this part I will implement my widget's programming and design interfaces.


Make the widget editable in the design editor

I want to be able to set my widgets selected color value in the Android Studio design editor. In order to do this I need to create an attribute file. I can do this by right-clicking on the res - values folder and selecting New - values file. The file name is not important but is usually called attrs.xml as a convention. This creates a resource file that just has an empty resource tag.

<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

I will add my attribute definition to the resource section:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="ColorPickerView">
        <attr name="selectedColor" format="color" />
    </declare-styleable>
</resources>


I have called my attribute 'selectedColor' instead of just 'color' because there is already a standard Android attribute called 'color'. In fact I could have used that attribute instead of defining my own.
Because I have designed my own attribute I will have to add an extra line to my main_activity.xml layout file before I can use the attribute with my widget.

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    xmlns:custom="http://schemas.android.com/apk/res-auto"
...

The xmlns:custom declaration defines a namespace for all the attributes defined in my project. The res-auto at the end of the URL tells Android Studio to automatically find all the attribute definitions. If I was not using Android Studio or I only wanted to include specific attributes I could replace res-auto with the path to a specific attribute definition file. 
I can now define a color using the custom namespace:


Here I have used auto-completion to pick one of the predefined colors. As you can see the design editor displays the color itself in the margin. It can do this because I defined the attribute's format as 'color'.
As you can see that my widget does not yet reflect the color I have set.


For this to happen I need to modify my class init method to retrieve the color and set the sub widgets' values appropriately.

private void init(AttributeSet attrs, int defStyleAttr, int defStyleRes) {
...
    initColor(attrs);
}
private void initColor(AttributeSet attrs) {
    TypedArray a = getContext().obtainStyledAttributes(attrs,
            R.styleable.ColorPickerView, 0, 0);
    int valueColor = a.getColor(R.styleable.ColorPickerView_selectedColor,
            getResources().getColor(android.R.color.white));
    a.recycle();
    
    updateColorFromValue(valueColor);
}

The last line of my init method now calls a new method called initColor which retrieves the color value that may have been defined in the layout file. If the value has not been defined it defaults to white. It then calls another new method called updateColorFromValue.

private void updateColorFromValue(int color) {
    int r = Color.red(color);
    int g = Color.green(color);
    int b = Color.blue(color);

    redBar.setProgress(r);
    greenBar.setProgress(g);
    blueBar.setProgress(b);
    preview.setBackgroundColor(color);
    invalidate();
}


The updateColorFromValue method sets each of the SeekBars to its respective color value and sets the preview's background color to the selected color. It then calls the invalidate method of the View class to ensure all the sub widgets are redrawn with their new values. 

When I switch back to looking at my main_activity in the design editor and rebuild the project I can see the defined color is now displayed in the preview and its red, green, and blue values are reflected in the position of their respective SeekBars..

Add a programming interface to my widget.

I am going to create two things for my widgets programming interface. Firstly a property that the rest of my code can use to get and set the selected color value of my widget. Secondly an event that will fire whenever the user changes the selected color.

Add a property to my class to hold the current color value

For my new widget to be useful I need a way to set and get the currently selected color from code outside my class. To do this I will start by adding a private class variable called selectedColor. 

public class ColorPickerView extends LinearLayout {
    private int selectedColor;


I need to update the sub controls when this value is changed so I cannot just make it public. Instead I need to create getter and setter methods for it. to do this I right-click on the variable and select 'Generate...' and then 'Getter and Setter'. 





This gives prompts me to choose which class variables I want to create getter and setter methods for. 





selectedColor variable is already selected so I will just click 'OK' to create the new methods.


public int getSelectedColor() {
    return selectedColor;
}

public void setSelectedColor(int selectedColor) {
    this.selectedColor = selectedColor;
}

The getter method just returns the value of the private variable. The setter assigns the given value to the class variable. I am going to change this so that the setter calls my updateColorFromValue method so that the sub widgets will all reflect the new value.

public void setSelectedColor(int selectedColor) {
    this.selectedColor = selectedColor;
    updateColorFromValue(selectedColor);
}
To ensure the getter method works correctly I will  update my initColor method so that the selectedColor variable is set to the initial color value.

private void initColor(AttributeSet attrs) {
    TypedArray a = getContext().obtainStyledAttributes(attrs,
            R.styleable.ColorPickerView, 0, 0);
    selectedColor = a.getColor(R.styleable.ColorPickerView_selectedColor,
            getResources().getColor(android.R.color.white));
    a.recycle();

    updateColorFromValue(selectedColor);
}

Add an event listener to signal when the color has been changed by the user

I want my widget to signal the rest of my code when the color is changed by the user. The usual way to do this for an Android widget is to implement a Listener interface.
To do this I will create a new Interface called OnColorChangeListener.


This creates an empty interface definition to which I will add one method definition called onColorChange.

public interface OnColorChangeListener {
    void onColorChange(int color);
}

I will then add a private class variable to hold a reference to an object that implements this interface and a setter method to set the reference.

private OnColorChangeListener colorChangeListener;

public void setOnColorChangeListener(OnColorChangeListener listener) {
    colorChangeListener = listener;
}


For this to be useful I need to call the Interface's onColorChange method when the user changes the color but only if a listener has been set.

private void updateColorFromSeekBars() {
    int r = redBar.getProgress();
    int g = greenBar.getProgress();
    int b = blueBar.getProgress();

    int color = Color.argb(0xff, r, g, b);

    preview.setBackgroundColor(color);

    if(colorChangeListener != null) {
        colorChangeListener.onColorChange(color);
    }
}


I can test this by adding a listener in the onCreate method of my test project's MainActivity class.

public class MainActivity extends Activity {
    private ColorPickerView colorPicker;

    @Override    
protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        colorPicker = (ColorPickerView) findViewById(R.id.colorPicker);
        
        colorPicker.setOnColorChangeListener(new OnColorChangeListener() {
            @Override
            public void onColorChange(int color) {
                Log.d("MainActivity", "onColorChange");
            }
        });
    }

My compound widget is now complete.


This concludes this part of the article. You can read the other parts here:



No comments:

Post a Comment