Mobile App Dev 2022W: Tutorial 9: Difference between revisions
(13 intermediate revisions by the same user not shown) | |||
Line 4: | Line 4: | ||
You'll need to do the following to get PicViewer2 running: | You'll need to do the following to get PicViewer2 running: | ||
# Create a new empty activity project compatible with Android 5.0, don't check the legacy library support box. | |||
# Add [https://homeostasis.scs.carleton.ca/~soma/mad-2022w/images/ the kittens and roshi images] to the drawable resources. Just copy these files into app/src/main/res/drawable in your project, they should then show up under res->drawable in Android Studio. | |||
# Replace [https://homeostasis.scs.carleton.ca/~soma/mad-2022w/code/PicViewer2/MainActivity.kt MainActivity.kt] and [https://homeostasis.scs.carleton.ca/~soma/mad-2022w/code/PicViewer2/activity_main.xml activity_main.xml] with the versions below. This should enable the main functionality of the app. | |||
# Get rid of the titlebar by editing [https://homeostasis.scs.carleton.ca/~soma/mad-2022w/code/PicViewer2/themes.xml themes.xml]. | |||
# Add [https://homeostasis.scs.carleton.ca/~soma/mad-2022w/code/PicViewer2/SplashScreen.kt SplashScreen.kt] and [https://homeostasis.scs.carleton.ca/~soma/mad-2022w/code/PicViewer2/splashscreen.xml splashscreen.xml]. This will add the splashscreen but won't run it. | |||
# Change the intent filters in [https://homeostasis.scs.carleton.ca/~soma/mad-2022w/code/PicViewer2/AndroidManifest.xml AndroidManifest.xml] so as to enable the starting of the splash screen. | |||
==Questions== | ==Questions== | ||
Line 15: | Line 18: | ||
# Where are objects passed as arguments? Where are functions passed as arguments? Where is the functionality of existing objects changed? | # Where are objects passed as arguments? Where are functions passed as arguments? Where is the functionality of existing objects changed? | ||
# How is state being managed? How would you make a similar app in SwiftUI? Would it be simpler or more complex? Why? | # How is state being managed? How would you make a similar app in SwiftUI? Would it be simpler or more complex? Why? | ||
# How is the splash screen called before the main activity? | |||
==Tasks== | ==Tasks== | ||
Line 22: | Line 26: | ||
# Add a widget that shows the name of the current image just above the rotation text entry area and that allows a new image to be selected. This field can be a text entry field or a pop-up menu. | # Add a widget that shows the name of the current image just above the rotation text entry area and that allows a new image to be selected. This field can be a text entry field or a pop-up menu. | ||
# Change the rotation and scaling parameters so they are automatically set to their default values if the image is not at its default (0,0) position (whether because the image was dragged or new coordinates were entered). | # Change the rotation and scaling parameters so they are automatically set to their default values if the image is not at its default (0,0) position (whether because the image was dragged or new coordinates were entered). | ||
# Make it impossible to drag the image off the screen | # Make it impossible to drag more than half the image off the screen. If X and Y coordinates are specified that would place more than half of the image (in the X or Y coordinates) off screen, they should be changed to the closet value that will keep the image on the screen. | ||
# Change the image click event to instead load the given image in a browser. (For your added images, you'll need to make sure they are accessible somewhere on the web.) Here's how you can load the course wiki using an intent (make sure it is in MainActivity): | |||
<pre> | |||
fun openPage() { | |||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://homeostasis.scs.carleton.ca/wiki")) | |||
startActivity(intent) | |||
} | |||
</pre> | |||
==Code== | ==Code== | ||
Line 188: | Line 199: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
===SplashScreen.kt=== | ===[https://homeostasis.scs.carleton.ca/~soma/mad-2022w/code/PicViewer2/SplashScreen.kt SplashScreen.kt]=== | ||
<syntaxhighlight lang="kotlin" line> | <syntaxhighlight lang="kotlin" line> | ||
Line 318: | Line 329: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
===AndroidManifest.xml=== | ===[https://homeostasis.scs.carleton.ca/~soma/mad-2022w/code/PicViewer2/splashscreen.xml splashscreen.xml]=== | ||
<syntaxhighlight lang="xml" line> | |||
<?xml version="1.0" encoding="utf-8"?> | |||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" | |||
xmlns:app="http://schemas.android.com/apk/res-auto" | |||
xmlns:tools="http://schemas.android.com/tools" | |||
android:layout_width="match_parent" | |||
android:layout_height="match_parent" | |||
android:onClick="startMain" | |||
tools:context=".SplashScreen"> | |||
<TextView | |||
android:id="@+id/title" | |||
android:layout_width="wrap_content" | |||
android:layout_height="wrap_content" | |||
android:text="Picture Viewer Demo" | |||
android:textAlignment="center" | |||
android:textAppearance="@style/TextAppearance.AppCompat.Display3" | |||
android:textStyle="bold" | |||
app:layout_constraintStart_toStartOf="parent" | |||
app:layout_constraintTop_toTopOf="parent" /> | |||
<TextView | |||
android:id="@+id/textView2" | |||
android:layout_width="wrap_content" | |||
android:layout_height="wrap_content" | |||
android:text="COMP 1601" | |||
android:textAppearance="@style/TextAppearance.AppCompat.Display2" | |||
app:layout_constraintBottom_toTopOf="@+id/continueButton" | |||
app:layout_constraintTop_toBottomOf="@+id/title" | |||
app:layout_constraintEnd_toEndOf="parent" | |||
app:layout_constraintStart_toStartOf="parent" /> | |||
<Button | |||
android:id="@+id/continueButton" | |||
android:layout_width="wrap_content" | |||
android:layout_height="wrap_content" | |||
android:layout_marginBottom="16dp" | |||
android:onClick="startMain" | |||
android:text="Continue" | |||
app:layout_constraintBottom_toBottomOf="parent" | |||
app:layout_constraintEnd_toEndOf="parent" | |||
app:layout_constraintStart_toStartOf="parent" /> | |||
</androidx.constraintlayout.widget.ConstraintLayout> | |||
</syntaxhighlight> | |||
===[https://homeostasis.scs.carleton.ca/~soma/mad-2022w/code/PicViewer2/AndroidManifest.xml AndroidManifest.xml]=== | |||
<syntaxhighlight lang="xml" line> | <syntaxhighlight lang="xml" line> | ||
Line 345: | Line 403: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
===themes.xml=== | ===[https://homeostasis.scs.carleton.ca/~soma/mad-2022w/code/PicViewer2/themes.xml themes.xml]=== | ||
<syntaxhighlight lang="xml" line> | <syntaxhighlight lang="xml" line> |
Latest revision as of 16:54, 25 March 2022
In this tutorial you'll be working with PicViewer2, and Android app with some similarity to RemotePicViewer from Tutorial 4. The key differences are 1) the pictures are local, not remote, and 2) it displays the image scale, rotation, and position as editable fields, and 3) it only supports image dragging and clicking, not any other gestures.
Getting Started
You'll need to do the following to get PicViewer2 running:
- Create a new empty activity project compatible with Android 5.0, don't check the legacy library support box.
- Add the kittens and roshi images to the drawable resources. Just copy these files into app/src/main/res/drawable in your project, they should then show up under res->drawable in Android Studio.
- Replace MainActivity.kt and activity_main.xml with the versions below. This should enable the main functionality of the app.
- Get rid of the titlebar by editing themes.xml.
- Add SplashScreen.kt and splashscreen.xml. This will add the splashscreen but won't run it.
- Change the intent filters in AndroidManifest.xml so as to enable the starting of the splash screen.
Questions
- How is the layout of the page specified? Specifically, how are the labels aligned with the text entry boxes?
- How does dragging interact with manually entering positions, scale, and rotations? In particular, test dragging a picture that has been scaled below 1. Does it move as you expect?
- What do picParamWatcher and dragView classes do? Would it make sense to combine them into one class? Why or why not?
- Where are objects passed as arguments? Where are functions passed as arguments? Where is the functionality of existing objects changed?
- How is state being managed? How would you make a similar app in SwiftUI? Would it be simpler or more complex? Why?
- How is the splash screen called before the main activity?
Tasks
- Add at least one image to the app. Make sure the old and new images are displayed with clicking on the current image.
- Move the scale, rotation, and position text entry widgets and their labels to the left or the center of the screen (rather than being aligned on the right). Make sure your layout maintains the same rough positioning when the screen is rotated or a different sized screen is used.
- Add a widget that shows the name of the current image just above the rotation text entry area and that allows a new image to be selected. This field can be a text entry field or a pop-up menu.
- Change the rotation and scaling parameters so they are automatically set to their default values if the image is not at its default (0,0) position (whether because the image was dragged or new coordinates were entered).
- Make it impossible to drag more than half the image off the screen. If X and Y coordinates are specified that would place more than half of the image (in the X or Y coordinates) off screen, they should be changed to the closet value that will keep the image on the screen.
- Change the image click event to instead load the given image in a browser. (For your added images, you'll need to make sure they are accessible somewhere on the web.) Here's how you can load the course wiki using an intent (make sure it is in MainActivity):
fun openPage() { val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://homeostasis.scs.carleton.ca/wiki")) startActivity(intent) }
Code
MainActivity.kt
package carleton.comp1601.picviewer2
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.MotionEvent
import android.view.View
import android.widget.EditText
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
private lateinit var p: ImageView
private var pic = R.drawable.kittens
private lateinit var X: PicParamWatcher
private lateinit var Y: PicParamWatcher
private lateinit var scale: PicParamWatcher
private lateinit var r: PicParamWatcher
private lateinit var dragV: dragView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState != null) {
// do something
}
p = findViewById(R.id.mypic)
dragV = dragView(p, ::update, ::toggleImage)
X = PicParamWatcher(0F, findViewById(R.id.x), ::update)
Y = PicParamWatcher(0F, findViewById(R.id.y), ::update)
scale = PicParamWatcher(1F, findViewById(R.id.scale), ::update)
r = PicParamWatcher(0F, findViewById(R.id.rotation), ::update)
update()
}
fun update() {
p.setImageResource(pic)
if (dragV.picDragged) {
dragV.picDragged = false
X.value = p.x
X.refresh()
Y.value = p.y
Y.refresh()
} else {
p.setX(X.value)
p.setY(Y.value)
}
p.setRotation(r.value)
p.setScaleX(scale.value)
p.setScaleY(scale.value)
}
fun toggleImage() {
if (pic == R.drawable.roshi) {
pic = R.drawable.kittens
} else {
pic = R.drawable.roshi
}
update()
}
}
class dragView: View.OnTouchListener {
var dv: View
var update: () -> Unit
var picDragged = false
var lastWasDown = false
var clickHandler: () -> Unit
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
if (event == null || v == null) {
return false
}
val action = event.getAction()
val x = event.rawX
val y = event.rawY
var screenLoc = IntArray(2)
dv.getLocationOnScreen(screenLoc)
val Xoffset = screenLoc[0] - dv.x
val Yoffset = screenLoc[1] - dv.y
if (action == MotionEvent.ACTION_MOVE) {
lastWasDown = false
dv.x = x - (dv.width/2 + Xoffset)
dv.y = y - (dv.height/2 + Yoffset)
picDragged = true
update()
} else if (action == MotionEvent.ACTION_DOWN) {
lastWasDown = true
} else if (action == MotionEvent.ACTION_UP) {
if (lastWasDown) {
lastWasDown = false
clickHandler()
}
}
return true
}
constructor(v: View, u: () -> Unit, ch: () -> Unit) {
dv = v
dv.setOnTouchListener(this)
update = u
clickHandler = ch
}
}
class PicParamWatcher: TextWatcher {
var update: () -> Unit
var widget: EditText
var defaultValue: Float
var value: Float
var valueChanged = false
override fun afterTextChanged(s: Editable) {
if (valueChanged) {
valueChanged = false
return
}
val new_value = s.toString().toFloatOrNull()
if (new_value != null) {
value = new_value
} else {
value = defaultValue
}
update()
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
fun refresh() {
valueChanged = true
widget.setText(value.toString())
}
constructor(x: Float, w: EditText, u: () -> Unit) {
update = u
widget = w
defaultValue = x
value = x
w.addTextChangedListener(this)
refresh()
}
}
SplashScreen.kt
package carleton.comp1601.picviewer2
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
class SplashScreen : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.splashscreen)
}
fun startMain(v: View) {
intent = Intent(this, MainActivity::class.java)
startActivity(intent)
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<ImageView
android:id="@+id/mypic"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="0dp"
android:layout_marginTop="0dp"
android:layout_marginEnd="0dp"
android:layout_marginBottom="20dp"
app:layout_constraintBottom_toTopOf="@+id/rotation"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<EditText
android:id="@+id/rotation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ems="5"
android:inputType="number"
app:layout_constraintBottom_toTopOf="@+id/scale"
app:layout_constraintStart_toStartOf="@+id/scale" />
<EditText
android:id="@+id/scale"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ems="5"
android:inputType="numberDecimal"
app:layout_constraintBottom_toTopOf="@+id/x"
app:layout_constraintStart_toStartOf="@+id/x" />
<EditText
android:id="@+id/x"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ems="5"
android:inputType="numberSigned"
app:layout_constraintBottom_toTopOf="@+id/y"
app:layout_constraintStart_toStartOf="@+id/y" />
<EditText
android:id="@+id/y"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:ems="5"
android:inputType="numberSigned"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/ylabel" />
<TextView
android:id="@+id/rotationlabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:text="rotation"
android:textStyle="bold"
app:layout_constraintBaseline_toBaselineOf="@+id/rotation"
app:layout_constraintEnd_toStartOf="@+id/rotation"
/>
<TextView
android:id="@+id/scalelabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:layout_marginBottom="11dp"
android:text="scale"
android:textStyle="bold"
app:layout_constraintBaseline_toBaselineOf="@+id/scale"
app:layout_constraintEnd_toStartOf="@+id/scale" />
<TextView
android:id="@+id/xlabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:text="X"
android:textStyle="bold"
app:layout_constraintBaseline_toBaselineOf="@+id/x"
app:layout_constraintEnd_toStartOf="@+id/x" />
<TextView
android:id="@+id/ylabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:text="Y"
android:textStyle="bold"
app:layout_constraintBaseline_toBaselineOf="@+id/y"
app:layout_constraintEnd_toStartOf="@+id/y" />
</androidx.constraintlayout.widget.ConstraintLayout>
splashscreen.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:onClick="startMain"
tools:context=".SplashScreen">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Picture Viewer Demo"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.AppCompat.Display3"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="COMP 1601"
android:textAppearance="@style/TextAppearance.AppCompat.Display2"
app:layout_constraintBottom_toTopOf="@+id/continueButton"
app:layout_constraintTop_toBottomOf="@+id/title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/continueButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:onClick="startMain"
android:text="Continue"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="carleton.comp1601.picviewer2">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.PicViewer2">
<activity android:name=".SplashScreen" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".MainActivity">
</activity>
</application>
</manifest>
themes.xml
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.PicViewer2" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
</resources>