click tracking

http://chiuki.github.com/devconf-ru

Built-in Components

  • TextView
  • EditText
  • Button
  • Spinner
  • DatePicker

Why custom components?

  • Modularize repeated code
  • Access protected methods
  • Optimize rendering speed
  • Complete control with draw, measure and layout

Simple view, container and compound control

Simple View Container Compound Control
  ↳ TextView
    ↳ EditText
    ↳ Button
  ↳ ImageView
    ↳ ImageButton
  ↳ AdapterView
    ↳ ListView
    ↳ Gallery
    ↳ GridView
  ↳ LinearLayout
  ↳ RelativeLayout
  ↳ FrameLayout
  ↳ TableLayout
  ↳ DatePicker
  ↳ TwoLineListItem

TextView showing date

http://github.com/chiuki/android-date-view

<TextView
  android:id="@+id/date"
  android:layout_width="match_parent"
  android:layout_height="wrap_content" />
TextView dateView = (TextView) findViewById(R.id.date);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
String today = dateFormat.format(Calendar.getInstance().getTime());
dateView.setText(today);

TextView showing date: screenshot

DateView: constructor

package com.sqisland.android.dateview;

public class DateView extends TextView {
  public DateView(Context context) {
    super(context);
  }

  public DateView(Context context, AttributeSet attrs) {
    super(context, attrs);
  }

  public DateView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
  }
}

DateView: setDate

public class DateView extends TextView {
  public DateView(Context context) {
    super(context);
    setDate();
  }

  public DateView(Context context, AttributeSet attrs) {
    super(context, attrs);
    setDate();
  }

  public DateView(
      Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    setDate();
  }
}

DateView: setDate

private void setDate() {
  SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
  String today = dateFormat.format(Calendar.getInstance().getTime());
  setText(today);  // self = DateView = subclass of TextView
}

DateView: create with code

public class DateActivity extends Activity {
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    DateView dateView = new DateView(this);
    setContentView(dateView);
  }
}

DateView: create with xml

public class DateActivity extends Activity {
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
  }
}
<com.sqisland.android.dateview.DateView
  android:layout_width="match_parent"
  android:layout_height="wrap_content" />

DateView: configure with TextView attributes

<com.sqisland.android.dateview.DateView
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:textColor="#0f0"
  android:textSize="40sp" />
      

DateView with attributes: screenshot

Compound Control

  • A group of existing controls
  • Encapsulate reusable functionality

LengthPicker

http://github.com/chiuki/android-length-picker

length_picker.xml

<merge xmlns:android="http://schemas.android.com/apk/res/android" >
  <Button android:id="@+id/minus_button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/minus" />
  <TextView android:id="@+id/text"
    android:layout_width="50dp"
    android:layout_height="wrap_content"
    android:gravity="center" />
  <Button android:id="@+id/plus_button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/plus" />
</merge>

LengthPicker.java

// Called from constructor
private void init() {
  LayoutInflater inflater = (LayoutInflater) getContext()
      .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
  inflater.inflate(R.layout.length_picker, this);

  mPlusButton = findViewById(R.id.plus_button);
  mTextView = (TextView) findViewById(R.id.text);
  mMinusButton = findViewById(R.id.minus_button);

  updateControls();

  mPlusButton.setOnClickListener(this);
  mMinusButton.setOnClickListener(this);
}

updateControls

private void updateControls() {
  int feet = mNumInches / 12;
  int inches = mNumInches % 12;

  String text = String.format("%d' %d\"", feet, inches);
  if (feet == 0) {
    text = String.format("%d\"", inches);
  } else {
    if (inches == 0) {
      text = String.format("%d'", feet);
    }
  }
  mTextView.setText(text);

  mMinusButton.setEnabled(mNumInches > 0);
}

onClick

public void onClick(View v) {
  switch (v.getId()) {
  case R.id.plus_button:
    mNumInches++;
    updateControls();
    break;
  case R.id.minus_button:
    if (mNumInches > 0) {
      mNumInches--;
      updateControls();
    }
    break;
  }
}

Expose data

public int getNumInches() {
  return mNumInches;
}

Save state

public Parcelable onSaveInstanceState() {
  Bundle bundle = new Bundle();
  bundle.putParcelable("superState", super.onSaveInstanceState());
  bundle.putInt("numInches", mNumInches);
  return bundle;
}

public void onRestoreInstanceState(Parcelable state) {
  if (state instanceof Bundle) {
    Bundle bundle = (Bundle) state;
    mNumInches = bundle.getInt("numInches");
    super.onRestoreInstanceState(bundle.getParcelable("superState"));
  } else {
    super.onRestoreInstanceState(state);
  }
  updateControls();
}

Reusing LengthPicker

main.xml

<com.sqisland.android.length_picker.LengthPicker
  android:id="@+id/width"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content" />
<com.sqisland.android.length_picker.LengthPicker
  android:id="@+id/height"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content" />
<TextView
  android:id="@+id/area"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content" />
<Button
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="@string/compute"
  android:onClick="updateArea" />

Activity

public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);
  mWidth = (LengthPicker) findViewById(R.id.width);
  mHeight = (LengthPicker) findViewById(R.id.height);
  mArea = (TextView) findViewById(R.id.area);
}

public void onResume() {
  super.onResume();
  updateArea(null);
}

public void updateArea(View v) {
  int area = mWidth.getNumInches() * mHeight.getNumInches();
  mArea.setText(getString(R.string.area_format, area));
}

Fully customized components

  • onLayout
  • onMeasure
  • onDraw / dispatchDraw

Android layout procedure

  • requestLayout up to ViewRoot
  • measure all children
  • layout all children

Customizing a View

  • onMeasure - how big?
  • onDraw - what to show?

onMeasure

void onMeasure(int widthMeasureSpec, int heightMeasureSpec);
  • Measure its own size according to measure spec
  • Call setMeasuredDimension to store results

MeasureSpec

  • UNSPECIFIED - anything goes!
  • AT_MOST - as large as the specified size
  • EXACTLY - as given by parent

SquareView

http://github.com/chiuki/android-square-view

public void onMeasure(int widthSpec, int heightSpec) {
  super.onMeasure(widthSpec, heightSpec);
  int size = Math.min(getMeasuredWidth(), getMeasuredHeight());
  setMeasuredDimension(size, size);
}

onDraw

void onDraw(Canvas canvas);
  • drawLine
  • drawRect
  • drawCircle
  • drawPath
  • drawText
  • drawBitmap

Canvas

  • clipRect
  • translate
  • rotate
  • scale
  • skew
  • save
  • restore

Pizza

http://github.com/chiuki/android-pizza

Pizza

private void init() {
  mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
  mPaint.setColor(Color.YELLOW);
  mPaint.setStrokeWidth(STROKE_WIDTH);
  mPaint.setStyle(Paint.Style.STROKE);
}
public void onDraw(Canvas canvas) {
  int width = getWidth() - getPaddingLeft() - getPaddingRight();
  int height = getHeight() - getPaddingTop() - getPaddingBottom();
  int diameter = Math.min(width, height) - STROKE_WIDTH;
  int cx = getPaddingLeft() + width / 2;
  int cy = getPaddingTop() + height / 2;
  int radius = diameter / 2;
  canvas.drawCircle(cx, cy, radius, mPaint);
  drawPizzaCuts(canvas, cx, cy, radius);
}

drawPizzaCuts

private void drawPizzaCuts(
    Canvas canvas, float cx, float cy, float radius) {
  canvas.save();
  final float degrees = 360f / NUM_WEDGES;
  for (int i = 0; i < NUM_WEDGES; ++i) {
    canvas.rotate(degrees, cx, cy);
    canvas.drawLine(cx, cy, cx, cy - radius, mPaint);
  }
  canvas.restore();
}

main.xml

<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="vertical" >
  <com.sqisland.android.pizza.Pizza
    android:layout_width="200dp"
    android:layout_height="200dp"
    android:padding="20dp"
    android:background="#555" />
  <com.sqisland.android.pizza.Pizza
    android:layout_width="200dp"
    android:layout_height="200dp"
    android:layout_margin="20dp"
    android:background="#555" />
</LinearLayout>

Pizza: padding vs margin

Customizing a ViewGroup

  • onMeasure - how big are the children?
  • onLayout - where are the children?
  • dispatchDraw - what to show above or below the children?

onLayout: PhotoSpiral

http://github.com/chiuki/android-photo-spiral

PhotoSpiral: assumptions

  • 4 photos
  • Same dimensions
  • Landscape, portrait, landscape, portrait

PhotoSpiral: onMeasure

protected void onMeasure(int widthSpec, int heightSpec) {
  measureChildren(widthSpec, heightSpec);
  View first = getChildAt(0);
  int size = first.getMeasuredWidth() + first.getMeasuredHeight();
  int width = ViewGroup.resolveSize(size, widthSpec);
  int height = ViewGroup.resolveSize(size, heightSpec);
  setMeasuredDimension(width, height);
}

PhotoSpiral: onLayout

protected void onLayout(
    boolean changed, int l, int t, int r, int b) {
  View first = getChildAt(0);
  final int childWidth = first.getMeasuredWidth();
  final int childHeight = first.getMeasuredHeight();

  int x = getWidth() / 2 - (childWidth - childHeight) / 2;
  int y = getHeight() / 2 - (childWidth + childHeight) / 2;

  for (int i = 0; i < getChildCount(); ++i) {
    View child = getChildAt(i);
    x = adjustX(i, x, childWidth, childHeight);
    y = adjustY(i, y, childWidth, childHeight);
    child.layout(x, y,
        x + child.getMeasuredWidth(), y + child.getMeasuredHeight());
  }
}

PhotoSpiral: adjustX

private int adjustX(int pos, int x, int childWidth, int childHeight) {
  int delta = childWidth - childHeight;
  switch (pos) {
  case 1:
    return x + delta;
  case 2:
    return x - childWidth;
  }
  return x;
}

PhotoSpiral: adjustY

private int adjustY(int pos, int y, int childWidth, int childHeight) {
  int delta = childWidth - childHeight;
  switch (pos) {
  case 1:
    return y + childHeight;
  case 2:
    return y + delta;
  case 3:
    return y - childWidth;
  }
  return y;
}

dispatchDraw: SidewaysLayout

http://github.com/chiuki/android-sideways-layout

SidewaysLayout: LinearLayout rotated

SidewaysLayout: xml

<com.sqisland.android.sideways_layout.SidewaysLayout
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:orientation="vertical" >
  <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textColor="#f0f12b"
    android:text="@string/good_night_moon" />
  <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textStyle="bold"
    android:text="@string/green_eggs_and_ham" />
  <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/matilda" />
</com.sqisland.android.sideways_layout.SidewaysLayout>

SidewaysLayout.java

public class SidewaysLayout extends LinearLayout {
  // Constructors omitted

  protected void onMeasure(int widthSpec, int heightSpec) {
    super.onMeasure(widthSpec, heightSpec);
    setMeasuredDimension(getMeasuredHeight(), getMeasuredWidth());
  }

  protected void dispatchDraw(Canvas canvas) {
    canvas.save();
    canvas.translate(0, getHeight());
    canvas.rotate(-90, 0, 0);
    super.dispatchDraw(canvas);
    canvas.restore();
  }
}

SidewaysLayout: onMeasure

  • Swap the width and height
protected void onMeasure(int widthSpec, int heightSpec) {
  super.onMeasure(widthSpec, heightSpec);
  setMeasuredDimension(getMeasuredHeight(), getMeasuredWidth());
}

SidewaysLayout: dispatchDraw

  • onDraw: draw myself
  • dispatchDraw: draw children
  • Rotate and re-center canvas to draw sideways
protected void dispatchDraw(Canvas canvas) {
  canvas.save();
  canvas.translate(0, getHeight());
  canvas.rotate(-90, 0, 0);
  super.dispatchDraw(canvas);
  canvas.restore();
}

Reusable library project



  • New Android project
  • Change property to check "Is Library"
  • Store java code and xml attributes in library project

XML attributes

<com.sqisland.android.pizza.Pizza
  android:layout_width="120dp"
  android:layout_height="120dp"
  pizza:color="#f00"
  pizza:num_wedges="4" />

res/values/attrs.xml

<resources>
  <declare-styleable name="com.sqisland.android.pizza.Pizza">
    <attr name="stroke_width" format="integer" />
    <attr name="num_wedges" format="integer" />
    <attr name="color" format="color" />
  </declare-styleable>
</resources>

Pizza.java

private void init(AttributeSet attrs) {
  if (attrs != null) {
    String namespace = "http://schemas.android.com/apk/res-auto";
    mStrokeWidth = attrs.getAttributeIntValue(
      namespace, "stroke_width", DEFAULT_STROKE_WIDTH);
    mNumWedges = attrs.getAttributeIntValue(
      namespace, "num_wedges", DEFAULT_NUM_WEDGES);
    mColor = attrs.getAttributeIntValue(
      namespace, "color", DEFAULT_COLOR);
}
  • The build system replaces http://schemas.android.com/apk/res-auto with your app package
  • Available since ADT r17

Link library in main app

main.xml

<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:pizza="http://schemas.android.com/apk/res-auto"
  android:layout_width="match_parent"
  android:layout_height="wrap_content" >
  <com.sqisland.android.pizza.Pizza
    android:layout_width="120dp"
    android:layout_height="120dp"
    pizza:color="#f00"
    pizza:num_wedges="4" />
  <com.sqisland.android.pizza.Pizza
    android:layout_width="160dp"
    android:layout_height="160dp"
    pizza:stroke_width="15"
    pizza:num_wedges="6" />
</LinearLayout>

Pizzeria!

Fragments

  • Reusable sub-activities
  • Goes through activity cycle
  • Communicates with other Fragments via FragmentManager

Summary

  • Shortcut View and ViewGroup
  • Custom View
  • Custom ViewGroup

Shortcut View and ViewGroup

Modularize repeated code

  • Subclass existing widget
    e.g. DateView extends TextView
  • Group existing widgets into compound control
    e.g. LengthPicker

Custom View

Customize size and appearance of the View

  • onMeasure
    e.g. SquareView
  • onDraw
    e.g. Pizza

Custom ViewGroup

Container for positioning child views

  • onLayout
    e.g. PhotoSpiral
  • dispatchDraw
    e.g. SidewaysLayout

Thank you!