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:
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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>)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user