COMP 1601 2021W Assignment 4 Solutions
1. [2] Update PicViewer2 so all the fields use a PicWatcher with clamp
functions as specified in Tutorial 8. Include the full code of
MainActivity.kt as your answer to this question.
This is just the code at the end of Tutorial 8. A copy of this was
made available as part of the Tutorial 8 solutions.
package carleton.comp1601.picviewer2a
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.util.Log
import android.view.View
import android.widget.EditText
import android.widget.ImageView
val appName = "PicViewer2A"
val pics = arrayOf(R.drawable.kittens, R.drawable.roshi, R.drawable.roshi2)
class MainActivity : AppCompatActivity() {
private lateinit var p: ImageView
private var pic = R.drawable.kittens
private var picIndex = 0
private lateinit var rotationInput: EditText
private lateinit var scaleInput: EditText
private lateinit var pXInput: EditText
private lateinit var pYInput: EditText
val r = PicWatcher("rotation", 0F, ::updateImage)
{r ->
if (r > 720F) return@PicWatcher 720F
if (r < 0F) return@PicWatcher 0F
r
}
val s = PicWatcher("scale", 1F, ::updateImage)
{s ->
if (s > 5F) return@PicWatcher 5F
if (s < 0) return@PicWatcher 0F
s
}
val X = PicWatcher("X", 0F, ::updateImage, ::clamp_xy)
val Y = PicWatcher("Y", 0F, ::updateImage, ::clamp_xy)
fun clamp_xy(v: Float): Float {
if (v > 1000F) return 1000F
if (v < -1000F) return -1000F
return v
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState != null) {
// do something
}
p = findViewById(R.id.mypic)
rotationInput = findViewById(R.id.rotation)
rotationInput.addTextChangedListener(r)
scaleInput = findViewById(R.id.scale)
scaleInput.addTextChangedListener(s)
pXInput = findViewById(R.id.x)
pXInput.addTextChangedListener(X)
pYInput = findViewById(R.id.y)
pYInput.addTextChangedListener(Y)
updateImage()
Log.d(appName, "Activity Created")
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
Log.d(appName, "State saved")
}
fun updateImage() {
p.setImageResource(pic)
p.setX(X.value)
p.setY(Y.value)
p.setRotation(r.value)
p.setScaleX(s.value)
p.setScaleY(s.value)
}
fun nextImage(v: View) {
picIndex = (picIndex + 1) % pics.size
pic = pics[picIndex]
updateImage()
}
}
class PicWatcher: TextWatcher {
var value: Float
var default_value: Float
var name: String
var update: () -> Unit
var clamp: (Float) -> Float
override fun afterTextChanged(s: Editable) {
val new_value = s.toString().toFloatOrNull()
if (new_value != null) {
value = clamp(new_value)
Log.d(appName, "Set ${name} to ${value}.")
} else {
value = default_value
Log.d(appName, "Set ${name} to default value ${value}.")
}
update()
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
constructor(n: String, v: Float, u: () -> Unit, c: (Float) -> Float) {
value = v
default_value = v
name = n
update = u
clamp = c
}
}
2. [2] Remove the title bar from the screens. Make sure your solution
works for both light and dark modes.
Insert the following just after line 14 of both values/themes.xml and
night/themes.xml:
falsetrue
3. [4] Add a title screen with a continue button. The title should be
"Picture Viewer Demo". When the title screen's continue button
is pressed the main activity should be shown.
To do this we have to create a new activity, which means we need to
specify the activity in AndroidManifest (saying it should be run first
rather than the main screen), create a TitleScreen.kt, and create an
associated title_screen.xml. We can basically copy the code from
Tutorial 9 for all this.
First, we change the activity portion AndroidManifest (starting at
line 12) to the following:
We've added the TitleScreen activity and given it the intent filter
that MainActivity previously had, so TitleScreen will be called when
the app is started. Next, we need to create TitleScreen.kt, a new
Kotlin file (which is an almost identical copy of SplashScreen.kt):
package carleton.comp1601.picviewer2a
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.appcompat.app.AppCompatActivity
class TitleScreen : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.title_screen)
Log.d(appName, "Title screen created")
}
fun startMain(v: View) {
Log.d(appName, "Starting Main button pressed")
intent = Intent(this, MainActivity::class.java)
startActivity(intent)
Log.d(appName, "Starting Main intent sent")
}
}
And finally, create title_screen.xml that was referenced above. This
is identical to splashscreen.xml, except the reference to SplashScreen
has been replaced by TitleScreen, the title was changed, and the COMP
1601 text was removed. (It is fine if you kept it in.)
4. [4] Add the name of the picture as a title above the image,
centered. It should be in a Display3 bold font. You'll need to
assign each picture a name.
First, add the TextView to activity_main.xml. It should go after the
ImageView so it will be on top of the picture always.
Modify the mypic ImageView's constraints so it makes room for the
title. Specifically, change
app:layout_constraintTop_toTopOf="parent"
To
app:layout_constraintTop_toBottomOf="@id/pictitle"
In MainActivity, first add in names:
val picNames = arrayOf("Kittens", "Roshi 1", "Roshi 2")
Initialize picTitle:
private lateinit var picTitle: TextView
...
picTitle = findViewById(R.id.pictitle)
And then use picIndex to update the title in updateImage():
picTitle.setText(picNames[picIndex])
(This assumes you defined picIndex as done in Tutorial 8's solutions.
If you solved it a different way your answer may differ.)
5. [2] When you click on the image title, have it open up a DuckDuckGo
image search for that title in the system web browser. We can
do this with a specially crafted URL. For example, if the image
title is kittens, we can initiate the search by visiting the
page https://duckduckgo.com/?q=kittens&iax=images&ia=images
We can borrow the openPage() function from IntentDemo. First, import
Uri and Intent:
import android.content.Intent
import android.net.Uri
Then we'll modify openPage() as follows:
fun searchForTitle(v: View) {
val t = picNames[picIndex]
val u = "https://duckduckgo.com/?q=${t}&iax=images&ia=images"
Log.d(appName, "Searching for ${t}")
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(u))
startActivity(intent)
Log.d(appName, "URL intent sent")
}
And then we'll add an onClick handler for the picTitle TextView in
activity_main.xml:
android:onClick="searchForTitle"
6. [4] Add a touch event handler that allows the picture to be dragged
to different positions on the screen. As the image is moved the
X and Y fields should be updated.
8. [BONUS 2] Modify your touch event handler so it cycles pictures on clicks
as before and updates the position on drag.
(combined answer)
We just need to add a touch listener to the picture in onCreate of
MainActivity:
p.setOnTouchListener(trackPic)
We copy the OnTouchListener object from Tutorial 9 and place it inside
MainActivity, calling it trackPic. We need to make some modifications
though. We add a wasDown boolean property and a method
updatePosition().
updatePosition() sets the x and y position of the picture and the
textfields pXInput and pYinput to the passed in x and y parameters.
The onTouch() method then does the following for the key touch events:
* for ACTION_DOWN, sets wasDown to true
* for ACTION_MOVE, sets wasDown to false and calls updatePosition
with new coordinates
* for ACTION_UP, checks wasDown and if it is true does what we did
previously for onClick, namely call nextImage(). This version also
moves the image to its default position (0,0). It then sets
wasDown to false.
Below is the code for trackPic:
val trackPic = object: View.OnTouchListener {
var wasDown = false
fun updatePosition(x: Float, y: Float) {
p.x = x
p.y = y
pXInput.setText(x.toString())
pYInput.setText(y.toString())
}
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
if (event == null || v == null) {
return false
}
val action = event.getAction()
val x = event.rawX -600F
val y = event.rawY -600F
if (action == MotionEvent.ACTION_DOWN) {
Log.d(appName, "Picture touched at ${x}, ${y}")
wasDown = true
} else if (action == MotionEvent.ACTION_UP) {
if (wasDown) {
nextImage(v)
updatePosition(0F, 0F)
}
wasDown = false
} else if (action == MotionEvent.ACTION_MOVE) {
updatePosition(x, y)
wasDown = false
Log.d(appName, "Picture repositioned to ${x}, ${y}")
}
return true;
}
}
7. [2] When the device is rotated, is the position of the image
preserved after it is dragged? Why or why not?
The position (as well as other picture state variables) are preserved
after rotation because the EditText objects preserve their state. If
you disable updates to the EditText objects (so no editable values are
shown on screen for x, y, rotation, and scale) then rotation resets
the picture to its default state. The EditText objects save and
restore their own state and then update MainActivity's state through
calls to afterTextChanged that happen right when they are restored.