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!

E9bf8f6d5480ea2a2623df7dccfd1f70?s=128

Cyril Mottier

April 25, 2013
Tweet

Transcript

  1. by Cyril Mottier CRAFTING CUSTOM ANDROID VIEWS

  2. @cyrilmottier

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

  5. Introduction to Android Views 1

  6. DEFINITION building block The basic for user interface components

  7. The Android SDK is bundled with several built-in Views TextView

    ImageView Button View ViewGroup LinearLayout RelativeLayout ProgressBar ViewStub
  8. View

  9. View TextView ImageView

  10. View TextView Button ImageView

  11. View TextView ViewGroup Button ImageView

  12. View TextView ViewGroup Button ImageView LinearLayout FrameLayout

  13. View TextView ViewGroup Button ImageView LinearLayout FrameLayout

  14. What are Views made for ?

  15. A View is responsible for MEASURING

  16. LAYOUTING A View is responsible for

  17. DRAWING A View is responsible for

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

    etc. RESPONSIBILITIES
  19. Why develop custom components ?

  20. Factorize Optimize Innovate Extend 4main purposes

  21. 1 Factorize Optimize Innovate Extend Avoid creating the same snippets

    of code over and over again over over over over over ove
  22. Factorize 2 Optimize Innovate Extend Speed up UI rendering

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

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

  25. Creating compound controls 2

  26. Demo ProgressIndicator

  27. Extending an existing View 3

  28. Demo UnderlinedTextView

  29. Creating a completely custom View 4

  30. completely custom = ˝ ˝

  31. extend View completely custom = ˝ ˝

  32. completely custom = ˝ ˝ extend View ViewGroup

  33. passes to render a View hierarchy 3 Android does

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

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

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

    right, int bottom)
  37. A View can have a persisting state STATE SAVE/RESTORE

  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<SavedState> CREATOR = new Parcelable.Creator<SavedState>() { public SavedState createFromParcel(Parcel in) { return new SavedState(in); } public SavedState[] newArray(int size) { return new SavedState[size]; } }; }
  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<SavedState> CREATOR = new Parcelable.Creator<SavedState>() { public SavedState createFromParcel(Parcel in) { return new SavedState(in); } public SavedState[] newArray(int size) { return new SavedState[size]; } }; }
  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); }
  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); }
  42. To every action there is a reaction TOUCH HANDLING

  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; }
  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; }
  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; }
  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; }
  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; }
  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; }
  49. What if a parent View wants to intercept the gesture?

  50. boolean onInterceptTouchEvent(MotionEvent event) FOR EXPERTS

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

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

  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; }
  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; }
  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; }
  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; }
  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; }
  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); }
  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); }
  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); }
  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); }
  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); }
  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); }
  64. CONSISTENCY with the rest of the platform behavior is consistent

    Make sure your custom View
  65. EdgeEffect

  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(); } }
  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(); } }
  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(); } }
  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(); } }
  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(); } }
  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(); } }
  72. This was only the drawing part ...

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

  74. ViewConfiguration documentation Contains methods to standard constants used in the

    UI for timeouts, sizes, and distances.
  75. private void init(Context context, AttributeSet attrs, int defStyle) { mViewConfiguration

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

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

    = ViewConfiguration.get(context); final int maxFlingV = mViewConfiguration .getScaledMaximumFlingVelocity(); final int touchSlop = mViewConfiguration .getScaledTouchSlop(); // ... }
  78. Conclusion 5

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

  80. Custom views must fit to the platform appearance

  81. Custom views must match to the mental model

  82. THANK YOU @cyrilmottier cyrilmottier.com Cyril Mottier