Controls UI - Update drag behavior for ranges

Consolidate toggle() fns. Update range handling. It was previously
acting like a slider but now detects the diff between how far you
swiped and updates the current value appropriately.

Bug: 148207527
Test: manual testing of drag behavior
Change-Id: I85896527bd3ff225cd08d11fbe2e4b34190a6b5a
This commit is contained in:
Matt Pietal
2020-02-10 07:27:01 -05:00
parent 20024529db
commit 307b1ef70c
7 changed files with 171 additions and 104 deletions

View File

@@ -0,0 +1,50 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.controls.ui
import android.app.PendingIntent
import android.content.Intent
import android.service.controls.actions.BooleanAction
import android.util.Log
import android.view.HapticFeedbackConstants
object ControlActionCoordinator {
public const val MIN_LEVEL = 0
public const val MAX_LEVEL = 10000
fun toggle(cvh: ControlViewHolder, templateId: String, isChecked: Boolean) {
cvh.action(BooleanAction(templateId, !isChecked))
val nextLevel = if (isChecked) MIN_LEVEL else MAX_LEVEL
cvh.clipLayer.setLevel(nextLevel)
}
fun longPress(cvh: ControlViewHolder) {
// Long press snould only be called when there is valid control state, otherwise ignore
cvh.cws.control?.let {
try {
cvh.layout.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
it.getAppIntent().send()
val closeDialog = Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)
cvh.context.sendBroadcast(closeDialog)
} catch (e: PendingIntent.CanceledException) {
Log.e(ControlsUiController.TAG, "Error sending pending intent", e)
cvh.setTransientStatus("Error opening application")
}
}
}
}

View File

@@ -17,7 +17,6 @@
package com.android.systemui.controls.ui
import android.content.Context
import android.content.Intent
import android.graphics.drawable.ClipDrawable
import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.Icon
@@ -37,8 +36,6 @@ import com.android.systemui.controls.controller.ControlsController
import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.R
const val MIN_LEVEL = 0
const val MAX_LEVEL = 10000
private const val UPDATE_DELAY_IN_MILLIS = 3000L
class ControlViewHolder(
@@ -79,12 +76,9 @@ class ControlViewHolder(
Pair(Control.STATUS_UNKNOWN, ControlTemplate.NO_TEMPLATE)
}
cws.control?.let { c ->
cws.control?.let {
layout.setOnLongClickListener(View.OnLongClickListener() {
val closeDialog = Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)
context.sendBroadcast(closeDialog)
c.getAppIntent().send()
ControlActionCoordinator.longPress(this@ControlViewHolder)
true
})
}
@@ -100,19 +94,23 @@ class ControlViewHolder(
}
if (!text.isEmpty()) {
val previousText = status.getText()
val previousTextExtra = statusExtra.getText()
cancelUpdate = uiExecutor.executeDelayed({
status.setText(previousText)
statusExtra.setText(previousTextExtra)
}, UPDATE_DELAY_IN_MILLIS)
status.setText(text)
statusExtra.setText("")
setTransientStatus(text)
}
}
fun setTransientStatus(tempStatus: String) {
val previousText = status.getText()
val previousTextExtra = statusExtra.getText()
cancelUpdate = uiExecutor.executeDelayed({
status.setText(previousText)
statusExtra.setText(previousTextExtra)
}, UPDATE_DELAY_IN_MILLIS)
status.setText(tempStatus)
statusExtra.setText("")
}
fun action(action: ControlAction) {
controlsController.action(cws.ci, action)
}

View File

@@ -24,6 +24,10 @@ import android.view.ViewGroup
interface ControlsUiController {
val available: Boolean
companion object {
public const val TAG = "ControlsUiController"
}
fun show(parent: ViewGroup)
fun hide()
fun onRefreshState(componentName: ComponentName, controls: List<Control>)

View File

@@ -51,8 +51,6 @@ import java.text.Collator
import javax.inject.Inject
import javax.inject.Singleton
private const val TAG = "ControlsUi"
// TEMP CODE for MOCK
private const val TOKEN = "https://www.googleapis.com/auth/assistant"
private const val SCOPE = "oauth2:" + TOKEN
@@ -63,13 +61,13 @@ class TokenProviderConnection(val cc: ControlsController, val context: Context)
override fun onServiceConnected(cName: ComponentName, binder: IBinder) {
Thread({
Log.i(TAG, "TokenProviderConnection connected")
Log.i(ControlsUiController.TAG, "TokenProviderConnection connected")
mTokenProvider = TokenProvider.Stub.asInterface(binder)
val mLastAccountName = mTokenProvider?.getAccountName()
if (mLastAccountName == null || mLastAccountName.isEmpty()) {
Log.e(TAG, "NO ACCOUNT IS SET. Open HomeMock app")
Log.e(ControlsUiController.TAG, "NO ACCOUNT IS SET. Open HomeMock app")
} else {
mTokenProvider?.setAuthToken(getAuthToken(mLastAccountName))
cc.subscribeToFavorites()
@@ -85,7 +83,7 @@ class TokenProviderConnection(val cc: ControlsController, val context: Context)
val am = AccountManager.get(context)
val accounts = am.getAccountsByType("com.google")
if (accounts == null || accounts.size == 0) {
Log.w(TAG, "No com.google accounts found")
Log.w(ControlsUiController.TAG, "No com.google accounts found")
return null
}
@@ -104,7 +102,7 @@ class TokenProviderConnection(val cc: ControlsController, val context: Context)
try {
return am.blockingGetAuthToken(account!!, SCOPE, true)
} catch (e: Throwable) {
Log.e(TAG, "Error getting auth token", e)
Log.e(ControlsUiController.TAG, "Error getting auth token", e)
return null
}
}
@@ -146,7 +144,7 @@ class ControlsUiControllerImpl @Inject constructor (
}
override fun show(parent: ViewGroup) {
Log.d(TAG, "show()")
Log.d(ControlsUiController.TAG, "show()")
this.parent = parent
@@ -218,7 +216,7 @@ class ControlsUiControllerImpl @Inject constructor (
val listView = parent.requireViewById(R.id.global_actions_controls_list) as ViewGroup
var lastRow: ViewGroup = createRow(inflater, listView)
controlInfos.forEach {
Log.d(TAG, "favorited control id: " + it.controlId)
Log.d(ControlsUiController.TAG, "favorited control id: " + it.controlId)
if (lastRow.getChildCount() == 2) {
lastRow = createRow(inflater, listView)
}
@@ -240,7 +238,7 @@ class ControlsUiControllerImpl @Inject constructor (
}
override fun hide() {
Log.d(TAG, "hide()")
Log.d(ControlsUiController.TAG, "hide()")
controlsController.get().unsubscribe()
context.unbindService(tokenProviderConnection)
tokenProviderConnection = null
@@ -252,10 +250,10 @@ class ControlsUiControllerImpl @Inject constructor (
}
override fun onRefreshState(componentName: ComponentName, controls: List<Control>) {
Log.d(TAG, "onRefreshState()")
Log.d(ControlsUiController.TAG, "onRefreshState()")
controls.forEach { c ->
controlsById.get(ControlKey(componentName, c.getControlId()))?.let {
Log.d(TAG, "onRefreshState() for id: " + c.getControlId())
Log.d(ControlsUiController.TAG, "onRefreshState() for id: " + c.getControlId())
val cws = ControlWithState(it.ci, c)
val key = ControlKey(componentName, c.getControlId())
controlsById.put(key, cws)

View File

@@ -24,6 +24,8 @@ import android.service.controls.templates.TemperatureControlTemplate
import android.widget.TextView
import com.android.systemui.R
import com.android.systemui.controls.ui.ControlActionCoordinator.MIN_LEVEL
import com.android.systemui.controls.ui.ControlActionCoordinator.MAX_LEVEL
class TemperatureControlBehavior : Behavior {
lateinit var clipLayer: Drawable

View File

@@ -22,10 +22,11 @@ import android.graphics.drawable.LayerDrawable
import android.view.View
import android.widget.TextView
import android.service.controls.Control
import android.service.controls.actions.BooleanAction
import android.service.controls.templates.ToggleTemplate
import com.android.systemui.R
import com.android.systemui.controls.ui.ControlActionCoordinator.MIN_LEVEL
import com.android.systemui.controls.ui.ControlActionCoordinator.MAX_LEVEL
class ToggleBehavior : Behavior {
lateinit var clipLayer: Drawable
@@ -42,13 +43,15 @@ class ToggleBehavior : Behavior {
status.setText(control.getStatusText())
cvh.layout.setOnClickListener(View.OnClickListener() { toggle() })
template = control.getControlTemplate() as ToggleTemplate
cvh.layout.setOnClickListener(View.OnClickListener() {
ControlActionCoordinator.toggle(cvh, template.getTemplateId(), template.isChecked())
})
val ld = cvh.layout.getBackground() as LayerDrawable
clipLayer = ld.findDrawableByLayerId(R.id.clip_layer)
template = control.getControlTemplate() as ToggleTemplate
val checked = template.isChecked()
val deviceType = control.getDeviceType()
@@ -56,11 +59,4 @@ class ToggleBehavior : Behavior {
cvh.setEnabled(checked)
cvh.applyRenderInfo(RenderInfo.lookup(deviceType, checked))
}
fun toggle() {
cvh.action(BooleanAction(template.getTemplateId(), !template.isChecked()))
val nextLevel = if (template.isChecked()) MIN_LEVEL else MAX_LEVEL
clipLayer.setLevel(nextLevel)
}
}

View File

@@ -19,17 +19,23 @@ package com.android.systemui.controls.ui
import android.content.Context
import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
import android.util.Log
import android.view.GestureDetector
import android.view.GestureDetector.SimpleOnGestureListener
import android.view.MotionEvent
import android.view.View
import android.widget.TextView
import android.service.controls.Control
import android.service.controls.actions.BooleanAction
import android.service.controls.actions.FloatAction
import android.service.controls.templates.RangeTemplate
import android.service.controls.templates.ToggleRangeTemplate
import android.util.TypedValue
import com.android.systemui.R
import com.android.systemui.controls.ui.ControlActionCoordinator.MIN_LEVEL
import com.android.systemui.controls.ui.ControlActionCoordinator.MAX_LEVEL
import java.util.IllegalFormatException
class ToggleRangeBehavior : Behavior {
lateinit var clipLayer: Drawable
@@ -41,6 +47,10 @@ class ToggleRangeBehavior : Behavior {
lateinit var status: TextView
lateinit var context: Context
companion object {
private const val DEFAULT_FORMAT = "%.1f"
}
override fun apply(cvh: ControlViewHolder, cws: ControlWithState) {
this.control = cws.control!!
this.cvh = cvh
@@ -52,10 +62,25 @@ class ToggleRangeBehavior : Behavior {
context = status.getContext()
cvh.layout.setOnTouchListener(ToggleRangeTouchListener())
val gestureListener = ToggleRangeGestureListener(cvh.layout)
val gestureDetector = GestureDetector(context, gestureListener)
cvh.layout.setOnTouchListener({ v: View, e: MotionEvent ->
if (gestureDetector.onTouchEvent(e)) {
return@setOnTouchListener true
}
if (e.getAction() == MotionEvent.ACTION_UP && gestureListener.isDragging) {
gestureListener.isDragging = false
endUpdateRange()
return@setOnTouchListener true
}
return@setOnTouchListener false
})
val ld = cvh.layout.getBackground() as LayerDrawable
clipLayer = ld.findDrawableByLayerId(R.id.clip_layer)
clipLayer.setLevel(MIN_LEVEL)
template = control.getControlTemplate() as ToggleRangeTemplate
rangeTemplate = template.getRange()
@@ -63,30 +88,31 @@ class ToggleRangeBehavior : Behavior {
val checked = template.isChecked()
val deviceType = control.getDeviceType()
updateRange((rangeTemplate.getCurrentValue() / 100.0f), checked)
val currentRatio = rangeTemplate.getCurrentValue() /
(rangeTemplate.getMaxValue() - rangeTemplate.getMinValue())
updateRange(currentRatio, checked)
cvh.setEnabled(checked)
cvh.applyRenderInfo(RenderInfo.lookup(deviceType, checked))
}
fun toggle() {
cvh.action(BooleanAction(template.getTemplateId(), !template.isChecked()))
val nextLevel = if (template.isChecked()) MIN_LEVEL else MAX_LEVEL
clipLayer.setLevel(nextLevel)
}
fun beginUpdateRange() {
status.setVisibility(View.GONE)
statusExtra.setTextSize(TypedValue.COMPLEX_UNIT_PX, context.getResources()
.getDimensionPixelSize(R.dimen.control_status_expanded).toFloat())
}
fun updateRange(f: Float, checked: Boolean) {
clipLayer.setLevel(if (checked) (MAX_LEVEL * f).toInt() else MIN_LEVEL)
fun updateRange(ratioDiff: Float, checked: Boolean) {
val changeAmount = if (checked) (MAX_LEVEL * ratioDiff).toInt() else MIN_LEVEL
val newLevel = Math.max(MIN_LEVEL, Math.min(MAX_LEVEL, clipLayer.getLevel() + changeAmount))
clipLayer.setLevel(newLevel)
if (checked && f < 100.0f && f > 0.0f) {
statusExtra.setText("" + (f * 100.0).toInt() + "%")
if (checked) {
val newValue = levelToRangeValue()
val formattedNewValue = format(rangeTemplate.getFormatString().toString(),
DEFAULT_FORMAT, newValue)
statusExtra.setText(formattedNewValue)
statusExtra.setVisibility(View.VISIBLE)
} else {
statusExtra.setText("")
@@ -94,17 +120,30 @@ class ToggleRangeBehavior : Behavior {
}
}
fun endUpdateRange(f: Float) {
statusExtra.setText(" - " + (f * 100.0).toInt() + "%")
private fun format(primaryFormat: String, backupFormat: String, value: Float): String {
return try {
String.format(primaryFormat, value)
} catch (e: IllegalFormatException) {
Log.w(ControlsUiController.TAG, "Illegal format in range template", e)
if (backupFormat == "") {
""
} else {
format(backupFormat, "", value)
}
}
}
val newValue = rangeTemplate.getMinValue() +
(f * (rangeTemplate.getMaxValue() - rangeTemplate.getMinValue()))
private fun levelToRangeValue(): Float {
val ratio = clipLayer.getLevel().toFloat() / MAX_LEVEL
return rangeTemplate.getMinValue() +
(ratio * (rangeTemplate.getMaxValue() - rangeTemplate.getMinValue()))
}
fun endUpdateRange() {
statusExtra.setTextSize(TypedValue.COMPLEX_UNIT_PX, context.getResources()
.getDimensionPixelSize(R.dimen.control_status_normal).toFloat())
status.setVisibility(View.VISIBLE)
cvh.action(FloatAction(rangeTemplate.getTemplateId(), findNearestStep(newValue)))
cvh.action(FloatAction(rangeTemplate.getTemplateId(), findNearestStep(levelToRangeValue())))
}
fun findNearestStep(value: Float): Float {
@@ -125,59 +164,39 @@ class ToggleRangeBehavior : Behavior {
return rangeTemplate.getMaxValue()
}
inner class ToggleRangeTouchListener() : View.OnTouchListener {
private var initialTouchX: Float = 0.0f
private var initialTouchY: Float = 0.0f
private var isDragging: Boolean = false
private val minDragDiff = 20
override fun onTouch(v: View, e: MotionEvent): Boolean {
when (e.getActionMasked()) {
MotionEvent.ACTION_DOWN -> setupTouch(e)
MotionEvent.ACTION_MOVE -> detectDrag(v, e)
MotionEvent.ACTION_UP -> endTouch(v, e)
}
inner class ToggleRangeGestureListener(
val v: View
) : SimpleOnGestureListener() {
var isDragging: Boolean = false
override fun onDown(e: MotionEvent): Boolean {
return true
}
private fun setupTouch(e: MotionEvent) {
initialTouchX = e.getX()
initialTouchY = e.getY()
override fun onLongPress(e: MotionEvent) {
ControlActionCoordinator.longPress(this@ToggleRangeBehavior.cvh)
}
private fun detectDrag(v: View, e: MotionEvent) {
val xDiff = Math.abs(e.getX() - initialTouchX)
val yDiff = Math.abs(e.getY() - initialTouchY)
if (xDiff < minDragDiff) {
isDragging = false
} else {
if (!isDragging) {
this@ToggleRangeBehavior.beginUpdateRange()
}
v.getParent().requestDisallowInterceptTouchEvent(true)
isDragging = true
if (yDiff > xDiff) {
endTouch(v, e)
} else {
val percent = Math.max(0.0f, Math.min(1.0f, e.getX() / v.getWidth()))
this@ToggleRangeBehavior.updateRange(percent, true)
}
}
}
private fun endTouch(v: View, e: MotionEvent) {
override fun onScroll(
e1: MotionEvent,
e2: MotionEvent,
xDiff: Float,
yDiff: Float
): Boolean {
if (!isDragging) {
this@ToggleRangeBehavior.toggle()
} else {
val percent = Math.max(0.0f, Math.min(1.0f, e.getX() / v.getWidth()))
this@ToggleRangeBehavior.endUpdateRange(percent)
this@ToggleRangeBehavior.beginUpdateRange()
isDragging = true
}
initialTouchX = 0.0f
initialTouchY = 0.0f
isDragging = false
this@ToggleRangeBehavior.updateRange(-xDiff / v.getWidth(), true)
return true
}
override fun onSingleTapUp(e: MotionEvent): Boolean {
val th = this@ToggleRangeBehavior
ControlActionCoordinator.toggle(th.cvh, th.template.getTemplateId(),
th.template.isChecked())
return true
}
}
}