as4: Color Picker
Last revised: 11am Thursday, February 13th, 2020- February 8th, 2020
- 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
- Goal
- Getting Started
- Part 1: Creating your interactor
- Part 2: Implementing the application layer
- Part 3: Save and Restore Application Model using Bundle
- Part 4: Reflection
- Debugging tips and tricks
- Misc.
- Turn-in
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
- You will be a Component Developer as you implement the RBG Color Picker interactor.
- You will be an Application Developer as you use this new interactor in your App.
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
- Handle different input events in
onTouchEvent
- Initialize properties during
onLayout
(register the first measure before initializing all views) - Draw the rainbow colored wheel and thumb in
onDraw
- Save/restore view state locally
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
- Register callbacks
- Save application state in bundle
- Restore activity state from bundle
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
.
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
where
- A is updateModel();invalidate()
- B is invokeColorChangeListeners();invalidate()
- C is updateModel();invalidate()
- D is doNothing()
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)
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.
- When a finger drags on screen inside the wheel, the thumb will follow the angle the finger is at, and the color of the center circle will update to reflect the change in the local model for the Color Picker
- When a finger drags on screen outside the wheel, the thumb will stay at the most recent angle within the wheel. If the finger re-enters the wheel at a different angle, the thumb should jump to that angle and the color within the wheel should display the corresponding color.
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
:
- angle: 2.5769272, color: -16774401 (blue)
- angle: -1.5461564, color: -64000 (red)
- angle: 0.42093232, color: -15073536 (green)
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
-
With what you know about accessibility in Android, could you make color picker accessible? Why or why not?
-
How might you change
ColorPickerView
if you were a Library Developer and you decided to share this new component with others? -
We were intentional in giving you the Color Picker state machine as a way to model the interaction between the user and the interactor. How did you use this state machine to write the code in
ColorPickerView
? Did this visualization help to make the spec clearer than if it hadn’t been there?
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.
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:
- Code quality (including but not limited to the code compiles and runs) (4 pts)
- Event Handling (onTouchEvent, etc) : (9 pts)
- Correctly determines essential geometry
- Implement PPS properly
- Implements circular interaction properly
- Feedback (onDraw) (6pts)
- Responds to callback in Application (3 pts)
- Layout Management (2 pts)
- View Model Management (2 pts)
- Correctly calls invalidate when setColor is called (1pt)
- Correctly update model in the view whenever internal state changes (1 pt)
- Application model management (2 pts)
- Application/View Resilience (3 pts)
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.
- 0 Missing
- 1 Not complete
- 2 Minimal effort
- 3 Solid answer, gets at the heart of the question.
- 4 Exceptionally Insightful answer (extra credit)
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.
-
onTouchEvent
- Custom view ColorPickerViewSolution overrides onTouchEvent but not performClick
- Anonymous Class Replaced with Lambda
- “anonymous new
AbstractColorPickerView.ColorListener()
can be replaced with lambda”
- “anonymous new