FloatNodes.java
/*
* Copyright 2022 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 androidx.wear.protolayout.expression.pipeline;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat;
import androidx.wear.protolayout.expression.proto.AnimationParameterProto.AnimationSpec;
import androidx.wear.protolayout.expression.proto.DynamicProto;
import androidx.wear.protolayout.expression.proto.DynamicProto.AnimatableFixedFloat;
import androidx.wear.protolayout.expression.proto.DynamicProto.ArithmeticFloatOp;
import androidx.wear.protolayout.expression.proto.DynamicProto.StateFloatSource;
import androidx.wear.protolayout.expression.proto.FixedProto.FixedFloat;
/** Dynamic data nodes which yield floats. */
class FloatNodes {
private FloatNodes() {}
/** Dynamic float node that has a fixed value. */
static class FixedFloatNode implements DynamicDataSourceNode<Float> {
@Nullable private final Float mValue;
private final DynamicTypeValueReceiverWithPreUpdate<Float> mDownstream;
FixedFloatNode(
FixedFloat protoNode, DynamicTypeValueReceiverWithPreUpdate<Float> downstream) {
this.mValue = getValidValueOrNull(protoNode.getValue());
this.mDownstream = downstream;
}
@Override
@UiThread
public void preInit() {
mDownstream.onPreUpdate();
}
@Override
@UiThread
public void init() {
if (mValue == null) {
mDownstream.onInvalidated();
} else {
mDownstream.onData(mValue);
}
}
@Override
@UiThread
public void destroy() {}
}
/** Dynamic float node that gets value from the state. */
static class StateFloatSourceNode extends StateSourceNode<Float> {
StateFloatSourceNode(
DataStore dataStore,
StateFloatSource protoNode,
DynamicTypeValueReceiverWithPreUpdate<Float> downstream) {
super(
dataStore,
StateSourceNode.<DynamicFloat>createKey(
protoNode.getSourceNamespace(), protoNode.getSourceKey()),
se -> getValidValueOrNull(se.getFloatVal().getValue()),
downstream);
}
}
/** Dynamic float node that supports arithmetic operations. */
static class ArithmeticFloatNode extends DynamicDataBiTransformNode<Float, Float, Float> {
private static final String TAG = "ArithmeticFloatNode";
ArithmeticFloatNode(
ArithmeticFloatOp protoNode,
DynamicTypeValueReceiverWithPreUpdate<Float> downstream) {
super(
downstream,
(lhs, rhs) ->
getValidValueOrNull(
computeResult(protoNode.getOperationType(), lhs, rhs)));
}
private static float computeResult(
DynamicProto.ArithmeticOpType opType, float lhs, float rhs) {
try {
switch (opType) {
case ARITHMETIC_OP_TYPE_ADD:
return lhs + rhs;
case ARITHMETIC_OP_TYPE_SUBTRACT:
return lhs - rhs;
case ARITHMETIC_OP_TYPE_MULTIPLY:
return lhs * rhs;
case ARITHMETIC_OP_TYPE_DIVIDE:
return lhs / rhs;
case ARITHMETIC_OP_TYPE_MODULO:
return lhs % rhs;
case ARITHMETIC_OP_TYPE_UNDEFINED:
case UNRECOGNIZED:
break;
}
} catch (ArithmeticException ex) {
Log.e(TAG, "ArithmeticException in ArithmeticFloatNode", ex);
return Float.NaN;
}
throw new IllegalArgumentException(
"Unknown operation type in ArithmeticFloatNode: " + opType);
}
}
/** Dynamic float node that gets value from INTEGER. */
static class Int32ToFloatNode extends DynamicDataTransformNode<Integer, Float> {
Int32ToFloatNode(DynamicTypeValueReceiverWithPreUpdate<Float> downstream) {
super(downstream, i -> (float) i);
}
}
/** Dynamic float node that gets animatable value from fixed source. */
static class AnimatableFixedFloatNode extends AnimatableNode
implements DynamicDataSourceNode<Float> {
private final AnimatableFixedFloat mProtoNode;
private final DynamicTypeValueReceiverWithPreUpdate<Float> mDownstream;
private boolean mFirstUpdateFromAnimatorDone = false;
AnimatableFixedFloatNode(
AnimatableFixedFloat protoNode,
DynamicTypeValueReceiverWithPreUpdate<Float> downstream,
QuotaManager quotaManager) {
super(quotaManager, protoNode.getAnimationSpec());
this.mProtoNode = protoNode;
this.mDownstream = downstream;
mQuotaAwareAnimator.addUpdateCallback(
animatedValue -> {
// The onPreUpdate has already been called once before the first update.
if (mFirstUpdateFromAnimatorDone) {
mDownstream.onPreUpdate();
}
mDownstream.onData((Float) animatedValue);
mFirstUpdateFromAnimatorDone = true;
});
}
@Override
@UiThread
public void preInit() {
mDownstream.onPreUpdate();
}
@Override
@UiThread
public void init() {
if (isValid(mProtoNode.getFromValue()) && isValid(mProtoNode.getToValue())) {
mQuotaAwareAnimator.setFloatValues(
mProtoNode.getFromValue(), mProtoNode.getToValue());
// For the first update from the animator with the above from & to values, the
// onPreUpdate has already been called.
mFirstUpdateFromAnimatorDone = false;
startOrSkipAnimator();
} else {
mDownstream.onInvalidated();
}
}
@Override
@UiThread
public void destroy() {
mQuotaAwareAnimator.stopAnimator();
}
}
/** Dynamic float node that gets animatable value from dynamic source. */
static class DynamicAnimatedFloatNode extends AnimatableNode implements DynamicDataNode<Float> {
final DynamicTypeValueReceiverWithPreUpdate<Float> mDownstream;
private final DynamicTypeValueReceiverWithPreUpdate<Float> mInputCallback;
@Nullable Float mCurrentValue = null;
int mPendingCalls = 0;
private boolean mFirstUpdateFromAnimatorDone = false;
// Static analysis complains about calling methods of parent class AnimatableNode under
// initialization but mInputCallback is only used after the constructor is finished.
@SuppressWarnings("method.invocation.invalid")
DynamicAnimatedFloatNode(
DynamicTypeValueReceiverWithPreUpdate<Float> downstream,
@NonNull AnimationSpec spec,
QuotaManager quotaManager) {
super(quotaManager, spec);
this.mDownstream = downstream;
mQuotaAwareAnimator.addUpdateCallback(
animatedValue -> {
if (mPendingCalls == 0) {
// The onPreUpdate has already been called once before the first update.
if (mFirstUpdateFromAnimatorDone) {
mDownstream.onPreUpdate();
}
mCurrentValue = (Float) animatedValue;
mDownstream.onData(mCurrentValue);
mFirstUpdateFromAnimatorDone = true;
}
});
this.mInputCallback =
new DynamicTypeValueReceiverWithPreUpdate<Float>() {
@Override
public void onPreUpdate() {
mPendingCalls++;
if (mPendingCalls == 1) {
mDownstream.onPreUpdate();
}
}
@Override
public void onData(@NonNull Float newData) {
if (mPendingCalls > 0) {
mPendingCalls--;
}
if (mPendingCalls == 0) {
if (mCurrentValue == null) {
mCurrentValue = newData;
mDownstream.onData(mCurrentValue);
} else {
mQuotaAwareAnimator.setFloatValues(mCurrentValue, newData);
// For the first update from the animator with the above from &
// to values, the onPreUpdate has already been called.
mFirstUpdateFromAnimatorDone = false;
startOrSkipAnimator();
}
}
}
@Override
public void onInvalidated() {
if (mPendingCalls > 0) {
mPendingCalls--;
}
if (mPendingCalls == 0) {
mCurrentValue = null;
mDownstream.onInvalidated();
}
}
};
}
public DynamicTypeValueReceiverWithPreUpdate<Float> getInputCallback() {
return mInputCallback;
}
}
private static boolean isValid(Float value) {
return value != null && Float.isFinite(value);
}
@Nullable
private static Float getValidValueOrNull(Float value) {
return isValid(value) ? value : null;
}
}