as4: Color Picker

Last revised: February 11, 2021
Assigned:
  • 03 February, 2021
Due:
  • Code due 18-Feb, 10:00pm
  • Code lock, Reflection due 20-Feb, 10:00pm
  • Reflection lock (with 10% deduction) 21-Feb, 10:00pm

Android Goals:

  • Understand Android event handling APIs
  • Handle touch input properly
  • Understand app lifecycle
  • Save app state in Bundle

HCI Goals:

  • Create non-rectangle interactor
  • Propositional Production System
  • Event handlers and event bubbling
  • Callbacks
  • Interactor Design

GitGrade links

Goal

There are two parts to this assignment, creating an RGB color picker interactor which lets you choose a color on a rainbow circle (color wheel), and creating a second color picker of your choice. Both are used in a very simple application that displays the chosen color.

Here is a video of our solution playing on the Pixel 2 emulator (API level 28)

You will play two developer roles in this assignment

Component Developer Role

Your primary goal in this assignment is to create CircleColorPickerView.java and MyColorPickerView.java. These are your custom interactors and must be implemented so they can be used by any application. Both inherit from ColorPickerView.java which inherits AbstractColorPickerView.java. AbstractColorPickerView.java must remain untouched.

A second goal in this assignment is to properly handle input by separating the model code, geometric details and drawing (view code) from the controller code (which is in onTouchEvent in this case). You will implement onTouchEvent in ColorPickerView and handle model updates and the essential geometry and drawing methods in your custom views.

Tasks in ColorPickerView

Tasks in CircleColorPickerVIew

Tasks in MyColorPickerView

Interface Programmer Role

You will also edit MainActivity.java. This is your application which will use your custom color picker interactor. MainActivity inherits from AbstractMainActivity which must remain untouched.

Tasks for MainActivity

Note: We will be asking you to re-use your color picker (CircleColorPickerView and ColorPickerView) in a later assignment so it is important that you understand how the custom interactor communicates with an application.

Getting Started

You will be editing ColorPickerView, CircleColorPickerView, MyColorPickerView and MainActivity. As such it is important to understand the inheritance chains of these two files, as you will be using a lot of variables and functions defined in both these and parent classes.

Read the abstract base classes, including all of the comments in AbstractColorPicker.java and AbstractMainActivity.java before you begin. Where applicable, you must use the inherited variables and functions (do not overload the inherited functions).

The structure of the code is represented by the Unified Modeling Language (UML) diagram shown below. The symbols can be read as follows: + is a public field or method, # is protected, and - is private. Any method that is in italics is an abstract method, meaning is must be overridden in the child class. We are showing the absolute minimum necessary for MyColorPickerView.

classDiagram AppCompatActivity <|-- AbstractMainActivity AbstractMainActivity <|-- MainActivity AppCompatImageView <|-- AbstractColorPickerView AbstractColorPickerView <|-- ColorPickerView ColorPickerView <|-- CircleColorPickerView ColorPickerView <|-- MyColorPickerView class AbstractMainActivity{ #ColorPicker mColorPicker -mColorModel +colorToString() #setStartingColor()* } class MainActivity{ -mColorView -mLabelView #setStartingColor() +onSaveInstanceState() +onRestoreInstanceState() } class AbstractColorPickerView{ +DEFAULT_COLOR #mCurrentColor -mColorChangeListeners +setColor()* +addColorChangeListener() +removeColorChangeListener() #invokeColorChangeListeners() } class ColorPickerView { #mState #essentialGeometry +setColor() #updateModel() #essentialGeometry() +onTouchEvent() } class CircleColorPickerView{ #RADIUS_TO_THUMB_RATIO #mCenterX #mCenterY #mRadius #... #updateModel() #onDraw() +onLayout() #essentialGeometry() +getAngleFromColor() #getTouchAngle() +getAngleFromColor() } class MyColorPickerView{ -updateModel() +onDraw() #essentialGeometry() }

Related Readings: It will be helpful to read Android/Custom-Drawing and Android/UI-Events to understand parts of the assignment that seem tricky.

Part 1: Creating your Circle Color Picker

Implementing your color picker interactor will require you to support input handling, maintaining and mutating state, and drawing to the screen in CircleColorPickerView.java.

The Circle Color Picker

Important definition: The term wheel used throughout the spec refers to the dial and inner circle; it is the larger circle that contains all interface you will be drawing.

The RGB color picker works as follows: There is a small white “thumb” that marks the color currently indicated on the dial (the outer rim of the color picker), and that indicated color is displayed in the inner circle of the color wheel. The user interacts with this thumb by pressing down on it, then rotating it around the wheel. While the thumb is moving it is 50% opaque, and it will return to 100% opaque as soon as the user lifts their pointer from the screen and a new color is selected.

When the user has completed the selection of a new color using the RGB color picker interactor, the application will change the color displayed on the screen behind the wheel.

Drawing

Drawing is implemented in CircleColorPickerView#onDraw(Canvas). You will need to draw the thumb and the color in the center of the circle. We provide a color dial in the drawable folder and it is already being drawn by ColorPickerView#onDraw(Canvas) which is called because CircleColorPickerView inherits from ColorPickerView.

The height and width of the of the dial determined by the bounding box of the CircleColorPickerView. The the radius of the actual ColorPicker interactor is the half the smaller of the width or the height of that that bounding box.

Screenshot of color picker, original Screenshot of color picker, after moving to a new color

Important Variables

Related APIs: View#onLayout

Thumb

In the screenshots above there is a visible thumb (the white circle) that marks the selected color on the dial. The thumb is drawn in CircleColorPickerView#onDraw(Canvas). It must move around as a user interacts with the color picker.

The thumb must be constrained to move along a circular track that places it within the dial. It must move along that track even when the user is dragging their finger inside the inner circle.

Visually, the thumb’s radius is 0.085 times the outer-radius of the dial (center of circle to outside edge of color). This value is provided to you as a constant in ColorPickerView. Positioning the thumb is similar to #getTouchAngle(float, float) but instead of finding the angle based on the thumb location, you’re finding the thumb location based on the angle, additionally constraining the thumb to stay within the color band.

The PPS specification (found below) uses a float [0,1] to represent alpha, but Paint expects an int [0, 255]. Be sure to make the correct conversion (multiply by 255, then cast the result to int).

Center Circle

Inside the multi-color dial is a circle whose color is the same as the live selected color. It must be centered on the center of the wheel, and use up all available space up to the dial. The color of the inner circle, which represents the RGB Color Picker model, must update while you drag the thumb. In contrast, the colored box and text, which represent the application’s model (remember the Model View Controller (MVC)), must update only when the mouse is released.

Touch Input Events

stateDiagram-v2 [*] --> Inside: "Press insideWheel? A" Inside --> [*]: "Release B" Inside --> Inside: "Drag insideWheel? C else D"

Where

Note that the End state only exists to show the lifetime of a single interaction. Because the user can interact with the color picker any number of times, we would actually return to the Start state when the thumb is released. For some examples of single interactions, see the images below.

We’ll handle touch input by implementing ColorPickerView#onTouchEvent(MotionEvent). This is the event handler that will be called when a touch occurs in this view. Feedback is needed when the user is interacting with the color picker, so you will have to ensure that the view is redrawn. Recall that we want to use invalidate() to do this and even though invalidate() does not directly trigger redraws and may have no impact, you still do not want to call it more than needed. In other words, it is considered good code quality to only call invalidate() when necessary. In fact we will be taking off points for unnecessary invalidate() calls. Follow the PPS spec and don’t call aything it doesn’t specify. You should never need to call onDraw().

As you write the PPS, make sure to utilize proper coding style to ensure that the code is readable to someone not familiar with the project. For an example of how to translate PPS into code, see the PPS page.

Related APIs: View (see documentation on Drawing)

Diagrams of single interactions

Transitioning out of the Start State

As shown in the state diagram, when in the Start state (before interaction begins), we ignore any touches that are outside of the wheel. These events must be rejected by your PPS so that other interactors can use them if they want. Specifically, views that may lie underneath our CircleColorPickerView must be able to react to events outside the wheel, but within the square of the CircleColorPickerView. Only transition out of the start state when the user presses on or inside the wheel. When you transition out of the start state, color is updated, thumb transparency is changed (alpha becomes 0.5f), and thumb position is updated.

The starter code already has some built-in functionality to help you test whether or not you are correctly rejecting input. When you click outside the wheel, there will be a Toast (a pop up message) that says “You have clicked outside the wheel!”. If this message does not appear when you click outside the wheel, then you are not correctly rejecting input.

Transitions within the Inside State

Once interaction with the wheel begins, you must only update the CircleColorPickerView’s local model when the user is dragging their finger inside the wheel.

Use the x and y coordinates of the touch event to calculate the angle (in radians) of the touch on the wheel with #getTouchAngle(float, float). It is difficult to do this mapping in traditional RGB color space. The HSV color space discussed during class fits this task well. You can read more about the HSV color space here. Since we’re just adjusting color, we only want to modify hue while leaving saturation and value constant. You may see instruction in code comments under #getAngleFromColor(int), which we provide you. Use this implementation to guide your work on #getColorFromAngle(double), which does the opposite operation.

Notice that our color dial is rotated 90° from just the hue value converted to radians - our red color is at the top, but in the HSV model, the red color is to the right. This adjustment is applied in #getAngleFromColor(int). You will also have to apply this when implementing #getColorFromAngle(double). For information about why colors are being stored as int values, see the Misc. section below.

Here are some test values to help test your implementation of #getAngleFromColor(int):

Transition to the end state.

When the user finishes interacting with the wheel, you must update the UI to reflect the new selected color, by calling the onColorSelected method in the ColorChangeListener with our newly selected color. In addition, the thumb transparency must be reset to an alpha of 1f (fully opaque).

Essential Geometry

When a motion event occurs, we must change the coordinates into the form consumed by the state machine queries. The essentialGeometry method translates these coordinates into an ENUM which represents whether or not the motion event was inside of the dial. This is important because the state machine only relies on whether or not the motion event is within the color wheel and must not be interpreting “raw” coordinates.

Related APIs: MotionEvent / Color / ColorUtils / View#onTouchEvent / EssentialGeometry

Part 2: Implementing the application layer

Your application is to make use of your color picker. The application needs to be notified from the ColorPickerView when the color changes. In our case, it will use the information to display the newly chosen color in a rectangle at the bottom of the screen and update the application model, though other applications might do something different. Examples of other applications that use their own implementation of a color picker include Photoshop, MS Paint, etc.

In our application, the menu at the top right can be used to switch color picker types. We’ve shown a grey square for our custom MyColorPickerView – you may want to update this to an icon that reflects what you did.

Setting up the Application

The code you will write for the application is in MainActivity which inherits from AbstractMainActivity. An important variable stored in AbstractMainActivity is the ColorPickerView named mColorPicker.

The application layer must set the default color of mColorPicker using MainActivity#setStartingColor(int). We provide this default as ColorPickerView.DEFAULT_VALUE (it’s red). MainActivity#setStartingColor(int) must also ensure that the default color value is shown on the screen.

Managing Application State with Listeners

To find out about color changes, the application needs to register a callback by calling ColorPickerView.addColorListener(ColorChangeListener). This callback must update the application’s mColorView and mLabelView whenever onColorSelected is called to demonstrate that the application correctly retrieved a color from colorPickerView. This means you are prohibited from leveraging publicly accessible fields/functions on the color picker to observe the CircleColorPickerView state.

(Optional) As good practice, you should always unregister listeners when they are no longer relevant. This should be done in MainActivity.java#onDestroy() which is called when the application is killed.

You may notice that ColorPickerView.java keeps a List of ColorChangeListeners. This allows for our interactor to be more flexible because it can register many listeners that will all be notified when a new color is selected. For more on custom listeners, see CodePath’s guide to creating custom listeners. For more information about Fragments, see the Android Fragment API.

Part 3: Save and Restore Application Model using Bundle

You are to also save application model (i.e. the current color as known by the application) in the onSaveInstanceState bundle object. When user switches focus to some other app, Android kills our Activity. We will use the bundle to get the saved state back.

We want to manage the state at the application level (MainActivity.java) versus at the interactor level. Thus you will need to set the state of the color picker in the application layer when the bundle is loaded.

Notice from the documentation that onRestoreInstanceState is called after onCreate if a bundle exists. This is where you will access the information we saved in onSaveInstanceState to restore the current color with the color we had before our Activity was killed.

We will kill your application during our testing process to ensure the state is properly saved. To simulate our tests, you can use the adb to test killing it, or in your phone’s Developer options set Apps -> Don’t keep activity.

Clicking 'Don't Keep Activites' Android Developer Settings, 50%

If you do not already have developer options enabled follow the guide here.

Wheel default state and bundle interaction

The best way to test this functionality is to enable the setting referenced above, and then press home, then return to the app. The color that was selected when you killed the app should still be restored when the app is restarted. Quitting the app from multitasking (i.e. when the app is open, click on the square) will destroy the bundle. Steps to test this is working correctly are as follows:

Using the bundle:

  1. User opens app for first time. The wheel is invisible and color in the box is red (the default).
  2. User clicks on box to show wheel and changes color to blue.
  3. User leaves app (via home button) while wheel is still visible.
  4. User returns to app. The color in the box is blue and the wheel is invisible.
  5. User clicks in the color box and the wheel becomes visible with blue as the selected color.

No bundle exists when app is unloaded:

  1. User opens app for first time. The wheel is invisible and color in the box is red (the default).
  2. User clicks on box to show wheel and changes color to blue.
  3. User leaves app (via home button).
  4. User unloads the app completely from memory.
  5. User returns to app. The color in the box is red and the wheel is not visible. Clicking on the color box brings up the Color Picker with red as the selected color.

Note that you do not have to do anything to handle the logic for displaying the wheel. The wheel is invisible by default when the activity is created, and its visible/invisible state is NOT stored in the bundle.

Related APIs: Saving and Restoring State | Android Developer Options | Explanations for how to use Bundle

Part 4: Implementing your own color picker

Once you have implemented the CircleColorPickerView and gotton ColorPickerView working properly, you can also implement a color picker of your choice. In the solution video, we show a simple two-color color picker. This works without any further modifications to ColorPicker and without over riding ColorPickerView#onTouch() or ColorPickerView#setColor(). You will need to implement MyColorPickerView#essentialGeometry() and MyColorPickerView#updateModel() and MyColorPickerView#onDraw() at a minimum for this to work.

You can make your color picker look any way you want as long as it has a minimum of two colors to choose from.

Part 5: Reflection

For this part, you will submit your reflection on this assignment to Gradescope. Create a MS Word, Google or other type of document and copy the following questions (in italics below) into that document. Add your responses below each question. You can have more than one answer per page, but if you can, please try to avoid page breaks in the middle of a question. Insert page breaks between questions as needed.

Debugging tips and tricks

Logging output is especially useful for testing the functionality of sections of code such as getAngleFromColor and other methods. Much like System.out.print in Java, Android provides its own class for producing output: Log. We suggest that you use Log.i and create your own custom tag so that you can filter the output for the information you want. Below is an example of how to use the Log.i function.

private static final String TAG = "ColorPicker MainActivity";

Log.i(TAG, "Hello world!");

To make full use of Logcat, make sure to configure the priority level (in this case, the “i” in Log.i stands for “Info”) and use the correct tag (in this case, “ColorPicker MainActivity”). It’s also good to check that you have the correct device/emulator selected.

Note: Remember to take your Log.i debugging calls out of your code before turning it in.

Logcat diagram

Related APIs: Android Log.* | Using Logcat

Misc.

This assignment does require doing some math, and you are welcome to use the Java Math functions. Hint: Remember that the y direction is positive pointing down the canvas, not pointing up like a traditional cartesian coordinate system. This may impact the values returned from trigonometric functions.

Related APIs: Java Math.*

Integer representations of RGB colors

Colors on computer screens are often thought of as R, G, B, (and alpha) values ranging from 0 to 255, using hexidecimal representations (#00 to #FF) of those numbers for each value. For example the hexidecimal representation of the color #FF0410 would be

FF <- red value = 255 in decimal
04 <- green value = 4 in decimal
10 <- blue value = 16 in decimal

But the real int representation of this RGB value is

255 * 256 ^ 2 + 04 * 256 + 16 * 1 = 16712720.

Turn-in

Submission Instructions

Part 1-4:

Remember to continually commit your changes to Gitlab (git add/git commit/git push), and then turn in your code using the GitGrade link at the top of this page.

Note: we will ONLY be using your code in the following files, do not modify any other files beyond this list:

 - MainActivity.java
 - ColorPickerView.java
 - CircleColorPickerView.java
 - MyColorPickerView.java

Part 5:

You are to turn in Part 5 to Gradescope.

Grading (40pts)

Code (31 pts)

This code portion of this homework will be out of 31 points and will roughly (subject to small adjustments) be distributed as:

Reflection (9pts)

For this part, you will submit your reflection on this assignment to Gradescope. Create a MS Word, Google or other type of document and copy the following questions (in italics below) into that document. Add your responses below each question. You can have more than one answer per page, but if you can, please try to avoid page breaks in the middle of a question. Insert page breaks between questions as needed.

Each of the 3 reflection questions is worth 3 points. Remember that the grading for these reflection questions is based on the following rubric:

IDE Errors/Warnings you can ignore

NOTE: An error/warning that can be ignored for this assignment cannot be ignored for every assignment. Check IDE notices against specs on per assignment basis.