package trikita.anvil; import android.animation.Animator; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.text.Editable; import android.text.TextWatcher; import android.util.TypedValue; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.AutoCompleteTextView; import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.RadioGroup; import android.widget.RelativeLayout; import android.widget.SeekBar; import android.widget.TextSwitcher; import android.widget.TextView; import java.util.AbstractMap; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.WeakHashMap; public class BaseDSL implements Anvil.AttributeSetter { static { Anvil.registerAttributeSetter(new BaseDSL()); } // // Common attrs // // weight constants public final static int FILL = ViewGroup.LayoutParams.MATCH_PARENT; public final static int MATCH = ViewGroup.LayoutParams.MATCH_PARENT; public final static int WRAP = ViewGroup.LayoutParams.WRAP_CONTENT; // gravity constants public final static int TOP = Gravity.TOP; public final static int BOTTOM = Gravity.BOTTOM; public final static int LEFT = Gravity.LEFT; public final static int RIGHT = Gravity.RIGHT; public final static int CENTER_VERTICAL = Gravity.CENTER_VERTICAL; public final static int GROW_VERTICAL = Gravity.FILL_VERTICAL; public final static int CENTER_HORIZONTAL = Gravity.CENTER_HORIZONTAL; public final static int GROW_HORIZONTAL = Gravity.FILL_HORIZONTAL; public final static int CENTER = CENTER_VERTICAL | CENTER_HORIZONTAL; public final static int GROW = GROW_VERTICAL | GROW_HORIZONTAL; public final static int CLIP_VERTICAL = Gravity.CLIP_VERTICAL; public final static int CLIP_HORIZONTAL = Gravity.CLIP_HORIZONTAL; public final static int START = Gravity.START; public final static int END = Gravity.END; // // Helper DSL methods, not related to any specific attributes // public static final class ViewClassResult { } public static ViewClassResult skip() { Anvil.currentMount().iterator.skip(); return null; } public static ViewClassResult v(Class<? extends View> c) { Anvil.currentMount().iterator.start(c, 0, null); return null; } public static ViewClassResult xml(int layoutId) { Anvil.currentMount().iterator.start(null, layoutId, null); return null; } private static Void end() { Anvil.currentMount().iterator.end(); return null; } public static Void o(ViewClassResult c, Object... args) { return end(); } public static Void v(Class<? extends View> c, Anvil.Renderable r) { v(c); r.view(); return end(); } public static Void xml(int layoutId, Anvil.Renderable r) { xml(layoutId); r.view(); return end(); } public static <T> Void attr(String name, T value) { Anvil.currentMount().iterator.attr(name, value); return null; } public static View withId(int id, Anvil.Renderable r) { View v = Anvil.currentView(); if (v == null) { throw new RuntimeException("Anvil.currentView() is null"); } // FIXME: how to mock this for tests? v = v.findViewById(id); if (v == null) { throw new RuntimeException("No view found for ID " + id); } return Anvil.mount(v, r); } public static Resources R() { return Anvil.currentView().getResources(); } public static boolean isPortrait() { return R().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT; } public static float dip(float value) { return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, R().getDisplayMetrics()); } public static int dip(int value) { return Math.round(dip((float) value)); } public static float sip(float value) { return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, value, R().getDisplayMetrics()); } public static int sip(int value) { return Math.round(sip((float) value)); } // // Attribute setters // public static Void size(int w, int h) { return attr("size", new AbstractMap.SimpleImmutableEntry<>(w, h)); } public static Void padding(int p) { return padding(p, p, p, p); } public static Void padding(int h, int v) { return padding(h, v, h, v); } public static Void padding(int l, int t, int r, int b) { List<Integer> list = new ArrayList<>(4); list.add(l); list.add(t); list.add(r); list.add(b); return attr("padding", list); } public static Void margin(int w) { return margin(w, w, w, w); } public static Void margin(int h, int v) { return margin(h, v, h, v); } public static Void margin(int l, int t, int r, int b) { List<Integer> list = new ArrayList<>(4); list.add(l); list.add(t); list.add(r); list.add(b); return attr("margin", list); } public static Void weight(float w) { return attr("weight", w); } public static Void layoutGravity(int g) { return attr("layoutGravity", g); } public static Void align(int verb) { return align(verb, -1); } public static Void above(int anchor) { return align(RelativeLayout.ABOVE, anchor); } public static Void alignBaseline(int anchor) { return align(RelativeLayout.ALIGN_BASELINE, anchor); } public static Void alignBottom(int anchor) { return align(RelativeLayout.ALIGN_BOTTOM, anchor); } public static Void alignEnd(int anchor) { return align(RelativeLayout.ALIGN_END, anchor); } public static Void alignLeft(int anchor) { return align(RelativeLayout.ALIGN_LEFT, anchor); } public static Void alignParentBottom() { return align(RelativeLayout.ALIGN_PARENT_BOTTOM, RelativeLayout.TRUE); } public static Void alignParentEnd() { return align(RelativeLayout.ALIGN_PARENT_END, RelativeLayout.TRUE); } public static Void alignParentLeft() { return align(RelativeLayout.ALIGN_PARENT_LEFT, RelativeLayout.TRUE); } public static Void alignParentRight() { return align(RelativeLayout.ALIGN_PARENT_RIGHT, RelativeLayout.TRUE); } public static Void alignParentStart() { return align(RelativeLayout.ALIGN_PARENT_START, RelativeLayout.TRUE); } public static Void alignParentTop() { return align(RelativeLayout.ALIGN_PARENT_TOP, RelativeLayout.TRUE); } public static Void alignRight(int anchor) { return align(RelativeLayout.ALIGN_RIGHT, anchor); } public static Void alignStart(int anchor) { return align(RelativeLayout.ALIGN_START, anchor); } public static Void alignTop(int anchor) { return align(RelativeLayout.ALIGN_TOP, anchor); } public static Void below(int anchor) { return align(RelativeLayout.BELOW, anchor); } public static Void centerHorizontal() { return align(RelativeLayout.CENTER_HORIZONTAL, RelativeLayout.TRUE); } public static Void centerInParent() { return align(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE); } public static Void centerVertical() { return align(RelativeLayout.CENTER_VERTICAL, RelativeLayout.TRUE); } public static Void toEndOf(int anchor) { return align(RelativeLayout.END_OF, anchor); } public static Void toLeftOf(int anchor) { return align(RelativeLayout.LEFT_OF, anchor); } public static Void toRightOf(int anchor) { return align(RelativeLayout.RIGHT_OF, anchor); } public static Void toStartOf(int anchor) { return align(RelativeLayout.START_OF, anchor); } public static Void align(int verb, int anchor) { return attr("align", new AbstractMap.SimpleImmutableEntry<>(verb, anchor)); } public static Void textSize(float size) { return attr("textSize", size); } public static Void typeface(String font) { return attr("typeface", font); } public static Void typeface(String font, int style) { return attr("styledTypeface", new AbstractMap.SimpleImmutableEntry<>(font, style)); } public static Void compoundDrawables(Drawable l, Drawable t, Drawable r, Drawable b) { List<Drawable> list = new ArrayList<>(4); list.add(l); list.add(t); list.add(r); list.add(b); return attr("compoundDrawables", list); } public static Void compoundDrawablesWithIntrinsicBounds(Drawable l, Drawable t, Drawable r, Drawable b) { List<Drawable> list = new ArrayList<>(4); list.add(l); list.add(t); list.add(r); list.add(b); return attr("compoundDrawablesWithIntrinsicBounds", list); } public static Void compoundDrawablesWithIntrinsicBoundsResource(int l, int t, int r, int b) { List<Integer> list = new ArrayList<>(4); list.add(l); list.add(t); list.add(r); list.add(b); return attr("compoundDrawablesWithIntrinsicBoundsResource", list); } public static Void visibility(boolean visible) { if (visible) { return trikita.anvil.DSL.visibility(View.VISIBLE); } else { return trikita.anvil.DSL.visibility(View.GONE); } } public static Void check(int id) { return attr("check", id); } public static Void tag(int id, Object value) { return attr("tag", new AbstractMap.SimpleImmutableEntry<>(id, value)); } public static Void shadowLayer(float radius, float dx, float dy, int color) { List<Number> list = new ArrayList<>(4); list.add(radius); list.add(dx); list.add(dy); list.add(color); return attr("shadowLayer", list); } public static Void onItemSelected(SimpleItemSelectedListener l) { return attr("onItemSelected", l); } public static Void onSeekBarChange(SimpleSeekBarChangeListener l) { return attr("onSeekBarChange", l); } public static Void anim(boolean trigger, Animator a) { return attr("anim", new AnimatorPair(a, trigger)); } public static Void init(Runnable r) { return attr("init", r); } public static Void onTextChanged(SimpleTextWatcher w) { return attr("onTextChanged", w); } public static Void onTextChanged(TextWatcher w) { return attr("onTextChanged", w); } public static Void text(CharSequence arg) { return attr("text", arg); } // // Simple lambda-friendly interface wrappers for data bindings // public interface SimpleTextWatcher { void onTextChanged(CharSequence s); } public interface SimpleSeekBarChangeListener { void onSeekBarChange(SeekBar s, int progress, boolean fromUser); } public interface SimpleItemSelectedListener { void onItemSelected(AdapterView a, View v, int pos, long id); } // // Helper classes for data bindings // private static class TextWatcherProxy implements TextWatcher { private final static Map<TextWatcherProxy, Void> TEXT_WATCHERS = new WeakHashMap<>(); private static TextView CURRENT_INPUT_TEXT_VIEW = null; private final TextView v; private TextWatcher watcher; private SimpleTextWatcher simpleWatcher; private String text = ""; public TextWatcherProxy(TextView v) { this.v = v; } public TextWatcherProxy setImpl(TextWatcher w) { this.watcher = w; this.simpleWatcher = null; return this; } public TextWatcherProxy setImpl(SimpleTextWatcher w) { this.simpleWatcher = w; this.watcher = null; return this; } public boolean hasImpl(Object o) { if (o == null) { return false; } return o.equals(this.watcher) || o.equals(this.simpleWatcher); } public void afterTextChanged(Editable s) { if (this.watcher != null) { this.watcher.afterTextChanged(s); } } public void beforeTextChanged(CharSequence s, int from, int n, int after) { if (this.watcher != null) { this.watcher.beforeTextChanged(s, from, n, after); } } public void onTextChanged(CharSequence s, int from, int before, int n) { TextView old = CURRENT_INPUT_TEXT_VIEW; CURRENT_INPUT_TEXT_VIEW = this.v; if (this.text.equals(s.toString()) == false) { if (this.watcher != null) { this.watcher.onTextChanged(s, from, before, n); } if (this.simpleWatcher != null) { this.simpleWatcher.onTextChanged(s); } this.text = s.toString(); Anvil.render(); } CURRENT_INPUT_TEXT_VIEW = old; } } private final static class ItemSelectedWrapper implements AdapterView.OnItemSelectedListener { private final SimpleItemSelectedListener parent; private ItemSelectedWrapper(SimpleItemSelectedListener parent) { this.parent = parent; } public void onItemSelected(AdapterView a0, View a1, int a2, long a3) { parent.onItemSelected(a0, a1, a2, a3); Anvil.render(); } public void onNothingSelected(AdapterView a0) { } public int hashCode() { return parent.hashCode(); } public boolean equals(Object o) { return parent.equals(o); } } private final static class SeekBarChangeWrapper implements SeekBar.OnSeekBarChangeListener { private final SimpleSeekBarChangeListener parent; private SeekBarChangeWrapper(SimpleSeekBarChangeListener parent) { this.parent = parent; } public void onProgressChanged(SeekBar a0, int a1, boolean a2) { parent.onSeekBarChange(a0, a1, a2); Anvil.render(); } public void onStartTrackingTouch(SeekBar a0) { } public void onStopTrackingTouch(SeekBar a0) { } public int hashCode() { return parent.hashCode(); } public boolean equals(Object o) { return parent.equals(o); } } private final static class AnimatorPair { public Animator animator; public final boolean trigger; public AnimatorPair(Animator a, boolean trigger) { this.animator = a; this.trigger = trigger; } public int hashCode() { return trigger ? 0 : 1; } public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) { return false; } AnimatorPair pair = (AnimatorPair) o; if (pair.trigger != this.trigger) { if (pair.animator != null && pair.animator.isRunning()) { pair.animator.cancel(); } return false; } this.animator = pair.animator; return true; } } @Override public boolean set(View v, String name, Object value, Object prevValue) { switch (name) { case "init": if (Anvil.get(v, "_initialized") == null) { Anvil.set(v, "_initialized", true); ((Runnable) value).run(); } return true; case "anim": AnimatorPair a = (AnimatorPair) value; if (a.trigger) { a.animator.setTarget(v); a.animator.start(); } return true; case "onSeekBarChange": if (v instanceof SeekBar && value instanceof SimpleSeekBarChangeListener) { ((SeekBar) v).setOnSeekBarChangeListener( new SeekBarChangeWrapper((SimpleSeekBarChangeListener) value)); return true; } break; case "onItemSelected": if (v instanceof AdapterView && value instanceof SimpleItemSelectedListener) { ((AdapterView) v).setOnItemSelectedListener(new ItemSelectedWrapper((SimpleItemSelectedListener) value)); return true; } else if (v instanceof AutoCompleteTextView && value instanceof SimpleItemSelectedListener) { ((AutoCompleteTextView) v).setOnItemSelectedListener(new ItemSelectedWrapper((SimpleItemSelectedListener) value)); return true; } break; case "text": if (v instanceof TextView && value instanceof CharSequence) { if (v != TextWatcherProxy.CURRENT_INPUT_TEXT_VIEW) { ((TextView) v).setText((CharSequence) value); } return true; } else if (v instanceof TextSwitcher && value instanceof CharSequence) { ((TextSwitcher) v).setText((CharSequence) value); return true; } break; case "onTextChanged": if (v instanceof TextView && value instanceof SimpleTextWatcher) { TextView tv = (TextView) v; for (TextWatcherProxy proxy : TextWatcherProxy.TEXT_WATCHERS.keySet()) { if (proxy.hasImpl(prevValue)) { proxy.setImpl((SimpleTextWatcher) value); return true; } } TextWatcherProxy proxy = new TextWatcherProxy(tv).setImpl((SimpleTextWatcher) value); TextWatcherProxy.TEXT_WATCHERS.put(proxy, null); tv.addTextChangedListener(proxy); return true; } if (v instanceof TextView && value instanceof TextWatcher) { TextView tv = (TextView) v; for (TextWatcherProxy proxy : TextWatcherProxy.TEXT_WATCHERS.keySet()) { if (proxy.hasImpl(prevValue)) { proxy.setImpl((TextWatcher) value); return true; } } TextWatcherProxy proxy = new TextWatcherProxy(tv).setImpl((TextWatcher) value); TextWatcherProxy.TEXT_WATCHERS.put(proxy, null); tv.addTextChangedListener(proxy); return true; } break; case "tag": if (value instanceof AbstractMap.SimpleImmutableEntry) { AbstractMap.SimpleImmutableEntry p = (AbstractMap.SimpleImmutableEntry) value; v.setTag((Integer) p.getKey(), p.getValue()); return true; } break; case "size": if (value instanceof AbstractMap.SimpleImmutableEntry) { AbstractMap.SimpleImmutableEntry arg = (AbstractMap.SimpleImmutableEntry) value; ViewGroup.LayoutParams p = v.getLayoutParams(); p.width = (int) arg.getKey(); p.height = (int) arg.getValue(); v.setLayoutParams(p); return true; } break; case "check": if (v instanceof RadioGroup) { ((RadioGroup) v).check((Integer) value); return true; } break; case "shadowLayer": if (v instanceof TextView && value instanceof List) { List<Number> arg = (List<Number>) value; ((TextView) v).setShadowLayer(arg.get(0).floatValue(), arg.get(1).floatValue(), arg.get(2).floatValue(), arg.get(3).intValue()); return true; } break; case "padding": if (value instanceof List) { List<Integer> arg = (List<Integer>) value; v.setPadding(arg.get(0), arg.get(1), arg.get(2), arg.get(3)); return true; } break; case "margin": if (v.getLayoutParams() instanceof ViewGroup.MarginLayoutParams && value instanceof List) { List<Integer> arg = (List<Integer>) value; ViewGroup.MarginLayoutParams mp = (ViewGroup.MarginLayoutParams) v.getLayoutParams(); mp.leftMargin = arg.get(0); mp.topMargin = arg.get(1); mp.rightMargin = arg.get(2); mp.bottomMargin = arg.get(3); v.setLayoutParams(mp); return true; } break; case "weight": if (v.getLayoutParams() instanceof LinearLayout.LayoutParams && value instanceof Float) { ((LinearLayout.LayoutParams) v.getLayoutParams()).weight = (float) value; return true; } break; case "layoutGravity": if (v.getLayoutParams() instanceof LinearLayout.LayoutParams && value instanceof Integer) { ((LinearLayout.LayoutParams) v.getLayoutParams()).gravity = (int) value; return true; } else if (v.getLayoutParams() instanceof FrameLayout.LayoutParams && value instanceof Integer) { ((FrameLayout.LayoutParams) v.getLayoutParams()).gravity = (int) value; return true; } break; case "align": if (v.getLayoutParams() instanceof RelativeLayout.LayoutParams) { AbstractMap.SimpleImmutableEntry<Integer, Integer> e = (AbstractMap.SimpleImmutableEntry<Integer, Integer>) value; ((RelativeLayout.LayoutParams) v.getLayoutParams()).addRule(e.getKey(), e.getValue()); return true; } break; case "textSize": if (v instanceof TextView && value instanceof Float) { ((TextView) v).setTextSize(TypedValue.COMPLEX_UNIT_PX, (Float) value); return true; } break; case "typeface": if (v instanceof TextView && value instanceof String) { ((TextView) v).setTypeface(Typeface.createFromAsset(v.getContext().getAssets(), (String) value)); return true; } break; case "styledTypeface": if (v instanceof TextView && value instanceof AbstractMap.SimpleImmutableEntry) { AbstractMap.SimpleImmutableEntry<String, Integer> e = (AbstractMap.SimpleImmutableEntry<String, Integer>) value; Typeface typeface = null; if (e.getKey() != null) { typeface = Typeface.createFromAsset(v.getContext().getAssets(), e.getKey()); } ((TextView) v).setTypeface(typeface, e.getValue()); return true; } break; case "compoundDrawables": if (v instanceof TextView && value instanceof List) { List<Drawable> l = (List<Drawable>) value; ((TextView) v).setCompoundDrawables(l.get(0), l.get(1), l.get(2), l.get(3)); return true; } break; case "compoundDrawablesWithIntrinsicBounds": if (v instanceof TextView && value instanceof List) { List<Drawable> l = (List<Drawable>) value; ((TextView) v).setCompoundDrawablesWithIntrinsicBounds(l.get(0), l.get(1), l.get(2), l.get(3)); return true; } break; case "compoundDrawablesWithIntrinsicBoundsResource": if (v instanceof TextView && value instanceof List) { List<Integer> l = (List<Integer>) value; ((TextView) v).setCompoundDrawablesWithIntrinsicBounds(l.get(0), l.get(1), l.get(2), l.get(3)); return true; } break; } return false; } }