package trikita.anvil;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import java.lang.reflect.InvocationTargetException;
import java.lang.ref.WeakReference;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.WeakHashMap;
/**
* Anvil class is a namespace for top-level static methods and interfaces. Most
* users would only use it to call {@code Anvil.render()}.
*
* Internally, Anvil class defines how Renderables are mounted into Views
* and how they are lazily rendered, and this is the key functionality of the
* Anvil library.
*/
public final class Anvil {
private final static Map<View, Mount> mounts = new WeakHashMap<>();
private static Mount currentMount = null;
private static Handler anvilUIHandler = null;
/** Renderable can be mounted and rendered using Anvil library. */
public interface Renderable {
/** This method is a place to define the structure of your layout, its view
* properties and data bindings. */
void view();
}
public interface ViewFactory {
View fromClass(Context c, Class<? extends View> v);
View fromXml(ViewGroup parent, int xmlId);
}
private final static List<ViewFactory> viewFactories = new ArrayList<ViewFactory>() {{
add(new DefaultViewFactory());
}};
final static class DefaultViewFactory implements ViewFactory {
public View fromClass(Context c, Class<? extends View> viewClass) {
try {
return viewClass.getConstructor(Context.class).newInstance(c);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
} catch (InstantiationException e) {
throw new RuntimeException(e);
}
}
public View fromXml(ViewGroup parent, int xmlId) {
return LayoutInflater.from(parent.getContext()).inflate(xmlId, parent, false);
}
};
public static void registerViewFactory(ViewFactory viewFactory) {
if (!viewFactories.contains(viewFactory)) {
viewFactories.add(0, viewFactory);
}
}
private Anvil() {}
private final static Runnable anvilRenderRunnable = new Runnable() {
public void run() {
Anvil.render();
}
};
public interface AttributeSetter<T> {
boolean set(View v, String name, T value, T prevValue);
}
private final static List<AttributeSetter> attributeSetters =
new ArrayList<AttributeSetter>() {{ add(new PropertySetter()); }};
public static void registerAttributeSetter(AttributeSetter setter) {
if (!attributeSetters.contains(setter)) {
attributeSetters.add(0, setter);
}
}
/** Tags: arbitrary data bound to specific views, such as last cached attribute values */
private static Map<View, Map<String, Object>> tags = new WeakHashMap<>();
public static void set(View v, String key, Object value) {
Map<String, Object> attrs = tags.get(v);
if (attrs == null) {
attrs = new HashMap<>();
tags.put(v, attrs);
}
attrs.put(key, value);
}
public static Object get(View v, String key) {
Map<String, Object> attrs = tags.get(v);
if (attrs == null) {
return null;
}
return attrs.get(key);
}
/** Starts the new rendering cycle updating all mounted
* renderables. Update happens in a lazy manner, only the values that has
* been changed since last rendering cycle will be actually updated in the
* views. This method can be called from any thread, so it's safe to use
* {@code Anvil.render()} in background services. */
public static void render() {
// If Anvil.render() is called on a non-UI thread, use UI Handler
if (Looper.myLooper() != Looper.getMainLooper()) {
synchronized (Anvil.class) {
if (anvilUIHandler == null) {
anvilUIHandler = new Handler(Looper.getMainLooper());
}
}
anvilUIHandler.removeCallbacksAndMessages(null);
anvilUIHandler.post(anvilRenderRunnable);
return;
}
Set<Mount> set = new HashSet<>();
set.addAll(mounts.values());
for (Mount m : set) {
render(m);
}
}
/**
* Mounts a renderable function defining the layout into a View. If host is a
* viewgroup it is assumed to be empty, so the Renderable would define what
* its child views would be.
* @param v a View into which the renderable r will be mounted
* @param r a Renderable to mount into a View
*/
public static <T extends View> T mount(T v, Renderable r) {
Mount m = new Mount(v, r);
mounts.put(v, m);
render(v);
return v;
}
/**
* Unmounts a mounted renderable. This would also clean up all the child
* views inside the parent ViewGroup, which acted as a mount point.
* @param v A mount point to unmount from its View
*/
public static void unmount(View v) {
unmount(v, true);
}
public static void unmount(View v, boolean removeChildren) {
Mount m = mounts.get(v);
if (m != null) {
mounts.remove(v);
if (v instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) v;
int childCount = viewGroup.getChildCount();
for (int i = 0; i < childCount; i++) {
unmount(viewGroup.getChildAt(i));
}
if (removeChildren) {
viewGroup.removeViews(0, childCount);
}
}
}
}
/**
* Returns currently rendered Mount point. Must be called from the
* Renderable's view() method, otherwise it returns null
* @return current mount point
*/
static Mount currentMount() {
return currentMount;
}
/**
* Returns currently rendered View. It allows to access the real view from
* inside the Renderable.
* @return currently rendered View
*/
@SuppressWarnings("unchecked")
public static <T extends View> T currentView() {
if (currentMount == null) {
return null;
}
return (T) currentMount.iterator.currentView();
}
public static void render(View v) {
Mount m = mounts.get(v);
if (m == null) {
return;
}
render(m);
}
static void render(Mount m) {
if (m.lock) {
return;
}
m.lock = true;
Mount prev = currentMount;
currentMount = m;
m.iterator.start();
if (m.renderable != null) {
m.renderable.view();
}
m.iterator.end();
currentMount = prev;
m.lock = false;
}
/** Mount describes a mount point. Mount point is a Renderable function
* attached to some ViewGroup. Mount point keeps track of the virtual layout
* declared by Renderable */
static class Mount {
private boolean lock = false;
private final WeakReference<View> rootView;
private final Renderable renderable;
final Iterator iterator = new Iterator();
Mount(View v, Renderable r) {
this.renderable = r;
this.rootView = new WeakReference<>(v);
}
@SuppressLint("Assert")
class Iterator {
Deque<View> views = new ArrayDeque<>();
Deque<Integer> indices = new ArrayDeque<>();
private void start() {
assert views.size() == 0;
assert indices.size() == 0;
indices.push(0);
View v = rootView.get();
if (v != null) {
views.push(v);
}
}
void start(Class<? extends View> c, int layoutId, Object key) {
int i = indices.peek();
View parentView = views.peek();
if (parentView == null) {
return;
}
if (!(parentView instanceof ViewGroup)) {
throw new RuntimeException("child views are allowed only inside view groups");
}
ViewGroup vg = (ViewGroup) parentView;
View v = null;
if (i < vg.getChildCount()) {
v = vg.getChildAt(i);
}
Context context = rootView.get().getContext();
if (c != null && (v == null || !v.getClass().equals(c))) {
vg.removeView(v);
for (ViewFactory vf : viewFactories) {
v = vf.fromClass(context, c);
if (v != null) {
set(v, "_anvil", 1);
vg.addView(v, i);
break;
}
}
} else if (c == null && (v == null || !Integer.valueOf(layoutId).equals(get(v, "_layoutId")))) {
vg.removeView(v);
for (ViewFactory vf : viewFactories) {
v = vf.fromXml(vg, layoutId);
if (v != null) {
set(v, "_anvil", 1);
set(v, "_layoutId", layoutId);
vg.addView(v, i);
break;
}
}
}
assert v != null;
views.push(v);
indices.push(indices.pop() + 1);
indices.push(0);
}
void end() {
int index = indices.peek();
View v = views.peek();
if (v != null && v instanceof ViewGroup &&
get(v, "_layoutId") == null &&
(mounts.get(v) == null || mounts.get(v) == Mount.this)) {
ViewGroup vg = (ViewGroup) v;
if (index < vg.getChildCount()) {
removeNonAnvilViews(vg, index, vg.getChildCount() - index);
}
}
indices.pop();
views.pop();
}
<T> void attr(String name, T value) {
View currentView = views.peek();
if (currentView == null) {
return;
}
@SuppressWarnings("unchecked")
T currentValue = (T) get(currentView, name);
if (currentValue == null || !currentValue.equals(value)) {
for (AttributeSetter setter : attributeSetters) {
if (setter.set(currentView, name, value, currentValue)) {
set(currentView, name, value);
return;
}
}
}
}
private void removeNonAnvilViews(ViewGroup vg, int start, int count) {
final int end = start + count - 1;
for (int i = end; i >= start; i--) {
View v = vg.getChildAt(i);
if (get(v, "_anvil") != null) {
vg.removeView(v);
}
}
}
public void skip() {
int i;
ViewGroup vg = (ViewGroup) views.peek();
for (i = indices.pop(); i < vg.getChildCount(); i++) {
View v = vg.getChildAt(i);
if (get(v, "_anvil") != null) {
indices.push(i);
return;
}
}
indices.push(i);
}
public View currentView() {
return views.peek();
}
}
}
}