as4: Color Picker

Last revised: 11am Thursday, February 13th, 2020
Assigned:
  • February 8th, 2020
Due:
  • Due February 18th, 2020, 10:00pm
  • Lock February 20th, 2020, 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

GitGrade links

Classroom Summary

ColorPicker: Accept the Assignment / Turn-in the Assignment

Goal

There are two parts to this assignment, creating a an RGB color picker interactor which lets you choose a color on a rainbow circle (color wheel), then using it in an application.

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.

A video of how this interactor works in the application can be found here.

You will play two developer roles in this assignment

Component Developer Role

Your primary goal in this assignment is to create ColorPickerView.java. This is your custom interactor and it must be implemented so it can be used by any application. ColorPickerView.java inherits from AbstractColorPickerView.java which must remain untouched.

Tasks for ColorPickerView

Application Developer 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

You will be turning in ColorPickerView.java and MainActivity.java so make sure that any variables/fields you create/modify are in these files. Do NOT modify any other files in this project.

Note: We will be asking you to re-use your color picker (AbstractColorPickerView 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.java and MainActivity.java. 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 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).

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 interactor

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

Drawing

Drawing is implemented in ColorPickerView#onDraw. 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 AbstractColorPickerView#onDraw which is called because ColorPickerView inherits from AbstractColorPickerView.

The height and width of the of the dial determined by the bounding box of the ColorPickerView.

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

Important Variables

Some parts of your drawing code will require you to know the size of the view you are drawing in. The protected variables mRadius, mCenterX, and mCenterY in AbstractColorPickerView.java should be calculated in your onLayout method.

Related APIs: View#onLayout

Thumb

In the screenshots above there is a visible thumb that marks the selected color on the dial. The thumb is drawn in ColorPickerView#onDraw. 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 AbstractColorPickerView. Positioning the thumb is similar to AbstractColorPickerView#getTouchAngle 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 stub 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 should be a circle that’s 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, must update only when the mouse is released.

Touch Input Events

graph LR S((.)) --> A((Start)) A -- "Press:insideWheel? A" --> I((Inside)) I -- "Release:B" --> E[End] I -- "Drag:insideWheel? C" --> I I -- "Drag:outsideWheel? D" --> I classDef finish outline-style:double,fill:#d1e0e0,stroke:#333,stroke-width:2px; classDef normal fill:#e6f3ff,stroke:#333,stroke-width:2px; classDef start fill:#d1e0e0,stroke:#333,stroke-width:4px; classDef invisible fill:#FFFFFF,stroke:#FFFFFF,color:#FFFFFF class S invisible class A start class E finish class I normal

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. 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 don’t 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 should be rejected by your ColorPickerView so that other interactors can use them if they want. Specifically, views that may lie underneath our ColorPickerView must be able to react to events outside the wheel, but within the square of the ColorPickerView. 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 should be a message that pops up saying “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, we should update the ColorPickerView’s local model only 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 AbstractColorPickerView#getTouchAngle. 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 detailed instruction in code comments under AbstractColorPickerView#getColorFromAngle, which we provide you. Use this implementation to guide your work on ColorPickerView#getAngleFromColor, which does the opposite operation.

Notice that our color dial is rotated 90deg 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 AbstractColorPickerView#getColorFromAngle. You will also have to apply this fact when implementing ColorPickerView#getAngleFromColor.

Here are some test values to help test your implementation of ColorPickerView#getAngleFromColor:

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).

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

Part 2: Implementing the application layer

Your application it 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.

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 colorPicker.

The application layer should set the default color of colorPicker using MainActivity#setStartingColor. We provide this default as AbstractColorPickerView.DEFAULT_VALUE (it’s red). MainActivity#setStartingColor must also trigger onColorSelected to 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 AbstractColorPickerView.addColorListener. This callback should update the application’s colorView and colorTextView 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 ColorPickerView state.

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

You may notice that AbstractColorPickerView.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. 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 the during our testing of your application 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.

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.

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

Part 4: Reflection

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, Andriod 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, “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, you are welcome to use the Java Math functions. Related APIs: Java Math.*

Turn-in

Submission Instructions

Part 1-3

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:

 - ColorPickerView.java
 - MainActivity.java

Part 4:

You are to turn in Part 4 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)

Each question will be graded on a 3 point scale with a 4th extra credit point if the answer is above and beyond what is expected.

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.