Monday, September 7, 2015

Create a Compound Widget for Android Lollipop: Part 3

Create a class for my compound widget

This is the third in a four part article detailing the implementation of a compound widget for Android Lollipop. In this part I will implement a class to hold the code that defines my widget's behavior. I am going to start by implementing the minimum required to be able to use my widget in Android Studio's Design editor.


Create the class file

To create a new class file I will right click on my projects main java directory and select New - Java Class.




I am going to call my new class ColorPickerView. Despite calling it a view I am actually going to define it as a descendant of  LinearLayout.

public class ColorPickerView extends LinearLayout {

}

Adding constructors

LinearLayout descendants are required to implement several constructors. The editor shows this as an error. I can right click on the error symbol to have the editor create the constructors I need.


This opens another window that lists all the constructors that could be implemented to match the super class constructors.


 I will select all four constructors and click OK.

public class ColorPickerView extends LinearLayout {
    public ColorPickerView(Context context) {
        super(context);
    }

    public ColorPickerView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public ColorPickerView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public ColorPickerView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }
}



Next I will implement an initialization method that all the constructors can call.

public class ColorPickerView extends LinearLayout {
    public ColorPickerView(Context context) {
        super(context);
        init(null, 0, 0);
    }

    public ColorPickerView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(attrs, 0, 0);
    }

    public ColorPickerView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(attrs, defStyleAttr, 0);
    }

    public ColorPickerView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(attrs, defStyleAttr, defStyleRes);
    }

    private void init(AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    }
}



Finally I need to associate my class with my layout. This is done by inflating my layout in my init method.

private void init(AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    LayoutInflater inflater = (LayoutInflater) getContext()
            .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    inflater.inflate(R.layout.view_color_picker, this, true);
}


Add an instance of my compound widget to  my test project's activity

I can add an instance of my new control to the projects main activity by adding a new tag to its layout's XML file.

<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" android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">

    <TextView android:text="@string/hello_world" android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <com.example.colorpickerexample.ColorPickerView
        android:layout_width="fill_parent"
         android:layout_height="wrap_content"/>
    
</RelativeLayout>


This gives me an error in the design preview window.


Rebuilding the project fixes this.


Now all I need to do is to fix the layout so that my compound widget does not overlap the 'Hello world' TextView widget. Because the activity was created with a RelativeLayout. I need to give both controls IDs and define their relationship using those IDs.

<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" android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">

    <TextView android:text="@string/hello_world" android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/helloText"/>

    <com.example.colorpickerexample.ColorPickerView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:id="@+id/colorPicker"
        android:layout_below="@+id/helloText"/>

</RelativeLayout>

My control is now displayed correctly below the original "hello world" text.



Make the widget respond to the user

At this point I can run my test app in the emulator and move my widget's sliders. However that has no effect on the color shown by the preview. To fix this I need to add references and event handlers for the sub widgets.

Add references for the sub widgets

To add references to the sub widget I will need to add class variables to hold the references and then use the findViewById method to retrieve the references from the inflated layout.

public class ColorPickerView extends LinearLayout {    private SeekBar redBar;
    private SeekBar greenBar;
    private SeekBar blueBar;
    private ImageView preview;
...

private void init(AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    LayoutInflater inflater = (LayoutInflater) getContext()
            .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    inflater.inflate(R.layout.view_color_picker, this, true);
    preview = (ImageView) findViewById(R.id.preView);
    redBar = (SeekBar) findViewById(R.id.redBar);
    greenBar = (SeekBar) findViewById(R.id.greenBar);
    blueBar = (SeekBar) findViewById(R.id.blueBar);
}


The layout inflater creates a view tree and associates it with the instance of the class that called its inflate method. findViewById is a method that is inherited from the View class that returns a reference to a widget in that view tree.

Add event handlers for the sub controls

The only event I want to handle is when the user moves any of my sliders and the action I want to take in response to that event is to update the color shown in the preview. Because all three sliders will have identical behavior I can create a single event handler and assign it to all three widgets.

private void init(AttributeSet attrs, int defStyleAttr, int defStyleRes) {
...
    
    addEventHandlers();
}
private void addEventHandlers() {
    SeekBar.OnSeekBarChangeListener listener = new SeekBar.OnSeekBarChangeListener() {

        @Override
        public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
            updateColorFromSeekBars();
        }

        public void onStartTrackingTouch(SeekBar seekBar) {
        }

        public void onStopTrackingTouch(SeekBar seekBar) {
        }
    };
    redBar.setOnSeekBarChangeListener(listener);
    greenBar.setOnSeekBarChangeListener(listener);
    blueBar.setOnSeekBarChangeListener(listener);
}

private void updateColorFromSeekBars() {
}

I have put the event handler definition and assignments into a separate method to avoid bloating my init method. The event handler for SeekBar changes requires three methods be defined although I am only going to use one of them. The onProgressChanged method will be called every time one of the SeekBars is moved. All my event handler will do is call a new method I have defined for my class called updateColorFromSeekBars.

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);
}


This method sets the background color of the preview widget to the color given by the red, green, and blue values from the three SeekBars. The  argb method of the Color class creates a color value based on 4 values between 0 and 255.  The first value is the alpha which controls the amount of transparency used  when rendering the color. I do not want any transparency so I am setting the alpha to its maximum value. The other three parameters are the red, green, and blue values from my SeekBars.

Setting the ranges

In order for this to work properly I need to set the range of each SeekBar to 0-255. Because the SeekBar is based on a progress bar the minimum value is already fixed as 0. For the maximum value I could either set it in my init method as soon as I get references to the SeekBars or I could define it in the layout file.


I can now run the test project in the emulator and see the preview area is updated to match any changes I make to the color sliders.


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



No comments:

Post a Comment