Sunday, October 26, 2014

Invoking callbacks for multiple subscribers

How often do you need to invoke callbacks for a multiple subscribers? For example when you have few observer-like classes.

Consider foolowing code:

public void foo() {
  // do something
  for (Callback callback : mCallbacks) {
    callback.bar();
  }
  // do something
}

Personally, I hate to write code like this - too many lines of code that is mostly duplicate from other places. Even worst when you need to ensure UI thread execution and few more lines go to Handler...

So what is the solution? Most universal (and probably most elegant as well) solution that I've came up with is to use Reflections or to use Proxy to be more specific. Most people I ask didn't even know about existance of this class. In short it allows us to create a class in runtime which implements any number of interfaces. Yes, this adds some overhead but, frankly, this is quite small overhead if used somewhat sparingly and almost non-existantant when we add Handler.

So here is my callbacks invoker implementation:

public class Invoker<T> {

  public final T object;
  public final List callbacks = new ArrayList<>();

  private Invoker(T object) {
    this.object = object;
  }

  @SuppressWarnings("unchecked")
  public static <T> Invoker create(Class cls) {
    DirectInvocation<T> handler = new DirectInvocation<T>();
    Object object = Proxy.newProxyInstance(
        cls.getClassLoader(), new Class[] { cls }, handler
    );
    return handler.invoker = new Invoker<T>((T) object);
  }

  static class DirectInvocation<T> implements InvocationHandler {
    public Invoker<T> invoker;

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      for (Object object : invoker.callbacks.toArray()) {
        method.invoke(object, args);
      }
      return null;
    }
  }
}

And now some example how to use it:

...
Invoker<Callback> mInvoker = Invoker.create(Callback.class);
...
mInvoker.callbacks.add(...);
mInvoker.callbacks.add(...);
mInvoker.callbacks.add(...);
...

public void foo() {
  // do something
  mInvoker.object.bar(); // invokes all callbacks
  // do something
}

Now it looks much cleaner, In addition we can easily add Handlers, Threads or anything else to invoke out callbacks inside invoker itelf.

Saturday, October 18, 2014

EditText for CreditCards

Internet has tons of different implementations to add credit card support in Edit.
Most of them listen for text changes and add spaces and I don't like this approche. Mostly because user can see those space while selecting text.

So I will add mine implementation :)

Here is screenshot from Gennymotion:

And sources as well (link on GitHub):

public class CreditCardTransformation implements TransformationMethod {

  private static final Divider sDivider = new Divider();

  @Override
  public CharSequence getTransformation(final CharSequence source, View view) {
    return new SpannedWrapper(source) {
      @Override
      @SuppressWarnings("unchecked")
      public  T[] getSpans(int start, int end, Class type) {
        T[] array = super.getSpans(start, end, type);
        if (!type.isAssignableFrom(Divider.class)) {
          return array;
        }
        Object[] result = (Object[]) Array.newInstance(type, array.length + 1);
        result[0] = sDivider;
        System.arraycopy(array, 0, result, 1, array.length);
        return (T[]) result;
      }

      @Override
      public int getSpanStart(Object tag) {
        if (tag instanceof Divider) {
          return 0;
        }
        return super.getSpanStart(tag);
      }

      @Override
      public int getSpanEnd(Object tag) {
        if (tag instanceof Divider) {
          return length();
        }
        return super.getSpanEnd(tag);
      }
    };
  }

  @Override
  public void onFocusChanged(View view, CharSequence sourceText, boolean focused, int direction, Rect previouslyFocusedRect) {
  }

  static class SpannedWrapper implements Spanned {
    private Spanned mSpanned;
    private CharSequence mSource;

    SpannedWrapper(CharSequence source) {
      mSource = source;
      if (mSource instanceof Spanned) {
        mSpanned = (Spanned) source;
      }
    }

    @Override
    @SuppressWarnings("unchecked")
    public  T[] getSpans(int start, int end, Class type) {
      if (mSpanned != null) {
        return mSpanned.getSpans(start, end, type);
      }
      return (T[]) Array.newInstance(type, 0);
    }

    @Override
    public int getSpanStart(Object tag) {
      if (mSpanned != null) {
        return mSpanned.getSpanStart(tag);
      }
      return 0;
    }

    @Override
    public int getSpanEnd(Object tag) {
      if (mSpanned != null) {
        return mSpanned.getSpanEnd(tag);
      }
      return 0;
    }

    @Override
    public int getSpanFlags(Object tag) {
      if (mSpanned != null) {
        return mSpanned.getSpanFlags(tag);
      }
      return 0;
    }

    @Override
    public int nextSpanTransition(int start, int limit, Class type) {
      if (mSpanned != null) {
        return mSpanned.nextSpanTransition(start, limit, type);
      }
      return 0;
    }

    @Override
    public int length() {
      return mSource.length();
    }

    @Override
    public char charAt(int index) {
      return mSource.charAt(index);
    }

    @Override
    public CharSequence subSequence(int start, int end) {
      return mSource.subSequence(start, end);
    }

    @Override
    public String toString() {
      return mSource.toString();
    }
  }

  static class Divider extends ReplacementSpan {
    @Override
    public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
      int groups = Math.min(3, (end - start) / 4);
      return (int) (paint.measureText(text, start, end) + paint.getTextSize() * groups);
    }

    @Override
    public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
      int offset = 0;
      int groupStart = start;
      while (groupStart < end) {
        int groupEnd = Math.min(groupStart + 4, end);
        canvas.drawText(text, groupStart, groupEnd, x + offset, y, paint);
        offset += paint.measureText(text, groupStart, groupEnd) + paint.getTextSize();
        groupStart = groupEnd;
      }
    }
  }
}