Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Let's Sprinkle some #PerfMatters on your ViewGr...

Let's Sprinkle some #PerfMatters on your ViewGroups - Droidcon Paris 2015

Framework classes like RelativeLayout are extremely powerful but their first goal is to be versatile. This comes with significant performances costs that can prevent your app from being fast and smooth, especially in constrained areas like list scrolling. Let's see how we can remedy this by writing our own layouting and drawing logic where it matters.

François Blavoet

November 09, 2015
Tweet

More Decks by François Blavoet

Other Decks in Programming

Transcript

  1. From the Material spec 'Mo$on provides meaning' 'Mo$on is meaningful

    and appropriate, serving to focus a7en$on and maintain con$nuity'
  2. The 16 ms target 1000 ms / 16.7 = 60

    frames per second - Cinema : 24 fps - Video games : usually 30 or 60 fps
  3. What is Jank ? Problema)c blocking of a so2ware applica)on's

    user interface due to slow opera)ons1. In prac(ce : • Choppy UI • Discon/nuity of mo/on 1 h$ps:/ /en.wik/onary.org/wiki/jank
  4. Let's inves*gate ! Our first tool : SysTrace Displays the

    execu/on /mes of all the system's processes AndroidSdk/platform-tools/systrace/systrace.py
  5. ViewGroup 2 main methods : protected void onMeasure(int widthMeasureSpec, int

    heightMeasureSpec) {} protected void onLayout(boolean changed, int left, int top, int right, int bottom) {}
  6. Rela%veLayout At its core, Rela.veLayout is designed around collec.ng and

    applying constraints : private static final int[] RULES_VERTICAL = { ABOVE, BELOW, ALIGN_BASELINE, ALIGN_TOP, ALIGN_BOTTOM }; private static final int[] RULES_HORIZONTAL = { LEFT_OF, RIGHT_OF, ALIGN_LEFT, ALIGN_RIGHT, START_OF, END_OF, ALIGN_START, ALIGN_END };
  7. Rela%veLayout In order to apply these constraints, in onMeasure() :

    • sort the children according to their rela0ve constraints • apply the horizontal constraints • apply the ver0cal constraints • apply baseline, size wrapping, etc
  8. One poten(al solu(on : wri(ng our own ViewGroup Framework classes

    : • Correctness • Versa,lity • Performances
  9. One poten(al solu(on : wri(ng our own ViewGroup Custom implementa.on

    : • Correctness • Versa,lity Specialized • Performances
  10. public class AlbumView extends ViewGroup { public AlbumView(Context context) {

    this(context, null); } public AlbumView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public AlbumView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs, defStyleAttr, 0); } public AlbumView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); init(context, attrs, defStyleAttr, defStyleRes); } }
  11. private ImageView cover; private TextView title; private TextView artist; private

    ImageView heart; private ImageView overflow; private void init(Context context) { LayoutInflater.from(context).inflate(R.layout.album_view_internal, this, true); cover = (ImageView) findViewById(R.id.cover); title = (TextView) findViewById(R.id.title); artist = (TextView) findViewById(R.id.artist); heart = (ImageView) findViewById(R.id.artist); overflow = (ImageView) findViewById(R.id.artist); }
  12. @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(getContext(), attrs);

    } @Override protected LayoutParams generateDefaultLayoutParams() { return new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); }
  13. @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthUsed

    = 0; int heightUsed = 0; measureChildWithMargins(cover, widthMeasureSpec, widthUsed, heightMeasureSpec, heightUsed); widthUsed += getMeasuredWidthWithMargins(mCoverView); ... } protected static int getMeasuredWidthWithMargins(View child) { final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); return child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; }
  14. @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthUsed

    = 0; int heightUsed = 0; measureChildWithMargins(cover, widthMeasureSpec, widthUsed, heightMeasureSpec, heightUsed); widthUsed += getMeasuredWidthWithMargins(mCoverView); // same operation for a second child : measureChildWithMargins(overflow, widthMeasureSpec, widthUsed, heightMeasureSpec, heightUsed); widthUsed += getMeasuredWidthWithMargins(mCoverView); ... }
  15. @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthUsed

    = 0; int heightUsed = 0; measureChildWithMargins(cover, ...); widthUsed += getMeasuredWidthWithMargins(mCoverView); measureChildWithMargins(overflow, ...); widthUsed += getMeasuredWidthWithMargins(mCoverView); measureChildWithMargins(heart, ...); widthUsed += getMeasuredWidthWithMargins(mCoverView); measureChildWithMargins(title, ...); measureChildWithMargins(artist, ...); widthUsed += getMeasuredWidthWithMargins(mCoverView); setMeasuredDimension(resolveSize(widthUsed, measureSpec), MeasureSpec.getSize(heightMeasureSpec)); }
  16. @Override protected void onLayout(boolean changed, int l, int t, int

    r, int b) { final int paddingLeft = getPaddingLeft(); final int paddingTop = getPaddingTop(); final int paddingRight = getPaddingRight(); int spaceConsumedLeft = paddingLeft; //layout the first view on the left : layoutView(cover, spaceConsumedLeft, paddingTop, cover.getMeasuredWidth(), cover.getMeasuredHeight()); ... } private void layoutView(View view, int l, int t, int width, int height) { final MarginLayoutParams margins = (MarginLayoutParams) view.getLayoutParams(); final int leftWithMargins = l + margins.leftMargin; final int topWithMargins = t + margins.topMargin; view.layout(leftWithMargins, topWithMargins, leftWithMargins + width, topWithMargins + height); }
  17. @Override protected void onLayout(boolean changed, int l, int t, int

    r, int b) { final int paddingLeft = getPaddingLeft(); final int paddingTop = getPaddingTop(); final int paddingRight = getPaddingRight(); int spaceConsumedLeft = paddingLeft; //layout the first view on the left : layoutView(cover,...); spaceConsumedLeft += getMeasuredWidthWithMargin(cover); //layout the overflow button on the right, centered vertically: layoutView(overflow, r - l - paddingRight - overflow.getMeasuredWidth(), (b - t - overflow.getMeasuredHeight()) / 2, overflow.getMeasuredWidth(), overflow.getMeasuredHeight()); int spaceConsumedRight += getMeasuredWidthWithMargin(overflow); ... }
  18. The result : • Exact same disposi/on • Flat View

    hierarchy • Op/mal measure & layout performances
  19. Another poten*al bo.leneck : TextView In some cases, TextView can

    have unsa4sfactory performances : • Long text to display • Combina4on of several Spans
  20. public class AlbumView extends ViewGroup { private TextLayoutHelper textHelper; @Override

    public boolean willNotDraw() { return false; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); textHelper.draw(canvas); } }
  21. TextView : 10 000 lines of code BUT does not

    know how to draw a text. It is delegated to an abstract class : android.text.Layout 3 implementa+ons : • BoringLayout • Sta.cLayout • DynamicLayout
  22. Did you say boring ? BoringLayout.Metrics boring = BoringLayout.isBoring(mText, mPaint);

    if (boring != null) { mLayout = BoringLayout.make(mText, mPaint, width, Layout.Alignment.ALIGN_NORMAL, SPACING_MULT, SPACING_ADD, boring, true, TextUtils.TruncateAt.END, width); }
  23. Did you say boring ? BoringLayout.Metrics boring = BoringLayout.isBoring(mText, mPaint);

    if (boring != null) { // this is boring ! if (mSavedLayout != null) { mLayout = mSavedLayout.replaceOrMake(mText, ...); } else { mLayout = BoringLayout.make(mText, ...); } mSavedLayout = (BoringLayout) mLayout;
  24. protected final void createLayout(final int width, final int hOffset) {

    BoringLayout.Metrics boring = BoringLayout.isBoring(mText, mPaint); if (boring != null) { // this is boring ! if (mSavedLayout != null) { mLayout = mSavedLayout.replaceOrMake(mText, ...); } else { mLayout = BoringLayout.make(mText, ...); } mSavedLayout = (BoringLayout) mLayout; } else { mLayout = new StaticLayout(mText, 0, mText.length(), mPaint, width, Layout.Alignment.ALIGN_NORMAL, SPACING_MULT, SPACING_ADD, true, TextUtils.TruncateAt.END, width, MAX_LINES); }
  25. Finally, let's draw : public final void draw(final @NonNull Canvas

    canvas) { if (TextUtils.isEmpty(mText)) return; final int saveCount = canvas.save(); canvas.clipRect(mClipBounds); canvas.translate(mClipBounds.left, mClipBounds.top); mLayout.draw(canvas); canvas.restoreToCount(saveCount); }
  26. Advantages : • Total control over the drawing logic, allowing

    even more customiza7on than with Spans • Allows to mul7thread the text layou7ng logic : instead of crea7ng the text layouts on the main thread, we can create them on a Worker Thread and cache the result.