click tracking

Fun with
Android Shaders and Filters

by Chiu-Ki Chan

@chiuki
+ChiuKiChan

Paint

Paint

Shader

LinearGradient, RadialGradient, SweepGradient, BitmapGradient, ComposeGradient


ColorFilter

LightingColorFilter, ColorMatrixColorFilter, PorterDuffColorFilter


MaskFilter

BlurMaskFilter, EmbossMaskFilter

Gradient

From XML


<shape
    xmlns:android="http://schemas.android.com/apk/res/android"
    type="rectangle">
  <gradient
    android:startColor="@color/blue"
    android:centerColor="@color/white"
    android:endColor="@color/red"
    android:angle="45"/>
</shape>
              
LinearGradient extends Shader
LinearGradient from XML

More colors


public class RainbowTextView extends TextView {
  protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);

    int[] rainbow = getRainbowColors();
    Shader shader = new LinearGradient(0, 0, 0, w, rainbow, 
        null, Shader.TileMode.MIRROR);

    Matrix matrix = new Matrix();
    matrix.setRotate(90);
    shader.setLocalMatrix(matrix);

    getPaint().setShader(shader);
  }
              
  private int[] getRainbowColors() {
    return new int[] {
      getResources().getColor(R.color.rainbow_red),
      getResources().getColor(R.color.rainbow_yellow),
      getResources().getColor(R.color.rainbow_green),
      getResources().getColor(R.color.rainbow_blue),
      getResources().getColor(R.color.rainbow_purple)
    };
  }
}
Rainbow LinearGradient

BitmapShader

From XML


<bitmap
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:src="@drawable/cheetah_tile"
    android:tileMode="repeat"/>
              
BitmapShader extends Shader
BitmapShader from XML

Patterned Text


Bitmap bitmap = BitmapFactory.decodeResource(
    getResources(), R.drawable.cheetah_tile);
Shader shader = new BitmapShader(bitmap, 
    Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
textView.getPaint().setShader(shader);
              
TileMode.CLAMP
CLAMP
TileMode.REPEAT
REPEAT
TileMode.MIRROR
MIRROR
TextView with BitmapShader

Peek through

BitmapShader on touch area

Peek through: onTouchEvent


public class PeekThroughImageView extends ImageView {
  private final float radius;
  private Paint paint = null;

  private float x;
  private float y;
  private boolean shouldDrawOpening = false;

  public boolean onTouchEvent(MotionEvent motionEvent) {
    int action = motionEvent.getAction();
    shouldDrawOpening = (action == MotionEvent.ACTION_DOWN || 
      action == MotionEvent.ACTION_MOVE);
    x = motionEvent.getX();
    y = motionEvent.getY();
    invalidate();
    return true;
  }
}
              
BitmapShader on touch area

Peek through: onDraw


protected void onDraw(Canvas canvas) {
  if (paint == null) {
    Bitmap original = Bitmap.createBitmap(
        getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
    Canvas originalCanvas = new Canvas(original);
    super.onDraw(originalCanvas);    // ImageView

    Shader shader = new BitmapShader(original, 
        Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);

    paint = new Paint();
    paint.setShader(shader);
  }

  canvas.drawColor(Color.GRAY);
  if (shouldDrawOpening) {
    canvas.drawCircle(x, y - radius, radius, paint);
  }
}
              
BitmapShader on touch area

ColorFilter

ColorMatrix


[ a, b, c, d, e,
  f, g, h, i, j,
  k, l, m, n, o,
  p, q, r, s, t ]
          

Apply to [R, G, B, A], after clamping:


R' = a*R + b*G + c*B + d*A + e;
G' = f*R + g*G + h*B + i*A + j;
B' = k*R + l*G + m*B + n*A + o;
A' = p*R + q*G + r*B + s*A + t;
          

Identity:


[ R,     [ 1, 0, 0, 0, 0,    [ R,
  G,  *    0, 1, 0, 0, 0,  =   G,
  B,       0, 0, 1, 0, 0,      B,
  A ]      0, 0, 0, 1, 0 ]     A ]
            

ColorMatrix: Grayscale


Bitmap bitmap = Bitmap.createBitmap(original.getWidth(), 
    original.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);

Paint paint = new Paint();
paint.setColorFilter(new ColorMatrixColorFilter(
    getColorMatrix()));
canvas.drawBitmap(original, 0, 0, paint);

return bitmap;
              

private ColorMatrix getColorMatrix() {
  ColorMatrix colorMatrix = new ColorMatrix();
  colorMatrix.setSaturation(0);
  return colorMatrix;
}
              
colorMatrix.setSaturation(0)
 
[ 0.213, 0.715, 0.072, 0, 0,
  0.213, 0.715, 0.072, 0, 0, 
  0.213, 0.715, 0.072, 0, 0,
      0,     0,     0, 1, 0 ]

// 0.213 + 0.715 + 0.072 = 1
                
ColorMatrix: Grayscale

ColorMatrix: Sepia


private ColorMatrix getColorMatrix() {
  ColorMatrix colorMatrix = new ColorMatrix();
  colorMatrix.setSaturation(0);

  ColorMatrix colorScale = new ColorMatrix();
  colorScale.setScale(1, 1, 0.8f, 1);

  // Convert to grayscale, then apply brown color
  colorMatrix.postConcat(colorScale);

  return colorMatrix;
}
              
colorMatrix.setScale(1, 1, 0.8f, 1)
 
[ 1, 0,   0, 0, 0,
  0, 1,   0, 0, 0, 
  0, 0, 0.8, 0, 0,
  0, 0,   0, 1, 0 ]
                
ColorMatrix: Sepia

ColorMatrix: Binary


private ColorMatrix getColorMatrix() {
  ColorMatrix colorMatrix = new ColorMatrix();
  colorMatrix.setSaturation(0);

  float m = 255f;
  float t = -255*128f;
  ColorMatrix threshold = new ColorMatrix(new float[] {
      m, 0, 0, 1, t,
      0, m, 0, 1, t,
      0, 0, m, 1, t,
      0, 0, 0, 1, 0
  });

  // Convert to grayscale, then scale and clamp
  colorMatrix.postConcat(threshold);

  return colorMatrix;
}
              
ColorMatrix: Binary

ColorMatrix: Invert


private ColorMatrix getColorMatrix() {
  return new ColorMatrix(new float[] {
      -1,  0,  0,  0, 255,
       0, -1,  0,  0, 255,
       0,  0, -1,  0, 255,
       0,  0,  0,  1,   0
  });
}
              
ColorMatrix: Invert

ColorMatrix: Alpha blue


private ColorMatrix getColorMatrix() {
  return new ColorMatrix(new float[] {
         0,    0,    0, 0,   0,
      0.3f,    0,    0, 0,  50,
         0,    0,    0, 0, 255,
      0.2f, 0.4f, 0.4f, 0, -30
  });
}
              
ColorMatrix: Alpha blue

ColorMatrix: Alpha pink


private ColorMatrix getColorMatrix() {
  return new ColorMatrix(new float[] {
         0,    0,    0, 0, 255,
         0,    0,    0, 0,   0,
      0.2f,    0,    0, 0,  50,
      0.2f, 0.2f, 0.2f, 0, -20
  });
}
              
ColorMatrix: Alpha pink

LightingColorFilter


/** Create a colorfilter that multiplies the RGB channels by one color, 
    and then adds a second color. */
LightingColorFilter(int mul, int add)
          

R' = R * mul.R + add.R
G' = G * mul.G + add.G
B' = B * mul.B + add.B
          

Little brother of ColorMatrixColorFilter


[ mul.R,     0,     0, 0, add.R
      0, mul.G,     0, 0, add.G,
      0,     0, mul.B, 0, add.B,
      0,     0,     0, 1,     0 ]
            

mul.R = Color.red(mul) / 255f

e.g. #ff0000 → 0xff / 255 = 255 / 255 = 1

LightingColorFilter

Four colored quarters

Four colored quarters

Four colored quarters


public class FourColorsImageView extends ImageView {
  private Bitmap bitmap = null;

  protected void onDraw(Canvas canvas) {
    if (bitmap == null) {
      Bitmap quarter = Bitmap.createBitmap(
          getWidth()/2, getHeight()/2, Bitmap.Config.ARGB_8888);
      Canvas quarterCanvas = new Canvas(quarter);
      quarterCanvas.scale(0.5f, 0.5f);
      super.onDraw(quarterCanvas);
      quarterCanvas.scale(2, 2);

      createBitmap(quarter);
      quarter.recycle();
    }

    canvas.drawBitmap(bitmap, 0, 0, null);
  }
}
              
Quarter size

Four colored quarters


private void createBitmap(Bitmap quarter) {
  bitmap = Bitmap.createBitmap(
    getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
  Canvas canvas = new Canvas(bitmap);

  Paint paint = new Paint();

  // Top left
  paint.setColorFilter(new LightingColorFilter(Color.RED, 0));
  canvas.drawBitmap(quarter, 0, 0, paint);

  // Top right
  paint.setColorFilter(new LightingColorFilter(Color.YELLOW, 0));
  canvas.drawBitmap(quarter, getWidth()/2, 0, paint);

  // Bottom left
  paint.setColorFilter(new LightingColorFilter(Color.BLUE, 0));
  canvas.drawBitmap(quarter, 0, getHeight()/2, paint);

  // Bottom right
  paint.setColorFilter(new LightingColorFilter(Color.GREEN, 0));
  canvas.drawBitmap(quarter, getWidth()/2, getHeight()/2, paint);
}
              
LightingColorFilter, 4 colors
Porter-Duff Modes

Porter-Duff Modes

Circle dim around

Circle dim around

Circle Mask

Original

DST
(what was already there)

Mask

SRC
(what we are drawing)

DST_IN


Bitmap bitmap = Bitmap.createBitmap(
    original.getWidth(), original.getHeight(), 
    Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);

// Draw the original bitmap (DST during Porter-Duff transfer)
canvas.drawBitmap(original, 0, 0, null);

// DST_IN = Whatever was there, keep the part that overlaps 
// with what I'm drawing now
Paint maskPaint = new Paint();
maskPaint.setXfermode(
    new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
canvas.drawBitmap(mask, 0, 0, maskPaint);
              
Original

DST
(what was already there)

Mask

SRC
(what we are drawing)

DST_IN

Dim around

Mask with DST_IN

DST
(what was already there)

Dimmed original

SRC
(what we are drawing)

DST_OVER


Paint overPaint = new Paint();

// DST_OVER = Whatever was there (DST), put it over what
// we are drawing now
overPaint.setXfermode(
    new PorterDuffXfermode(PorterDuff.Mode.DST_OVER));

// Draw original (SRC) with dim filter
overPaint.setColorFilter(createDimFilter());

canvas.drawBitmap(original, 0, 0, overPaint);
              
Mask with DST_IN

DST
(what was already there)

Dimmed original

SRC
(what we are drawing)

Circle dim around

DST_OVER


private ColorFilter createDimFilter() {
  ColorMatrix colorMatrix = new ColorMatrix();
  colorMatrix.setSaturation(0f);
  float scale = 0.5f;
  colorMatrix.setScale(scale, scale, scale, 1f);
  return new ColorMatrixColorFilter(colorMatrix);
}
              
Mask with DST_IN

DST
(what was already there)

Dimmed original

SRC
(what we are drawing)

DST_IN

PathEffect

Hollow Text


int strokeWidth = getResources().getDimensionPixelSize(
  R.dimen.dashed_text_stroke_width);
final HollowSpan span = new HollowSpan(strokeWidth);

String text = textView.getText().toString();
final SpannableString spannableString = new SpannableString(text);
spannableString.setSpan(span, 0, text.length(), 0);
              

private static class HollowSpan extends ReplacementSpan {
  private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
  private final Path path = new Path();
  private int width;

  public HollowSpan(int strokeWidth) {
    paint.setStyle(Paint.Style.STROKE);
    paint.setStrokeWidth(strokeWidth);
  }

  public void setPathEffect(PathEffect effect) {
    paint.setPathEffect(effect);
  }
}
              
Hollow text

Paint.getTextPath()


public int getSize(Paint paint, CharSequence text, 
    int start, int end, Paint.FontMetricsInt fm) {
  this.paint.setColor(paint.getColor());

  width = (int) (paint.measureText(text, start, end) + 
      this.paint.getStrokeWidth());
  return width;
}

public void draw(
    Canvas canvas, CharSequence text, int start, int end,
    float x, int top, int y, int bottom, Paint paint) {
  path.reset();
  paint.getTextPath(text.toString(), start, end, x, y, path);
  path.close();

  canvas.translate(this.paint.getStrokeWidth() / 2, 0);
  canvas.drawPath(path, this.paint);
  canvas.translate(-this.paint.getStrokeWidth() / 2, 0);
}
              
Hollow text

DashPathEffect


PathEffect dash = new DashPathEffect(
    new float[] { strokeWidth * 3, strokeWidth }, 0);
span.setPathEffect(dash);
              

private static class HollowSpan extends ReplacementSpan {
  private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
  private final Path path = new Path();
  private int width;

  public HollowSpan(int strokeWidth) {
    paint.setStyle(Paint.Style.STROKE);
    paint.setStrokeWidth(strokeWidth);
  }

  public void setPathEffect(PathEffect effect) {
    paint.setPathEffect(effect);
  }
 
  // In draw, canvas.drawPath(path, this.paint);
}
              
Hollow text with dashes

ComposePathEffect


PathEffect dash = new DashPathEffect(
    new float[] { strokeWidth * 3, strokeWidth }, 0);
PathEffect corner = new CornerPathEffect(strokeWidth);
PathEffect effect = new ComposePathEffect(dash, corner);
span.setPathEffect(effect);
              
Hollow text with round dashes

PathDashPathEffect


private PathEffect getTrianglePathEffect(int strokeWidth) {
  return new PathDashPathEffect(
      getTriangle(strokeWidth),
      strokeWidth,
      0.0f,
      PathDashPathEffect.Style.ROTATE);
}

private Path getTriangle(float size) {
  Path path = new Path();
  float half = size / 2;
  path.moveTo(-half, -half);
  path.lineTo(half, -half);
  path.lineTo(0, half);
  path.close();
  return path;
}
              
Hollow text with triangles

MaskFilter

EmbossMaskFilter


private void applyFilter(
    TextView textView, float[] direction, float ambient, 
    float specular, float blurRadius) {
  if (Build.VERSION.SDK_INT >= 11) {
    textView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
  }
  EmbossMaskFilter filter = new EmbossMaskFilter(
    direction, ambient, specular, blurRadius);
  textView.getPaint().setMaskFilter(filter);
}
              

setLayerType(View.LAYER_TYPE_SOFTWARE, null);
(Not supported by hardware acceleration)


applyFilter(emboss, new float[] { 0f, 1f, 0.5f }, 0.8f, 3f, 3f);
              

applyFilter(deboss, new float[] { 0f, -1f, 0.5f }, 0.8f, 15f, 1f);
              
EmbossMaskFilter

BlurMaskFilter


private void applyFilter(
    TextView textView, BlurMaskFilter.Blur style) {
  if (Build.VERSION.SDK_INT >= 11) {
    textView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
  }
  textView.setText(style.name());
  float radius = textView.getTextSize() / 10;
  BlurMaskFilter filter = new BlurMaskFilter(radius, style);
  textView.getPaint().setMaskFilter(filter);
}
              
BlurMaskFilter

(Who is afraid of)
RenderScript

ScriptIntrinsic

@TargetApi(17)

ScriptIntrinsicBlur


private Bitmap blur(Bitmap original, float radius) {
  Bitmap bitmap = Bitmap.createBitmap(
      original.getWidth(), original.getHeight(),
      Bitmap.Config.ARGB_8888);

  RenderScript rs = RenderScript.create(this);

  Allocation allocIn = Allocation.createFromBitmap(rs, original);
  Allocation allocOut = Allocation.createFromBitmap(rs, bitmap);

  ScriptIntrinsicBlur blur = ScriptIntrinsicBlur.create(
      rs, Element.U8_4(rs));
  blur.setInput(allocIn);
  blur.setRadius(radius);
  blur.forEach(allocOut);

  allocOut.copyTo(bitmap);
  rs.destroy();
  return bitmap;
}
              
ScriptIntrinsicBlur

ScriptIntrinsicConvolve3x3


private Bitmap convolve(Bitmap original, float[] coefficients) {
  Bitmap bitmap = Bitmap.createBitmap(
      original.getWidth(), original.getHeight(),
      Bitmap.Config.ARGB_8888);

  RenderScript rs = RenderScript.create(this);

  Allocation allocIn = Allocation.createFromBitmap(rs, original);
  Allocation allocOut = Allocation.createFromBitmap(rs, bitmap);

  ScriptIntrinsicConvolve3x3 convolution
      = ScriptIntrinsicConvolve3x3.create(rs, Element.U8_4(rs));
  convolution.setInput(allocIn);
  convolution.setCoefficients(coefficients);
  convolution.forEach(allocOut);

  allocOut.copyTo(bitmap);         // { 1, 1, 1,
  re.destroy();                    //   1, 1, 1,
  return bitmap;                   //   1, 1, 1  } / 9
}
              
ScriptIntrinsicConvolve3x3

Sharpen


private Bitmap convolve(Bitmap original, float[] coefficients) {
  Bitmap bitmap = Bitmap.createBitmap(
      original.getWidth(), original.getHeight(),
      Bitmap.Config.ARGB_8888);

  RenderScript rs = RenderScript.create(this);

  Allocation allocIn = Allocation.createFromBitmap(rs, original);
  Allocation allocOut = Allocation.createFromBitmap(rs, bitmap);

  ScriptIntrinsicConvolve3x3 convolution
      = ScriptIntrinsicConvolve3x3.create(rs, Element.U8_4(rs));
  convolution.setInput(allocIn);
  convolution.setCoefficients(coefficients);
  convolution.forEach(allocOut);

  allocOut.copyTo(bitmap);         // {  0, -1,  0,
  re.destroy();                    //   -1 , 5, -1,
  return bitmap;                   //    0, -1,  0  }
}
              
Sharpen

Edge detection


private Bitmap convolve(Bitmap original, float[] coefficients) {
  Bitmap bitmap = Bitmap.createBitmap(
      original.getWidth(), original.getHeight(),
      Bitmap.Config.ARGB_8888);

  RenderScript rs = RenderScript.create(this);

  Allocation allocIn = Allocation.createFromBitmap(rs, original);
  Allocation allocOut = Allocation.createFromBitmap(rs, bitmap);

  ScriptIntrinsicConvolve3x3 convolution
      = ScriptIntrinsicConvolve3x3.create(rs, Element.U8_4(rs));
  convolution.setInput(allocIn);
  convolution.setCoefficients(coefficients);
  convolution.forEach(allocOut);

  allocOut.copyTo(bitmap);         // { -1, -1, -1,
  re.destroy();                    //   -1 , 8, -1,
  return bitmap;                   //   -1, -1, -1  }
}
              
Edge detection

Fuzzy glass


private Bitmap convolve(Bitmap original, float[] coefficients) {
  Bitmap bitmap = Bitmap.createBitmap(
      original.getWidth(), original.getHeight(),
      Bitmap.Config.ARGB_8888);

  RenderScript rs = RenderScript.create(this);

  Allocation allocIn = Allocation.createFromBitmap(rs, original);
  Allocation allocOut = Allocation.createFromBitmap(rs, bitmap);

  ScriptIntrinsicConvolve3x3 convolution
      = ScriptIntrinsicConvolve3x3.create(rs, Element.U8_4(rs));
  convolution.setInput(allocIn);
  convolution.setCoefficients(coefficients);
  convolution.forEach(allocOut);

  allocOut.copyTo(bitmap);         // {  0,  20,  0,
  re.destroy();                    //   20, -59, 20,
  return bitmap;                   //    1,  13,  0  } / 7
}
              
Fuzzy glass
Hammer

When?

  • Text styling
  • External images
  • Dynamic effects

Further Reading

Thank you!