$30 off During Our Annual Pro Sale. View Details »

Crafting Custom Android Views

Crafting Custom Android Views

The Android framework is bundled with a large set of UI widgets (aka Views) that may be used to create basic UIs. Using the provided widgets is often a good starting point but one may rapidly be confronted to the lack of possibilities. Fortunately, some options do exist and mainly consist on mastering the View class and its hierarchy.

In this talk, we will focus on pushing the Android UI framework to the next level. We will demonstrate Android lets you go beyond your imagination by improving the existing widgets, creating compound controls and/or crafting completely new Views. If you want to build insanely innovative and advanced UI on Android and want to delight your users, this talk is definitely for you!

Cyril Mottier

April 25, 2013
Tweet

More Decks by Cyril Mottier

Other Decks in Programming

Transcript

  1. by Cyril Mottier
    CRAFTING CUSTOM
    ANDROID VIEWS

    View Slide

  2. @cyrilmottier

    View Slide

  3. View Slide

  4. https://www.capitainetrain.com/jobs

    View Slide

  5. Introduction to
    Android Views
    1

    View Slide

  6. DEFINITION
    building block
    The basic
    for user interface components

    View Slide

  7. The Android SDK is bundled
    with several built-in Views
    TextView
    ImageView
    Button
    View
    ViewGroup
    LinearLayout
    RelativeLayout
    ProgressBar
    ViewStub

    View Slide

  8. View

    View Slide

  9. View
    TextView
    ImageView

    View Slide

  10. View
    TextView
    Button
    ImageView

    View Slide

  11. View
    TextView ViewGroup
    Button
    ImageView

    View Slide

  12. View
    TextView ViewGroup
    Button
    ImageView
    LinearLayout FrameLayout

    View Slide

  13. View
    TextView ViewGroup
    Button
    ImageView
    LinearLayout FrameLayout

    View Slide

  14. What are Views
    made for ?

    View Slide

  15. A View is responsible for
    MEASURING

    View Slide

  16. LAYOUTING
    A View is responsible for

    View Slide

  17. DRAWING
    A View is responsible for

    View Slide

  18. Saving UI state
    Managing a Drawable state
    Handling touch events
    etc.
    RESPONSIBILITIES

    View Slide

  19. Why develop
    custom components ?

    View Slide

  20. Factorize Optimize Innovate Extend
    4main purposes

    View Slide

  21. 1
    Factorize
    Optimize Innovate Extend
    Avoid creating the same
    snippets of code over and over
    again over
    over
    over
    over
    over
    ove

    View Slide

  22. Factorize 2
    Optimize
    Innovate Extend
    Speed up UI rendering

    View Slide

  23. Factorize Optimize 3
    Innovate
    Extend
    Create something
    unique & groundbreaking

    View Slide

  24. Factorize Optimize Innovate 4
    Extend
    Access View’s protected
    behaviors

    View Slide

  25. Creating compound
    controls
    2

    View Slide

  26. Demo
    ProgressIndicator

    View Slide

  27. Extending an
    existing View
    3

    View Slide

  28. Demo
    UnderlinedTextView

    View Slide

  29. Creating a completely
    custom View
    4

    View Slide

  30. completely custom
    =
    ˝
    ˝

    View Slide

  31. extend View
    completely custom
    =
    ˝
    ˝

    View Slide

  32. completely custom
    =
    ˝
    ˝
    extend View
    ViewGroup

    View Slide

  33. passes to render a
    View hierarchy
    3
    Android does

    View Slide

  34. 1 measure
    2 layout
    3 draw
    }
    }
    requestLayout()
    invalidate()

    View Slide

  35. MEASUREMENT
    void onMeasure(
    int widthMeasureSpec,
    int heightMeasureSpec);
    Contract
    Call setMeasuredDimension(int, int)

    View Slide

  36. LAYOUT
    void onLayout(
    boolean changed,
    int lelf,
    int top,
    int right,
    int bottom)

    View Slide

  37. A View can have a persisting
    state
    STATE SAVE/RESTORE

    View Slide

  38. static class SavedState extends BaseSavedState {
    int progress;
    SavedState(Parcelable superState) {
    super(superState);
    }
    private SavedState(Parcel in) {
    super(in);
    progress = in.readInt();
    }
    @Override
    public void writeToParcel(Parcel out, int flags) {
    super.writeToParcel(out, flags);
    out.writeInt(progress);
    }
    public static final Parcelable.Creator CREATOR =
    new Parcelable.Creator() {
    public SavedState createFromParcel(Parcel in) {
    return new SavedState(in);
    }
    public SavedState[] newArray(int size) {
    return new SavedState[size];
    }
    };
    }

    View Slide

  39. static class SavedState extends BaseSavedState {
    int progress;
    SavedState(Parcelable superState) {
    super(superState);
    }
    private SavedState(Parcel in) {
    super(in);
    progress = in.readInt();
    }
    @Override
    public void writeToParcel(Parcel out, int flags) {
    super.writeToParcel(out, flags);
    out.writeInt(progress);
    }
    public static final Parcelable.Creator CREATOR =
    new Parcelable.Creator() {
    public SavedState createFromParcel(Parcel in) {
    return new SavedState(in);
    }
    public SavedState[] newArray(int size) {
    return new SavedState[size];
    }
    };
    }

    View Slide

  40. @Override
    public Parcelable onSaveInstanceState() {
    Parcelable superState = super.onSaveInstanceState();
    SavedState ss = new SavedState(superState);
    ss.progress = mProgress;
    return ss;
    }
    @Override
    public void onRestoreInstanceState(Parcelable state) {
    SavedState ss = (SavedState) state;
    super.onRestoreInstanceState(ss.getSuperState());
    setProgress(ss.progress);
    }

    View Slide

  41. @Override
    public Parcelable onSaveInstanceState() {
    Parcelable superState = super.onSaveInstanceState();
    SavedState ss = new SavedState(superState);
    ss.progress = mProgress;
    return ss;
    }
    @Override
    public void onRestoreInstanceState(Parcelable state) {
    SavedState ss = (SavedState) state;
    super.onRestoreInstanceState(ss.getSuperState());
    setProgress(ss.progress);
    }

    View Slide

  42. To every action there is a
    reaction
    TOUCH HANDLING

    View Slide

  43. @Override
    public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
    // The first pointer went down (i.e. stop
    // animations and start a new drag gesture)
    break;
    case MotionEvent.ACTION_MOVE:
    // A pointer has moved (i.e. move the object
    // accordingly)
    break;
    case MotionEvent.ACTION_UP:
    // The last pointer went up (i.e. compute the
    // object’s velocity and start animating it)
    break;
    case MotionEvent.ACTION_CANCEL:
    // The gesture was intercepted by a parent
    break;
    }
    return true;
    }

    View Slide

  44. @Override
    public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
    // The first pointer went down (i.e. stop
    // animations and start a new drag gesture)
    break;
    case MotionEvent.ACTION_MOVE:
    // A pointer has moved (i.e. move the object
    // accordingly)
    break;
    case MotionEvent.ACTION_UP:
    // The last pointer went up (i.e. compute the
    // object’s velocity and start animating it)
    break;
    case MotionEvent.ACTION_CANCEL:
    // The gesture was intercepted by a parent
    break;
    }
    return true;
    }

    View Slide

  45. @Override
    public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
    // The first pointer went down (i.e. stop
    // animations and start a new drag gesture)
    break;
    case MotionEvent.ACTION_MOVE:
    // A pointer has moved (i.e. move the object
    // accordingly)
    break;
    case MotionEvent.ACTION_UP:
    // The last pointer went up (i.e. compute the
    // object’s velocity and start animating it)
    break;
    case MotionEvent.ACTION_CANCEL:
    // The gesture was intercepted by a parent
    break;
    }
    return true;
    }

    View Slide

  46. @Override
    public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
    // The first pointer went down (i.e. stop
    // animations and start a new drag gesture)
    break;
    case MotionEvent.ACTION_MOVE:
    // A pointer has moved (i.e. move the object
    // accordingly)
    break;
    case MotionEvent.ACTION_UP:
    // The last pointer went up (i.e. compute the
    // object’s velocity and start animating it)
    break;
    case MotionEvent.ACTION_CANCEL:
    // The gesture was intercepted by a parent
    break;
    }
    return true;
    }

    View Slide

  47. @Override
    public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
    // The first pointer went down (i.e. stop
    // animations and start a new drag gesture)
    break;
    case MotionEvent.ACTION_MOVE:
    // A pointer has moved (i.e. move the object
    // accordingly)
    break;
    case MotionEvent.ACTION_UP:
    // The last pointer went up (i.e. compute the
    // object’s velocity and start animating it)
    break;
    case MotionEvent.ACTION_CANCEL:
    // The gesture was intercepted by a parent
    break;
    }
    return true;
    }

    View Slide

  48. @Override
    public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
    // The first pointer went down (i.e. stop
    // animations and start a new drag gesture)
    break;
    case MotionEvent.ACTION_MOVE:
    // A pointer has moved (i.e. move the object
    // accordingly)
    break;
    case MotionEvent.ACTION_UP:
    // The last pointer went up (i.e. compute the
    // object’s velocity and start animating it)
    break;
    case MotionEvent.ACTION_CANCEL:
    // The gesture was intercepted by a parent
    break;
    }
    return true;
    }

    View Slide

  49. What if a parent
    View wants to
    intercept the gesture?

    View Slide

  50. boolean
    onInterceptTouchEvent(MotionEvent event)
    FOR EXPERTS

    View Slide

  51. SCROLLING/FLINGING
    A thrown object moves until it
    reaches its
    state of rest

    View Slide

  52. 1Track velocity
    2
    Scroll/fling to state of rest

    View Slide

  53. 1Track velocity
    @Override public boolean onTouchEvent(MotionEvent event) {
    if (mVelocityTracker == null) {
    mVelocityTracker = VelocityTracker.obtain();
    }
    mVelocityTracker.addMovement(event);
    switch (event.getAction()) {
    case MotionEvent.ACTION_UP:
    mVelocityTracker.computeCurrentVelocity(1000);
    if (mVelocityTracker.getXVelocity() > mThreshold) {
    // Animate the object
    }
    // no break;
    case MotionEvent.ACTION_CANCEL:
    mVelocityTracker.recycle();
    mVelocityTracker = null;
    break;
    }
    return true;
    }

    View Slide

  54. 1Track velocity
    @Override public boolean onTouchEvent(MotionEvent event) {
    if (mVelocityTracker == null) {
    mVelocityTracker = VelocityTracker.obtain();
    }
    mVelocityTracker.addMovement(event);
    switch (event.getAction()) {
    case MotionEvent.ACTION_UP:
    mVelocityTracker.computeCurrentVelocity(1000);
    if (mVelocityTracker.getXVelocity() > mThreshold) {
    // Animate the object
    }
    // no break;
    case MotionEvent.ACTION_CANCEL:
    mVelocityTracker.recycle();
    mVelocityTracker = null;
    break;
    }
    return true;
    }

    View Slide

  55. 1Track velocity
    @Override public boolean onTouchEvent(MotionEvent event) {
    if (mVelocityTracker == null) {
    mVelocityTracker = VelocityTracker.obtain();
    }
    mVelocityTracker.addMovement(event);
    switch (event.getAction()) {
    case MotionEvent.ACTION_UP:
    mVelocityTracker.computeCurrentVelocity(1000);
    if (mVelocityTracker.getXVelocity() > mThreshold) {
    // Animate the object
    }
    // no break;
    case MotionEvent.ACTION_CANCEL:
    mVelocityTracker.recycle();
    mVelocityTracker = null;
    break;
    }
    return true;
    }

    View Slide

  56. 1Track velocity
    @Override public boolean onTouchEvent(MotionEvent event) {
    if (mVelocityTracker == null) {
    mVelocityTracker = VelocityTracker.obtain();
    }
    mVelocityTracker.addMovement(event);
    switch (event.getAction()) {
    case MotionEvent.ACTION_UP:
    mVelocityTracker.computeCurrentVelocity(1000);
    if (mVelocityTracker.getXVelocity() > mThreshold) {
    // Animate the object
    }
    // no break;
    case MotionEvent.ACTION_CANCEL:
    mVelocityTracker.recycle();
    mVelocityTracker = null;
    break;
    }
    return true;
    }

    View Slide

  57. 1Track velocity
    @Override public boolean onTouchEvent(MotionEvent event) {
    if (mVelocityTracker == null) {
    mVelocityTracker = VelocityTracker.obtain();
    }
    mVelocityTracker.addMovement(event);
    switch (event.getAction()) {
    case MotionEvent.ACTION_UP:
    mVelocityTracker.computeCurrentVelocity(1000);
    if (mVelocityTracker.getXVelocity() > mThreshold) {
    // Animate the object
    }
    // no break;
    case MotionEvent.ACTION_CANCEL:
    mVelocityTracker.recycle();
    mVelocityTracker = null;
    break;
    }
    return true;
    }

    View Slide

  58. 2
    Scroll/fling to state of rest
    private final Runnable mScrollRunnable = new Runnable() {
    @Override
    public void run() {
    if (mOverScroller.computeScrollOffset()) {
    final int x = mOverScroller.getCurrX();
    final int y = mOverScroller.getCurrY();
    // Move object to (x, y).
    postOnAnimation(this);
    } else {
    // animation is over
    }
    }
    };
    public void fling() {
    mOverScroller.fling(
    mStartX, mStartY, // start x/y
    mVX, mVY, // velocity x/y
    mMinX, mMaxX, // min/max x
    mMinY, mMaxY); // min/max y
    postOnAnimation(mScrollRunnable);
    }

    View Slide

  59. 2
    Scroll/fling to state of rest
    private final Runnable mScrollRunnable = new Runnable() {
    @Override
    public void run() {
    if (mOverScroller.computeScrollOffset()) {
    final int x = mOverScroller.getCurrX();
    final int y = mOverScroller.getCurrY();
    // Move object to (x, y).
    postOnAnimation(this);
    } else {
    // animation is over
    }
    }
    };
    public void fling() {
    mOverScroller.fling(
    mStartX, mStartY, // start x/y
    mVX, mVY, // velocity x/y
    mMinX, mMaxX, // min/max x
    mMinY, mMaxY); // min/max y
    postOnAnimation(mScrollRunnable);
    }

    View Slide

  60. 2
    Scroll/fling to state of rest
    private final Runnable mScrollRunnable = new Runnable() {
    @Override
    public void run() {
    if (mOverScroller.computeScrollOffset()) {
    final int x = mOverScroller.getCurrX();
    final int y = mOverScroller.getCurrY();
    // Move object to (x, y).
    postOnAnimation(this);
    } else {
    // animation is over
    }
    }
    };
    public void fling() {
    mOverScroller.fling(
    mStartX, mStartY, // start x/y
    mVX, mVY, // velocity x/y
    mMinX, mMaxX, // min/max x
    mMinY, mMaxY); // min/max y
    postOnAnimation(mScrollRunnable);
    }

    View Slide

  61. 2
    Scroll/fling to state of rest
    private final Runnable mScrollRunnable = new Runnable() {
    @Override
    public void run() {
    if (mOverScroller.computeScrollOffset()) {
    final int x = mOverScroller.getCurrX();
    final int y = mOverScroller.getCurrY();
    // Move object to (x, y).
    postOnAnimation(this);
    } else {
    // animation is over
    }
    }
    };
    public void fling() {
    mOverScroller.fling(
    mStartX, mStartY, // start x/y
    mVX, mVY, // velocity x/y
    mMinX, mMaxX, // min/max x
    mMinY, mMaxY); // min/max y
    postOnAnimation(mScrollRunnable);
    }

    View Slide

  62. 2
    Scroll/fling to state of rest
    private final Runnable mScrollRunnable = new Runnable() {
    @Override
    public void run() {
    if (mOverScroller.computeScrollOffset()) {
    final int x = mOverScroller.getCurrX();
    final int y = mOverScroller.getCurrY();
    // Move object to (x, y).
    postOnAnimation(this);
    } else {
    // animation is over
    }
    }
    };
    public void fling() {
    mOverScroller.fling(
    mStartX, mStartY, // start x/y
    mVX, mVY, // velocity x/y
    mMinX, mMaxX, // min/max x
    mMinY, mMaxY); // min/max y
    postOnAnimation(mScrollRunnable);
    }

    View Slide

  63. 2
    Scroll/fling to state of rest
    private final Runnable mScrollRunnable = new Runnable() {
    @Override
    public void run() {
    if (mOverScroller.computeScrollOffset()) {
    final int x = mOverScroller.getCurrX();
    final int y = mOverScroller.getCurrY();
    // Move object to (x, y).
    postOnAnimation(this);
    } else {
    // animation is over
    }
    }
    };
    public void fling() {
    mOverScroller.fling(
    mStartX, mStartY, // start x/y
    mVX, mVY, // velocity x/y
    mMinX, mMaxX, // min/max x
    mMinY, mMaxY); // min/max y
    postOnAnimation(mScrollRunnable);
    }

    View Slide

  64. CONSISTENCY
    with the rest of the platform
    behavior is consistent
    Make sure your custom View

    View Slide

  65. EdgeEffect

    View Slide

  66. @Override
    public void draw(Canvas canvas) {
    super.draw(canvas);
    if (!mTopEdgeEffect.isFinished()) {
    canvas.save();
    canvas.translate(getPaddingLeft(), 0);
    final int w = getWidth() - getPaddingLeft()
    - getPaddingRight();
    mTopEdgeEffect.setSize(w, getHeight());
    if (mTopEdgeEffect.draw(canvas)) {
    postInvalidateOnAnimation();
    }
    canvas.restore();
    }
    }

    View Slide

  67. @Override
    public void draw(Canvas canvas) {
    super.draw(canvas);
    if (!mTopEdgeEffect.isFinished()) {
    canvas.save();
    canvas.translate(getPaddingLeft(), 0);
    final int w = getWidth() - getPaddingLeft()
    - getPaddingRight();
    mTopEdgeEffect.setSize(w, getHeight());
    if (mTopEdgeEffect.draw(canvas)) {
    postInvalidateOnAnimation();
    }
    canvas.restore();
    }
    }

    View Slide

  68. @Override
    public void draw(Canvas canvas) {
    super.draw(canvas);
    if (!mTopEdgeEffect.isFinished()) {
    canvas.save();
    canvas.translate(getPaddingLeft(), 0);
    final int w = getWidth() - getPaddingLeft()
    - getPaddingRight();
    mTopEdgeEffect.setSize(w, getHeight());
    if (mTopEdgeEffect.draw(canvas)) {
    postInvalidateOnAnimation();
    }
    canvas.restore();
    }
    }

    View Slide

  69. @Override
    public void draw(Canvas canvas) {
    super.draw(canvas);
    if (!mTopEdgeEffect.isFinished()) {
    canvas.save();
    canvas.translate(getPaddingLeft(), 0);
    final int w = getWidth() - getPaddingLeft()
    - getPaddingRight();
    mTopEdgeEffect.setSize(w, getHeight());
    if (mTopEdgeEffect.draw(canvas)) {
    postInvalidateOnAnimation();
    }
    canvas.restore();
    }
    }

    View Slide

  70. @Override
    public void draw(Canvas canvas) {
    super.draw(canvas);
    if (!mTopEdgeEffect.isFinished()) {
    canvas.save();
    canvas.translate(getPaddingLeft(), 0);
    final int w = getWidth() - getPaddingLeft()
    - getPaddingRight();
    mTopEdgeEffect.setSize(w, getHeight());
    if (mTopEdgeEffect.draw(canvas)) {
    postInvalidateOnAnimation();
    }
    canvas.restore();
    }
    }

    View Slide

  71. @Override
    public void draw(Canvas canvas) {
    super.draw(canvas);
    if (!mTopEdgeEffect.isFinished()) {
    canvas.save();
    canvas.translate(getPaddingLeft(), 0);
    final int w = getWidth() - getPaddingLeft()
    - getPaddingRight();
    mTopEdgeEffect.setSize(w, getHeight());
    if (mTopEdgeEffect.draw(canvas)) {
    postInvalidateOnAnimation();
    }
    canvas.restore();
    }
    }

    View Slide

  72. This was only
    the drawing part ...

    View Slide

  73. mTopEdgeEffect.onAbsorb((int) velocity);
    .onPull((float) distance);
    .onRelease();

    View Slide

  74. ViewConfiguration documentation
    Contains methods to
    standard constants used in
    the UI for timeouts, sizes,
    and distances.

    View Slide

  75. private void init(Context context, AttributeSet attrs, int
    defStyle) {
    mViewConfiguration = ViewConfiguration.get(context);
    final int maxFlingV = mViewConfiguration
    .getScaledMaximumFlingVelocity();
    final int touchSlop = mViewConfiguration
    .getScaledTouchSlop();
    // ...
    }

    View Slide

  76. private void init(Context context, AttributeSet attrs, int
    defStyle) {
    mViewConfiguration = ViewConfiguration.get(context);
    final int maxFlingV = mViewConfiguration
    .getScaledMaximumFlingVelocity();
    final int touchSlop = mViewConfiguration
    .getScaledTouchSlop();
    // ...
    }

    View Slide

  77. private void init(Context context, AttributeSet attrs, int
    defStyle) {
    mViewConfiguration = ViewConfiguration.get(context);
    final int maxFlingV = mViewConfiguration
    .getScaledMaximumFlingVelocity();
    final int touchSlop = mViewConfiguration
    .getScaledTouchSlop();
    // ...
    }

    View Slide

  78. Conclusion
    5

    View Slide

  79. Custom views are the
    best way for creating
    unique UIs

    View Slide

  80. Custom views must
    fit to the platform
    appearance

    View Slide

  81. Custom views must
    match to the mental
    model

    View Slide

  82. THANK YOU
    @cyrilmottier
    cyrilmottier.com
    Cyril Mottier

    View Slide