Difference between revisions of "Mobile App Dev 2022W: Tutorial 9"

From Soma-notes
Jump to navigation Jump to search
Line 23: Line 23:
# 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.
# 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.
# 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.
# Make it impossible to drag the image off the screen (partially or completely).  If X and Y coordinates are specified that would place the image off screen, they should be changed to the closet value that will keep the image on the screen.  No need to account for rotation and scaling.


==Code==
==Code==

Revision as of 14:55, 22 March 2022

This tutorial is still being developed. The code is final, the questions/tasks are being edited.

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.

Questions

  1. How is the layout of the page specified? Specifically, how are the labels aligned with the text entry boxes?
  2. 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?
  3. What do picParamWatcher and dragView classes do? Would it make sense to combine them into one class? Why or why not?
  4. Where are objects passed as arguments? Where are functions passed as arguments? Where is the functionality of existing objects changed?
  5. How is state being managed? How would you make a similar app in SwiftUI? Would it be simpler or more complex? Why?

Tasks

  1. Add at least one image to the app. Make sure the old and new images are displayed with clicking on the current image.
  2. 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.
  3. 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.
  4. Make it impossible to drag the image off the screen (partially or completely). If X and Y coordinates are specified that would place the image off screen, they should be changed to the closet value that will keep the image on the screen. No need to account for rotation and scaling.

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>