Deprecated: Creation of dynamic property wpdb::$categories is deprecated in /var/www/twodee/blog/wp-includes/wp-db.php on line 668

Deprecated: Creation of dynamic property wpdb::$post2cat is deprecated in /var/www/twodee/blog/wp-includes/wp-db.php on line 668

Deprecated: Creation of dynamic property wpdb::$link2cat is deprecated in /var/www/twodee/blog/wp-includes/wp-db.php on line 668

Deprecated: Return type of WP_Theme::offsetExists($offset) should either be compatible with ArrayAccess::offsetExists(mixed $offset): bool, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-theme.php on line 554

Deprecated: Return type of WP_Theme::offsetGet($offset) should either be compatible with ArrayAccess::offsetGet(mixed $offset): mixed, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-theme.php on line 595

Deprecated: Return type of WP_Theme::offsetSet($offset, $value) should either be compatible with ArrayAccess::offsetSet(mixed $offset, mixed $value): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-theme.php on line 535

Deprecated: Return type of WP_Theme::offsetUnset($offset) should either be compatible with ArrayAccess::offsetUnset(mixed $offset): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-theme.php on line 544

Deprecated: Return type of WP_REST_Request::offsetExists($offset) should either be compatible with ArrayAccess::offsetExists(mixed $offset): bool, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/rest-api/class-wp-rest-request.php on line 960

Deprecated: Return type of WP_REST_Request::offsetGet($offset) should either be compatible with ArrayAccess::offsetGet(mixed $offset): mixed, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/rest-api/class-wp-rest-request.php on line 980

Deprecated: Return type of WP_REST_Request::offsetSet($offset, $value) should either be compatible with ArrayAccess::offsetSet(mixed $offset, mixed $value): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/rest-api/class-wp-rest-request.php on line 992

Deprecated: Return type of WP_REST_Request::offsetUnset($offset) should either be compatible with ArrayAccess::offsetUnset(mixed $offset): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/rest-api/class-wp-rest-request.php on line 1003

Deprecated: Return type of WP_Block_List::current() should either be compatible with Iterator::current(): mixed, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-block-list.php on line 151

Deprecated: Return type of WP_Block_List::next() should either be compatible with Iterator::next(): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-block-list.php on line 175

Deprecated: Return type of WP_Block_List::key() should either be compatible with Iterator::key(): mixed, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-block-list.php on line 164

Deprecated: Return type of WP_Block_List::valid() should either be compatible with Iterator::valid(): bool, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-block-list.php on line 186

Deprecated: Return type of WP_Block_List::rewind() should either be compatible with Iterator::rewind(): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-block-list.php on line 138

Deprecated: Return type of WP_Block_List::offsetExists($index) should either be compatible with ArrayAccess::offsetExists(mixed $offset): bool, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-block-list.php on line 75

Deprecated: Return type of WP_Block_List::offsetGet($index) should either be compatible with ArrayAccess::offsetGet(mixed $offset): mixed, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-block-list.php on line 89

Deprecated: Return type of WP_Block_List::offsetSet($index, $value) should either be compatible with ArrayAccess::offsetSet(mixed $offset, mixed $value): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-block-list.php on line 110

Deprecated: Return type of WP_Block_List::offsetUnset($index) should either be compatible with ArrayAccess::offsetUnset(mixed $offset): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-block-list.php on line 127

Deprecated: Return type of WP_Block_List::count() should either be compatible with Countable::count(): int, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in /var/www/twodee/blog/wp-includes/class-wp-block-list.php on line 199

Deprecated: Creation of dynamic property Automattic\Jetpack\Connection\Manager::$error_handler is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/vendor/automattic/jetpack-connection/src/class-manager.php on line 91

Deprecated: Creation of dynamic property WPCOM_REST_API_V2_Endpoint_Instagram_Gallery::$is_wpcom is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-instagram-gallery.php on line 23

Deprecated: Creation of dynamic property WPCOM_REST_API_V2_Endpoint_Mailchimp::$wpcom_is_wpcom_only_endpoint is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-mailchimp.php on line 17

Deprecated: Creation of dynamic property WPCOM_REST_API_V2_Endpoint_Tweetstorm_Gather::$wpcom_is_wpcom_only_endpoint is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-tweetstorm-gather.php on line 21

Deprecated: Creation of dynamic property WPCOM_REST_API_V2_Endpoint_Tweetstorm_Gather::$is_wpcom is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-tweetstorm-gather.php on line 22

Deprecated: Creation of dynamic property WPCOM_REST_API_V2_Endpoint_Gutenberg_Available_Extensions::$wpcom_is_site_specific_endpoint is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/gutenberg-available-extensions.php on line 20

Deprecated: Creation of dynamic property WPCOM_REST_API_V2_Endpoint_Memberships::$wpcom_is_wpcom_only_endpoint is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/memberships.php on line 23

Deprecated: Creation of dynamic property WPCOM_REST_API_V2_Endpoint_Memberships::$wpcom_is_site_specific_endpoint is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/memberships.php on line 24

Deprecated: Creation of dynamic property WPCOM_REST_API_V2_Endpoint_Subscribers::$wpcom_is_wpcom_only_endpoint is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/subscribers.php on line 15

Deprecated: Creation of dynamic property Automattic\Jetpack\Sync\Queue::$random_int is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-queue.php on line 38

Deprecated: Creation of dynamic property Automattic\Jetpack\Sync\Queue::$random_int is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/vendor/automattic/jetpack-sync/src/class-queue.php on line 38

Deprecated: Creation of dynamic property WPCOM_Widget_Goodreads::$shelves is deprecated in /var/www/twodee/blog/wp-content/plugins/jetpack/modules/widgets/goodreads.php on line 33

Deprecated: DateTime::__construct(): Passing null to parameter #1 ($datetime) of type string is deprecated in /var/www/twodee/blog/wp-includes/script-loader.php on line 348

Deprecated: Creation of dynamic property Advanced_Editor_Tools::$toolbar_classic_block is deprecated in /var/www/twodee/blog/wp-content/plugins/tinymce-advanced/tinymce-advanced.php on line 347

Deprecated: Creation of dynamic property Advanced_Editor_Tools::$toolbar_block is deprecated in /var/www/twodee/blog/wp-content/plugins/tinymce-advanced/tinymce-advanced.php on line 349

Deprecated: Creation of dynamic property Advanced_Editor_Tools::$toolbar_block_side is deprecated in /var/www/twodee/blog/wp-content/plugins/tinymce-advanced/tinymce-advanced.php on line 350

Deprecated: Creation of dynamic property Advanced_Editor_Tools::$panels_block is deprecated in /var/www/twodee/blog/wp-content/plugins/tinymce-advanced/tinymce-advanced.php on line 351

Deprecated: Creation of dynamic property Advanced_Editor_Tools::$used_block_buttons is deprecated in /var/www/twodee/blog/wp-content/plugins/tinymce-advanced/tinymce-advanced.php on line 354

Deprecated: Creation of dynamic property WP_Block_Type::$plan_check is deprecated in /var/www/twodee/blog/wp-includes/class-wp-block-type.php on line 333

Deprecated: Creation of dynamic property WP_Block_Type::$plan_check is deprecated in /var/www/twodee/blog/wp-includes/class-wp-block-type.php on line 333

Deprecated: Creation of dynamic property WP_Block_Type::$plan_check is deprecated in /var/www/twodee/blog/wp-includes/class-wp-block-type.php on line 333

Deprecated: Creation of dynamic property WP_Block_Type::$plan_check is deprecated in /var/www/twodee/blog/wp-includes/class-wp-block-type.php on line 333

Deprecated: Creation of dynamic property WP_Block_Type::$plan_check is deprecated in /var/www/twodee/blog/wp-includes/class-wp-block-type.php on line 333

Deprecated: Creation of dynamic property WP_Block_Type::$plan_check is deprecated in /var/www/twodee/blog/wp-includes/class-wp-block-type.php on line 333

Deprecated: Creation of dynamic property WP_Term::$object_id is deprecated in /var/www/twodee/blog/wp-includes/class-wp-term.php on line 198

Deprecated: Creation of dynamic property WP_Term::$object_id is deprecated in /var/www/twodee/blog/wp-includes/class-wp-term.php on line 198

Deprecated: Creation of dynamic property WP_Term::$object_id is deprecated in /var/www/twodee/blog/wp-includes/class-wp-term.php on line 198
» SENG 440: Lecture 20 – Multitouch

teaching machines

SENG 440: Lecture 20 – Multitouch

May 16, 2019 by . Filed under
Deprecated: Creation of dynamic property WP_Term::$cat_ID is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 378

Deprecated: Creation of dynamic property WP_Term::$category_count is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 379

Deprecated: Creation of dynamic property WP_Term::$category_description is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 380

Deprecated: Creation of dynamic property WP_Term::$cat_name is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 381

Deprecated: Creation of dynamic property WP_Term::$category_nicename is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 382

Deprecated: Creation of dynamic property WP_Term::$category_parent is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 383

Deprecated: Creation of dynamic property WP_Term::$cat_ID is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 378

Deprecated: Creation of dynamic property WP_Term::$category_count is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 379

Deprecated: Creation of dynamic property WP_Term::$category_description is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 380

Deprecated: Creation of dynamic property WP_Term::$cat_name is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 381

Deprecated: Creation of dynamic property WP_Term::$category_nicename is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 382

Deprecated: Creation of dynamic property WP_Term::$category_parent is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 383

Deprecated: Creation of dynamic property WP_Term::$cat_ID is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 378

Deprecated: Creation of dynamic property WP_Term::$category_count is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 379

Deprecated: Creation of dynamic property WP_Term::$category_description is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 380

Deprecated: Creation of dynamic property WP_Term::$cat_name is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 381

Deprecated: Creation of dynamic property WP_Term::$category_nicename is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 382

Deprecated: Creation of dynamic property WP_Term::$category_parent is deprecated in /var/www/twodee/blog/wp-includes/category.php on line 383
lectures, semester1-2019, seng440.

Dear students,

Today we write an app that solves the problem of choosing of which player goes first in a game. All players place a finger on the phone, and the app chooses one of them randomly. The game is inspired by the commercial app Chwazi.

Next lecture we will discuss writing an app that is multitouch-aware. See the TODO below for the assigned exercises.

Lots

Following are the exercises assigned last time. We will use and discuss the solutions that you’ve submitted as we assemble our app, but I include my solutions here for reference.

  1. Declare a PreferenceScreen resource in xml/preferences.xml to present a single EditText preference for the delay in seconds from the last down or up event until one of the active pointers is randomly picked. Make the preference a decimal number with key pickDelay. Give it a title, a summary, a default of 3, and an icon of @drawable/timer. Possible solution.
    <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
      <EditTextPreference
        android:defaultValue="3"
        android:icon="@drawable/timer"
        android:inputType="numberSigned|number|numberDecimal"
        android:key="pickDelay"
        android:selectAllOnFocus="true"
        android:singleLine="true"
        android:summary="Time in seconds until random player selected"
        android:title="Delay" />
    </PreferenceScreen>
    
  2. Define class SettingsFragment as a subclass of PreferenceFragmentCompat that shows the app’s PreferenceScreen. Possible solution.
    class SettingsFragment : PreferenceFragmentCompat() {
      override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
        setPreferencesFromResource(R.xml.preferences, rootKey)
    
        findPreference<EditTextPreference>("delayToPick")?.let {
          it.setOnBindEditTextListener { editor ->
            editor.inputType = InputType.TYPE_CLASS_NUMBER
          }
        }
      }
    }
    
  3. Define field pickTask in PickerView to be a Runnable that sets pickedId to a randomly chosen key from pointerIdToPosition. Also, declare field onFirstDown in PickerView to be a lambda that accepts no parameters and returns nothing. Define it to do nothing, but allow clients to redefine it to trigger some action when a first touch event occurs. Possible solution.
    private val pickTask = Runnable {
      pickedId = pointerIdToPosition.keys.random()
    }
    
  4. Write method schedulePick that invalidates pickedId and unschedules pickTask from the inherited handler. If there are any entries left in the positions map, schedule pickTask to execute after the pick delay, which you can access via prefs. Watch your units. Possible solution.
    private fun schedulePick() {
      pickedId = -1
      handler.removeCallbacks(pickTask)
      if (pointerIdToPosition.isNotEmpty()) {
        val delay = ((prefs.getString("pickDelay", null) ?: "3").toFloat() * 1000).toLong()
        handler.postDelayed(pickTask, delay)
      }
    }
    
  5. Write method handleDowns in PickerView to accept a MotionEvent parameter. Call onFirstDown when the first touch occurs. When any down event occurs, first or otherwise, do two things:
    • Reschedule the picking.
    • Map the active pointer to position (0, 0)—but only if the map doesn’t already have an entry for the pointer.
    Possible solution.
    private fun handleDowns(event: MotionEvent) {
      if (event.actionMasked == MotionEvent.ACTION_DOWN) {
        onFirstDown()
      }
    
      when (event.actionMasked) {
        MotionEvent.ACTION_DOWN,
        MotionEvent.ACTION_POINTER_DOWN -> {
          val id = event.getPointerId(event.actionIndex)
          if (!pointerIdToPosition.containsKey(id)) {
            pointerIdToPosition[id] = PointF()
          }
          schedulePick()
        }
      }
    }
    
  6. Write method handlePositions in PickerView to accept a MotionEvent parameter. Iterate through all the currently tracked pointers and update the positions map. Possible solution.
    private fun handlePositions(event: MotionEvent) {
      repeat (event.pointerCount) { i ->
        id = event.getPointerId(i)
        pointerIdToPosition[id]?.apply {
          x = event.getX(i)
          y = event.getY(i)
        }
      }
    }
    
  7. Write method handleUps in PickerView to accept a MotionEvent parameter. When any up event occurs, first or otherwise, do two things:
    • Reschedule the picking.
    • Remove any entry for the active pointer from the positions map.
    Possible solution.
    private fun handleUps(event: MotionEvent) {
      when (event.actionMasked) {
        MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> {
          pointerIdToPosition.remove(event.getPointerId(event.actionIndex))
          schedulePick()
        }
      }
    }
    
  8. Write method onTouchEvent in PickerView to handle any down events, update the tracked pointer positions, and handle any update events. Also, so that the UI reflects the pointer state, force the view to be redrawn. Possible solution.
    override fun onTouchEvent(event: MotionEvent): Boolean {
      handleDowns(event)
      handlePositions(event)
      handleUps(event)
      invalidate()
      return true
    }
    
  9. Write method drawFingerprint in PickerView to accept a Canvas parameter. Draw fingerprint in the center of the view, ensuring that it fits within the bounds. Assume that the fingerprint is square. Possible solution.
    private fun drawFingerprint(canvas: Canvas) {
      val imageAspect = fingerprint.intrinsicWidth / fingerprint.intrinsicHeight.toFloat()
      val frameAspect = width / height.toFloat()
    
      val boxWidth: Int
      val boxHeight: Int
    
      if (frameAspect < imageAspect) {
        boxWidth = width
        boxHeight = (width / imageAspect).toInt()
      } else {
        boxHeight = height
        boxWidth = (height * imageAspect).toInt()
      }
    
      val centerX = (width * 0.5f).toInt()
      val centerY = (height * 0.5f).toInt()
    
      fingerprint.setBounds(centerX - boxWidth / 2, centerY - boxHeight / 2, centerX + boxWidth / 2, centerY + boxHeight / 2)
      fingerprint.draw(canvas)
    }
    
  10. Write method onDraw in PickerView to accept a Canvas parameter. If there are no currently tracked pointers, draw the fingerprint as a cue for the user to press the screen. Otherwise, draw circles around each pointer. Use unpickedPaint by default, but if a pointer is the picked pointer, use pickedPaint. Possible solution.
    override fun onDraw(canvas: Canvas) {
      if (pointerIdToPosition.isEmpty()) {
        drawFingerprint(canvas)
      } else {
        pointerIdToPosition.forEach { id, p ->
          canvas.drawCircle(p.x, p.y, 150f, if (id == pickedId) pickedPaint else unpickedPaint)
        }
      }
    }
    
  11. Write method enableFullscreen in MainActivity to put the app into fullscreen immersive mode. Ensure that a stable layout is used—even when the navigation controls and status bar are visible. Possible solution.
    private fun enableFullscreen() {
      window.decorView.systemUiVisibility =
        View.SYSTEM_UI_FLAG_IMMERSIVE or
          View.SYSTEM_UI_FLAG_FULLSCREEN or
          View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
          View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
          View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
          View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
    }
    

TODO

Next lecture we will examine two common gestures that occur on mobile platforms: pinch-and-zoom and panning. We won’t create a full app, just a custom view that displays an image and supports these two gestures. Here’s the start to the custom view:

class ZoomPanView @JvmOverloads constructor(context: Context, attributes: AttributeSet? = null, styleAttributes: Int = 0) : View(context, attributes, styleAttributes) {
  private val image: Bitmap = BitmapFactory.decodeResource(resources, R.drawable.some_image)
  private var bounds = RectF()
}

The image will be drawn in the view’s Canvas inside the frame demarcated by bounds. Most of the exercises deal with adjusting bounds appropriately.

The main activity does nothing but load this layout:

<org.twodee.zoompan.ZoomPanView xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:background="#FFFFFF"
  tools:context=".MainActivity" />

The exercises that you will collectively complete are listed below. Check your email for your assigned exercise and a link to submit your solution. Become an expert on your particular corner of the app, investigating background material as needed. Build on top of the activity and others’ code as you complete your exercise. No solution should be very long, but some have more overhead like curly braces and method signatures than others.

  1. Define field panListener to be a SimpleOnGestureListener. When a scroll event happens, it shifts the bounding box by the scrolled distance and forces the view to be redrawn.
  2. Define field zoomListener to be a SimpleOnScaleGestureListener. When a scale event happens, scale the bounding box. Bonus points if you clamp the scale from becoming too small. Tall poppy points if you scale about fingers’ centroid rather than (0, 0).
  3. Investigate gesture detectors in Android. Define fields panDetector and zoomDetector to be detectors for the two listeners defined in the previous steps.
  4. Define method fillWidth to set the bounds such that the image fills the view’s width. Set the height to maintain the image’s aspect ratio.
  5. Define method fillHeight to set the bounds such that the image fills the view’s height. Set the width to maintain the image’s aspect ratio.
  6. Define method onSizeChanged to set the bounds to fill the view as much possible—filling either the width or height. Bonus points if you figure out what criteria can fit the image to the view without cropping any of it.
  7. Define method onDraw to draw the bitmap to fill the current bounds.
  8. Define method onTouchEvent to let the detectors process the event.

See you next time!

Sincerely,

P.S. It’s time for a haiku!

It’s not multitouch
When I flit across the phone
It’s finger skating