blob: e7d59067f568820b6612027b67531df650003284 [file] [log] [blame]
/*
* Copyright (C) 2017 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.tools.idea.observable;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import java.util.ArrayList;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* Convenience class for managing property listeners.
* <p/>
* Although you can always use {@link ObservableValue#addListener(InvalidationListener)} directly,
* occasionally this requires creating a local field to store a listener so you can remove it
* later. This can be fine for one or two listeners, but for more complex cases, use this class
* to manage listeners for you (and remove them all easily using {@link #releaseAll()}
* <p/>
* This class also provides convenience methods for listening by {@link Receiver}, which has more
* type safety than using a generic {@link InvalidationListener}.
* <p/>
* Finally, it provides a {@link #listenAll(Collection)} method, which allows you to register a
* single listener for many properties in aggregate.
* <p/>
* Note: This class is currently not thread-safe. You are expected to add and remove listeners on
* the dispatch thread to avoid undefined behavior.
*/
public final class ListenerManager {
/**
* List of all listeners registered by one of the listen calls.
*/
private final List<ListenerPairing> myListeners = new ArrayList<>();
/**
* The listen methods take either an invalidation listener (untyped) or a receiver (typed).
* When a user adds a receiver, those are wrapped in an invalidation listener, and the
* relationship is recorded here so we can later remove by receiver as well.
*/
private final Map<Receiver<?>, InvalidationListener> myReceiverMapping = Maps.newHashMap();
/**
* List of listeners registered by listenAll.
*/
private final List<CompositeListener> myCompositeListeners = Lists.newArrayListWithExpectedSize(0);
private final BatchInvoker myInvoker;
public ListenerManager() {
myInvoker = new BatchInvoker();
}
public ListenerManager(@NotNull BatchInvoker.Strategy invokeStrategy) {
myInvoker = new BatchInvoker(invokeStrategy);
}
/**
* Registers the target listener with the specified observable.
*/
public void listen(@NotNull ObservableValue<?> src, @NotNull InvalidationListener listener) {
myListeners.add(new ListenerPairing(src, listener));
}
/**
* Like {@link #listen(ObservableValue, InvalidationListener)} but with a typed receiver.
*/
public <T> void listen(@NotNull final ObservableValue<T> src, @NotNull final Receiver<T> receiver) {
InvalidationListener listenerWrapper = () -> receiver.receive(src.get());
myReceiverMapping.put(receiver, listenerWrapper);
listen(src, listenerWrapper);
}
/**
* A convenience method which both registers the target listener and then fires it with the
* observable's latest value (to initialize it, essentially).
*/
public void listenAndFire(@NotNull ObservableValue<?> src, @NotNull InvalidationListener listener) {
listen(src, listener);
listener.onInvalidated();
}
/**
* A convenience method which both registers the target receiver and then fires it with the
* observable's latest value (to initialize it, essentially).
*/
public <T> void listenAndFire(@NotNull final ObservableValue<T> src, @NotNull final Receiver<T> receiver) {
listen(src, receiver);
receiver.receive(src.get());
}
/**
* Listen to a collection of observable values, firing an event whenever one or more of them
* change on any given frame.
*
* This method starts a fluent chain, but to actually hook up a listener, you must also call
* {@link CompositeListener#with(Runnable)} as well.
*
* For example: {@code listeners.listenAll(x, y, w, h).}<b>{@code with(repaint);}</b>
*/
@NotNull
public CompositeListener listenAll(@NotNull ObservableValue<?>... values) {
CompositeListener listener = new CompositeListener(values);
myCompositeListeners.add(listener);
return listener;
}
/**
* Convenience version of {@link #listenAll(ObservableValue[])} that works when you have a
* {@link Collection} instead of an array.
*/
@NotNull
public CompositeListener listenAll(@NotNull Collection<? extends ObservableValue<?>> values) {
//noinspection unchecked
return listenAll(Iterables.toArray(values, ObservableValue.class));
}
/**
* Releases a listener previously registered via
* {@link #listen(ObservableValue, InvalidationListener)}. If the listener was registered with
* multiple observables, they will all be released.
*/
public void release(@NotNull InvalidationListener listener) {
Iterator<ListenerPairing> i = myListeners.iterator();
while (i.hasNext()) {
ListenerPairing listenerPairing = i.next();
if (listenerPairing.myListener == listener) {
listenerPairing.dispose();
i.remove();
}
}
}
/**
* Releases a receiver previously registered via
* {@link #listen(ObservableValue, Receiver)}. If the receiver was registered with
* multiple observables, they will all be released.
*/
public void release(@NotNull Receiver<?> receiver) {
InvalidationListener listenerWrapper = myReceiverMapping.get(receiver);
if (listenerWrapper == null) {
return;
}
release(listenerWrapper);
}
/**
* Releases all listeners previously registered to a target observable via
* {@link #listen(ObservableValue, InvalidationListener)} or
* {@link #listen(ObservableValue, Receiver)}.
*/
public void release(@NotNull ObservableValue<?> observable) {
Iterator<ListenerPairing> i = myListeners.iterator();
while (i.hasNext()) {
ListenerPairing listenerPairing = i.next();
if (listenerPairing.myObservable == observable) {
listenerPairing.dispose();
i.remove();
}
}
}
/**
* Releases a listener previously registered via {@link #listenAll(ObservableValue...)}
*/
public void release(@NotNull Runnable listenAllRunnable) {
Iterator<CompositeListener> iterator = myCompositeListeners.iterator();
while (iterator.hasNext()) {
CompositeListener listener = iterator.next();
if (listener.ownsRunnable(listenAllRunnable)) {
listener.dispose();
iterator.remove();
}
}
}
/**
* Release all listeners registered with this manager.
*/
public void releaseAll() {
for (ListenerPairing listener : myListeners) {
listener.dispose();
}
myListeners.clear();
for (CompositeListener listener : myCompositeListeners) {
listener.dispose();
}
myCompositeListeners.clear();
}
private static class ListenerPairing {
private final ObservableValue<?> myObservable;
private final InvalidationListener myListener;
public ListenerPairing(ObservableValue<?> src, InvalidationListener listener) {
myObservable = src;
myListener = listener;
src.addListener(listener);
}
public void dispose() {
myObservable.removeListener(myListener);
}
}
/**
* Intermediate class which gives the {@link #listenAll(ObservableValue[])} method a fluent
* interface.
*/
public final class CompositeListener implements InvalidationListener, Runnable {
@NotNull private final ObservableValue<?>[] myValues;
@Nullable private Runnable myOnAnyInvalidated;
public CompositeListener(@NotNull ObservableValue<?>... values) {
myValues = values;
for (ObservableValue<?> value : myValues) {
value.addListener(this);
}
}
public void dispose() {
for (ObservableValue<?> value : myValues) {
value.removeListener(this);
}
}
/**
* Specify the callback which will be triggered whenever any of the values we are listening to
* changes.
*/
public void with(@NotNull Runnable onAnyInvalidated) {
myOnAnyInvalidated = onAnyInvalidated;
}
/**
* Like {@link #with(Runnable)} but immediately runs the target callback once registered.
*
* This is essentially the {@link #listenAndFire(ObservableValue, InvalidationListener)}
* equivalent of {@link #listenAll(ObservableValue[])}
*/
public void withAndFire(@NotNull Runnable onAnyInvalidated) {
with(onAnyInvalidated);
run();
}
boolean ownsRunnable(@NotNull Runnable onAnyInvalidated) {
return onAnyInvalidated.equals(myOnAnyInvalidated);
}
@Override
public void onInvalidated() {
myInvoker.enqueue(this);
}
@Override
public void run() {
if (myOnAnyInvalidated != null) {
myOnAnyInvalidated.run();
}
}
}
}