Mobile App Dev 2022W: Tutorial 9: Difference between revisions
| No edit summary | |||
| Line 1: | Line 1: | ||
| In this tutorial you'll be working with [https://homeostasis.scs.carleton.ca/~soma/mad-2022w/code/PicViewer2/ PicViewer2], and Android app with some similarity to RemotePicViewer from [[Mobile App Dev 2022W: Tutorial 4|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. | |||
| * 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. | |||
| * 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. | |||
| ==Code== | ==Code== | ||
Revision as of 18:02, 22 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.
- Replace MainActivity.kt and activity_main.xml with the versions below.
- 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.
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()
    }
}
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>