Advanced Animations & ConstraintLayout

Advanced Animations & ConstraintLayout

Advanced animations with ConstraintLayout

B9012970f22b84c5b344ffa6f8a884d5?s=128

Nicolas Roard

November 06, 2017
Tweet

Transcript

  1. Advanced Animations & ConstraintLayout Nicolas Roard @camaelon John Hoford @johnhoford

    .droidconSF 2017
  2. Overview

  3. Android Studio

  4. ConstraintLayout

  5. Where are we now

  6. Where are we now Android Studio 3.0 just shipped!

  7. Where are we now Android Studio 3.0 just shipped! ConstraintLayout

    1.0.2 stable
  8. Where are we now Android Studio 3.0 just shipped! ConstraintLayout

    1.0.2 stable ConstraintLayout 1.1 beta 3
  9. ConstraintLayout

  10. ConstraintLayout Relative positioning

  11. ConstraintLayout Relative positioning

  12. ConstraintLayout Center positioning & bias

  13. ConstraintLayout Center positioning & bias

  14. ConstraintLayout Ratio

  15. ConstraintLayout Ratio

  16. ConstraintLayout Chains

  17. ConstraintLayout Chains

  18. ConstraintLayout Guidelines

  19. ConstraintLayout Guidelines

  20. Constraints Vocabulary • Relative positioning • Center + bias •

    Dimension (e.g. ratio) • Group behavior (chains) • Helper objects (guidelines)
  21. Direct Manipulation

  22. Direct Manipulation Change parameters 1

  23. Direct Manipulation Change parameters 1 Use TransitionManager 2

  24. Direct Manipulation //Find the view you are over and set

    minimum height View child = layout.getChildAt(current); B
  25. Direct Manipulation //Find the view you are over and set

    minimum height View child = layout.getChildAt(current); B child.setMinimumHeight(400); C
  26. Direct Manipulation //Find the view you are over and set

    minimum height TransitionManager.beginDelayedTransition(layout); A View child = layout.getChildAt(current); B child.setMinimumHeight(400); C
  27. Direct Manipulation //Find the view you are over and set

    minimum height TransitionManager.beginDelayedTransition(layout); A View child = layout.getChildAt(current); B child.setMinimumHeight(400); C
  28. Direct Manipulation //Find the view you are over and set

    minimum height TransitionManager.beginDelayedTransition(layout); A View child = layout.getChildAt(current); B child.setMinimumHeight(400); C
  29. Use of GONE

  30. Use of GONE

  31. More examples Huyen Tue Dao’s talk at ChicagoRoboto: Cool ConstraintLayout

    https://speakerdeck.com/queencodemonkey/chicago-roboto-2017-cool- constraintlayout http://chicagoroboto.com/session-videos/
  32. Guideline Manipulation

  33. Direct Manipulation : Guideline

  34. Direct Manipulation : Guideline Guideline are powerful elements

  35. Direct Manipulation : Guideline Guideline are powerful elements Specify exact

    positioning or percent value
  36. Direct Manipulation : Guideline Guideline are powerful elements Specify exact

    positioning or percent value Parameterize a layout via guidelines
  37. Guidelines can be your Layout Parameters

  38. None
  39. final Guideline guideline = findViewById(R.id.guideline);

  40. final Guideline guideline = findViewById(R.id.guideline); final int end = ((LayoutParams)

    guideline.getLayoutParams()).guideEnd;
  41. final Guideline guideline = findViewById(R.id.guideline); final int end = ((LayoutParams)

    guideline.getLayoutParams()).guideEnd; ValueAnimator anim = ValueAnimator.ofInt(0, end); anim.addUpdateListener((animator) -> { }); anim.start();
  42. final Guideline guideline = findViewById(R.id.guideline); final int end = ((LayoutParams)

    guideline.getLayoutParams()).guideEnd; ValueAnimator anim = ValueAnimator.ofInt(0, end); anim.addUpdateListener((animator) -> { LayoutParams lp = (LayoutParams) guideline.getLayoutParams(); lp.guideEnd = (Integer) animator.getAnimatedValue(); guideline.setLayoutParams(lp); }); anim.start();
  43. final Guideline guideline = findViewById(R.id.guideline); final int end = ((LayoutParams)

    guideline.getLayoutParams()).guideEnd; ValueAnimator anim = ValueAnimator.ofInt(0, end); anim.setDuration(2000); anim.setInterpolator(new BounceInterpolator()); anim.addUpdateListener((animator) -> { LayoutParams lp = (LayoutParams) guideline.getLayoutParams(); lp.guideEnd = (Integer) animator.getAnimatedValue(); guideline.setLayoutParams(lp); }); anim.start();
  44. final Guideline guideline = findViewById(R.id.guideline); final int end = ((LayoutParams)

    guideline.getLayoutParams()).guideEnd; ObjectAnimator anim = ObjectAnimator.ofInt(g, "GuidelineEnd", 0, end); anim.setDuration(2000); anim.setInterpolator(new BounceInterpolator()); anim.start(); New in 1.1
  45. None
  46. None
  47. Parallax Effect

  48. None
  49. base.setOnTouchListener(new View.OnTouchListener() { public boolean onTouch(View view, MotionEvent motionEvent) {

    switch (motionEvent.getActionMasked( )) { case MotionEvent.ACTION_MOVE: break; } A return true; } B });
  50. base.setOnTouchListener(new View.OnTouchListener() { public boolean onTouch(View view, MotionEvent motionEvent) {

    switch (motionEvent.getActionMasked( )) { case MotionEvent.ACTION_MOVE: LayoutParams params = (LayoutParams) guideline.getLayoutParams(); params.guideBegin = (int) motionEvent.getX(); guideline.setLayoutParams(params); break; } A return true; } B });
  51. Guideline Manipulation

  52. Guideline Manipulation

  53. Parallax Effect

  54. Parallax Effect

  55. Parallax Effect

  56. recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView?, dx: Int,

    dy: Int) { super.onScrolled(recyclerView, dx, dy) a} })
  57. recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView?, dx: Int,

    dy: Int) { super.onScrolled(recyclerView, dx, dy) val manager = recyclerView!!.layoutManager as LinearLayoutManager val position = manager.findFirstVisibleItemPosition() val lastPosition = manager.findLastVisibleItemPosition() val offsetX = recyclerView.computeHorizontalScrollOffset() for (i in 0..lastPosition - position) { val layout = manager.findViewByPosition(position + i) as ConstraintLayout val guideline = layout.findViewById<Guideline>(R.id.guideline) val params = guideline.layoutParams as ConstraintLayout.LayoutParams val w = recyclerView.width val deltaPos = offsetX - (position + i) * w val percent = deltaPos / w.toFloat() params.guidePercent = Math.max(0.3f, Math.min(0.7f, 0.5f - percent)) guideline.layoutParams = params } a} })
  58. Parallax Effect

  59. Parallax Effect

  60. recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView?, dx: Int,

    dy: Int) { super.onScrolled(recyclerView, dx, dy) val manager = recyclerView!!.layoutManager as LinearLayoutManager val position = manager.findFirstVisibleItemPosition() val lastPosition = manager.findLastVisibleItemPosition() val offsetX = recyclerView.computeHorizontalScrollOffset() for (i in 0..lastPosition - position) { val layout = manager.findViewByPosition(position + i) as ConstraintLayout val guideline = layout.findViewById<Guideline>(R.id.guideline) val params = guideline.layoutParams as ConstraintLayout.LayoutParams val w = recyclerView.width val deltaPos = offsetX - (position + i) * w val percent = deltaPos / w.toFloat() params.guidePercent = Math.max(0.3f, Math.min(0.7f, 0.5f - percent)) guideline.layoutParams = params } a} })
  61. recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView?, dx: Int,

    dy: Int) { super.onScrolled(recyclerView, dx, dy) val manager = recyclerView!!.layoutManager as LinearLayoutManager val position = manager.findFirstVisibleItemPosition() val lastPosition = manager.findLastVisibleItemPosition() val offsetX = recyclerView.computeHorizontalScrollOffset() for (i in 0..lastPosition - position) { val layout = manager.findViewByPosition(position + i) as ConstraintLayout val guideline = layout.findViewById<Guideline>(R.id.guideline) val params = guideline.layoutParams as ConstraintLayout.LayoutParams val w = recyclerView.width val deltaPos = offsetX - (position + i) * w val percent = deltaPos / w.toFloat() params.guidePercent = Math.max(0.3f, Math.min(0.7f, 0.5f + percent)) guideline.layoutParams = params } a} })
  62. Parallax Effect: inversion

  63. Parallax Effect: inversion

  64. ConstraintSet

  65. ConstraintSet Contains all your constraints

  66. ConstraintSet Contains all your constraints Programming API

  67. ConstraintSet Contains all your constraints Programming API Clone from live

    layout or XML
  68. Initializing a ConstraintSet Layout resource set.clone(context, R.layout.main) Live layout set.clone(findViewById(R.id.main))

    XML file set.load(context, R.xml.main)
  69. Applying a ConstraintSet set.applyTo(layout)

  70. ConstraintSet

  71. ConstraintSet ConstraintSet mConstraintSet1 = new ConstraintSet(); // create a Constraint

    Set ConstraintSet mConstraintSet2 = new ConstraintSet(); // create a Constraint Set Initialization
  72. ConstraintSet ConstraintSet mConstraintSet1 = new ConstraintSet(); // create a Constraint

    Set ConstraintSet mConstraintSet2 = new ConstraintSet(); // create a Constraint Set mConstraintSet2.clone(context, R.layout.state2); // get constraints from layout setContentView(R.layout.state1); mConstraintLayout = (ConstraintLayout) findViewById(R.id.activity_main); mConstraintSet1.clone(mConstraintLayout); // get constraints from ConstraintSet Initialization onCreate
  73. ConstraintSet ConstraintSet mConstraintSet1 = new ConstraintSet(); // create a Constraint

    Set ConstraintSet mConstraintSet2 = new ConstraintSet(); // create a Constraint Set mConstraintSet2.clone(context, R.layout.state2); // get constraints from layout setContentView(R.layout.state1); mConstraintLayout = (ConstraintLayout) findViewById(R.id.activity_main); mConstraintSet1.clone(mConstraintLayout); // get constraints from ConstraintSet mConstraintSet1.applyTo(mConstraintLayout); Initialization onCreate To change state
  74. ConstraintSet ConstraintSet mConstraintSet1 = new ConstraintSet(); // create a Constraint

    Set ConstraintSet mConstraintSet2 = new ConstraintSet(); // create a Constraint Set mConstraintSet2.clone(context, R.layout.state2); // get constraints from layout setContentView(R.layout.state1); mConstraintLayout = (ConstraintLayout) findViewById(R.id.activity_main); mConstraintSet1.clone(mConstraintLayout); // get constraints from ConstraintSet TransitionManager.beginDelayedTransition(mConstraintLayout); mConstraintSet1.applyTo(mConstraintLayout); Initialization onCreate To change state
  75. Build two layouts

  76. Build two layouts

  77. Build two layouts

  78. Build two layouts

  79. Add Custom Transition static public class MyTransition extends TransitionSet {

    { setDuration(1000); setOrdering(ORDERING_SEQUENTIAL); addTransition(new TransitionSet() { { addTransition(new Fade(Fade.OUT)); addTransition(new ChangeBounds()); } }); addTransition(new Fade(Fade.IN)); } } private void animate(ConstraintLayout cl, ConstraintSet set) { TransitionManager.beginDelayedTransition(cl, new MyTransition()); set.applyTo(cl); }
  80. Add Custom Transition static public class MyTransition extends TransitionSet {

    { setDuration(1000); setOrdering(ORDERING_SEQUENTIAL); addTransition(new TransitionSet() { { addTransition(new Fade(Fade.OUT)); addTransition(new ChangeBounds()); } }); addTransition(new Fade(Fade.IN)); } } private void animate(ConstraintLayout cl, ConstraintSet set) { TransitionManager.beginDelayedTransition(cl, new MyTransition()); set.applyTo(cl); }
  81. Custom Transition 2 static public class MyTransition extends TransitionSet {

    { setDuration(1000); setOrdering(ORDERING_SEQUENTIAL); addTransition(new TransitionSet() { { addTransition(new Fade(Fade.OUT)); addTransition(new ChangeBounds()); addTransition(new Fade(Fade.IN)); } }); } } private void animate(ConstraintLayout cl, ConstraintSet set) { TransitionManager.beginDelayedTransition(cl, new MyTransition()); set.applyTo(cl); }
  82. Custom Transition 2 static public class MyTransition extends TransitionSet {

    { setDuration(1000); setOrdering(ORDERING_SEQUENTIAL); addTransition(new TransitionSet() { { addTransition(new Fade(Fade.OUT)); addTransition(new ChangeBounds()); addTransition(new Fade(Fade.IN)); } }); } } private void animate(ConstraintLayout cl, ConstraintSet set) { TransitionManager.beginDelayedTransition(cl, new MyTransition()); set.applyTo(cl); }
  83. Add Custom Transition static public class MyTransition extends TransitionSet {

    { setDuration(1000); setOrdering(ORDERING_SEQUENTIAL); addTransition(new TransitionSet() { { addTransition(new Fade(Fade.OUT)); ChangeBounds move = new ChangeBounds(); move.setInterpolator(new BounceInterpolator()); addTransition(move); } }); addTransition(new Fade(Fade.IN)); } }
  84. Add Custom Transition static public class MyTransition extends TransitionSet {

    { setDuration(1000); setOrdering(ORDERING_SEQUENTIAL); addTransition(new TransitionSet() { { addTransition(new Fade(Fade.OUT)); ChangeBounds move = new ChangeBounds(); move.setInterpolator(new BounceInterpolator()); addTransition(move); } }); addTransition(new Fade(Fade.IN)); } }
  85. Add Custom Transition Very custom…

  86. Add Custom Transition Very custom…

  87. Collapsible Toolbar

  88. None
  89. None
  90. Collapsible Toolbar CoordinatorLayout AppBarLayout CollapsingToolbarLayout

  91. ConstraintLayout Collapsible Toolbar CoordinatorLayout AppBarLayout

  92. XML <android.support.design.widget.CoordinatorLayout … > <android.support.design.widget.AppBarLayout android:id="@+id/app_bar" android:layout_width="match_parent" android:layout_height="@dimen/app_bar_height" android:fitsSystemWindows=“false" …

    > <android.support.design.widget.CollapsingToolbarLayout android:id=“@+id/toolbar_layout" … > </android.support.design.widget.AppBarLayout> <include layout="@layout/content_scrolling" /> <android.support.design.widget.FloatingActionButton /> </android.support.design.widget.CoordinatorLayout>
  93. <android.support.design.widget.CoordinatorLayout … > <android.support.design.widget.AppBarLayout android:id="@+id/app_bar" android:layout_width="match_parent" android:layout_height="@dimen/app_bar_height" android:fitsSystemWindows=“false" … >

    <android.support.constraint.ConstraintLayout android:id=“@+id/toolbar_layout" … > </android.support.design.widget.AppBarLayout> <include layout="@layout/content_scrolling" /> <android.support.design.widget.FloatingActionButton /> </android.support.design.widget.CoordinatorLayout> XML
  94. <android.support.design.widget.CoordinatorLayout … > <android.support.design.widget.AppBarLayout android:id="@+id/app_bar" android:layout_width="match_parent" android:layout_height="@dimen/app_bar_height" android:fitsSystemWindows=“false" … >

    <include layout=“@+id/toolbar" /> </android.support.design.widget.AppBarLayout> <include layout="@layout/content_scrolling" /> <android.support.design.widget.FloatingActionButton /> </android.support.design.widget.CoordinatorLayout> XML
  95. None
  96. None
  97. None
  98. None
  99. XML <android.support.constraint.ConstraintLayout android:id="@+id/constraintToolbar" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" android:minHeight="40dp" app:layout_scrollFlags="enterAlways|snap">

  100. XML <android.support.constraint.ConstraintLayout android:id="@+id/constraintToolbar" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" android:minHeight="40dp" app:layout_scrollFlags="scroll|enterAlways|snap|exitUntilCollapsed">

  101. None
  102. None
  103. Using ConstraintSet …& Kotlin

  104. Open ConstraintSet Closed ConstraintSet open.xml closed.xml

  105. class NoAnimCollapsibleConstraintLayout : ConstraintLayout, AppBarLayout.OnOffsetChangedListener { constructor(context: Context) : this(context,

    null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {} private var mTransitionThreshold = 0.35f private var mLastPosition : Int = 0 private var mToolbarOpen = true private val mOpenToolbarSet: ConstraintSet = ConstraintSet() private val mCloseToolbarSet: ConstraintSet = ConstraintSet() override fun onAttachedToWindow() { super.onAttachedToWindow() if (parent is AppBarLayout) { var appBarLayout = parent as AppBarLayout appBarLayout.addOnOffsetChangedListener(this) mOpenToolbarSet.clone(context, R.layout.open) mCloseToolbarSet.clone(context, R.layout.close) } } override fun onOffsetChanged(appBarLayout: AppBarLayout?, verticalOffset: Int) { if (mLastPosition == verticalOffset) { return } mLastPosition = verticalOffset val progress = Math.abs(verticalOffset / appBarLayout?.getHeight()?.toFloat()!!) val params = getLayoutParams() as AppBarLayout.LayoutParams params.topMargin = -verticalOffset setLayoutParams(params) if (mToolbarOpen && progress > mTransitionThreshold) { mCloseToolbarSet.applyTo(this) mToolbarOpen = false } else if (!mToolbarOpen && progress < mTransitionThreshold) { mOpenToolbarSet.applyTo(this) mToolbarOpen = true } } }
  106. None
  107. None
  108. class NoAnimCollapsibleConstraintLayout : ConstraintLayout, AppBarLayout.OnOffsetChangedListener { constructor(context: Context) : this(context,

    null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {} private var mTransitionThreshold = 0.35f private var mLastPosition : Int = 0 private var mToolbarOpen = true private val mOpenToolbarSet: ConstraintSet = ConstraintSet() private val mCloseToolbarSet: ConstraintSet = ConstraintSet() override fun onAttachedToWindow() { super.onAttachedToWindow() if (parent is AppBarLayout) { var appBarLayout = parent as AppBarLayout appBarLayout.addOnOffsetChangedListener(this) mOpenToolbarSet.clone(context, R.layout.open) mCloseToolbarSet.clone(context, R.layout.close) } } override fun onOffsetChanged(appBarLayout: AppBarLayout?, verticalOffset: Int) { if (mLastPosition == verticalOffset) { return } mLastPosition = verticalOffset val progress = Math.abs(verticalOffset / appBarLayout?.getHeight()?.toFloat()!!) val params = getLayoutParams() as AppBarLayout.LayoutParams params.topMargin = -verticalOffset setLayoutParams(params) if (mToolbarOpen && progress > mTransitionThreshold) { mCloseToolbarSet.applyTo(this) mToolbarOpen = false } else if (!mToolbarOpen && progress < mTransitionThreshold) { mOpenToolbarSet.applyTo(this) mToolbarOpen = true } } } Subclass of ConstraintLayout Implements OnOffsetChangedListener 1 2
  109. class NoAnimCollapsibleConstraintLayout : ConstraintLayout, AppBarLayout.OnOffsetChangedListener { constructor(context: Context) : this(context,

    null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {} private var mTransitionThreshold = 0.35f private var mLastPosition : Int = 0 private var mToolbarOpen = true private val mOpenToolbarSet: ConstraintSet = ConstraintSet() private val mCloseToolbarSet: ConstraintSet = ConstraintSet() override fun onAttachedToWindow() { super.onAttachedToWindow() if (parent is AppBarLayout) { var appBarLayout = parent as AppBarLayout appBarLayout.addOnOffsetChangedListener(this) mOpenToolbarSet.clone(context, R.layout.open) mCloseToolbarSet.clone(context, R.layout.close) } } override fun onOffsetChanged(appBarLayout: AppBarLayout?, verticalOffset: Int) { if (mLastPosition == verticalOffset) { return } mLastPosition = verticalOffset val progress = Math.abs(verticalOffset / appBarLayout?.getHeight()?.toFloat()!!) val params = getLayoutParams() as AppBarLayout.LayoutParams params.topMargin = -verticalOffset setLayoutParams(params) if (mToolbarOpen && progress > mTransitionThreshold) { mCloseToolbarSet.applyTo(this) mToolbarOpen = false } else if (!mToolbarOpen && progress < mTransitionThreshold) { mOpenToolbarSet.applyTo(this) mToolbarOpen = true } } } appBarLayout.addOnOffsetChangedListener(this) mOpenToolbarSet.clone(context, R.layout.open) mCloseToolbarSet.clone(context, R.layout.close) Implements OnOffsetChangedListener 3 Load the ConstraintSet 4
  110. class NoAnimCollapsibleConstraintLayout : ConstraintLayout, AppBarLayout.OnOffsetChangedListener { constructor(context: Context) : this(context,

    null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {} private var mTransitionThreshold = 0.35f private var mLastPosition : Int = 0 private var mToolbarOpen = true private val mOpenToolbarSet: ConstraintSet = ConstraintSet() private val mCloseToolbarSet: ConstraintSet = ConstraintSet() override fun onAttachedToWindow() { super.onAttachedToWindow() if (parent is AppBarLayout) { var appBarLayout = parent as AppBarLayout appBarLayout.addOnOffsetChangedListener(this) mOpenToolbarSet.clone(context, R.layout.open) mCloseToolbarSet.clone(context, R.layout.close) } } override fun onOffsetChanged(appBarLayout: AppBarLayout?, verticalOffset: Int) { if (mLastPosition == verticalOffset) { return } mLastPosition = verticalOffset val progress = Math.abs(verticalOffset / appBarLayout?.getHeight()?.toFloat()!!) val params = getLayoutParams() as AppBarLayout.LayoutParams params.topMargin = -verticalOffset setLayoutParams(params) if (mToolbarOpen && progress > mTransitionThreshold) { mCloseToolbarSet.applyTo(this) mToolbarOpen = false } else if (!mToolbarOpen && progress < mTransitionThreshold) { mOpenToolbarSet.applyTo(this) mToolbarOpen = true } } } Offset the Content 5 val params = getLayoutParams() as AppBarLayout.LayoutParams params.topMargin = -verticalOffset setLayoutParams(params)
  111. class NoAnimCollapsibleConstraintLayout : ConstraintLayout, AppBarLayout.OnOffsetChangedListener { constructor(context: Context) : this(context,

    null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {} private var mTransitionThreshold = 0.35f private var mLastPosition : Int = 0 private var mToolbarOpen = true private val mOpenToolbarSet: ConstraintSet = ConstraintSet() private val mCloseToolbarSet: ConstraintSet = ConstraintSet() override fun onAttachedToWindow() { super.onAttachedToWindow() if (parent is AppBarLayout) { var appBarLayout = parent as AppBarLayout appBarLayout.addOnOffsetChangedListener(this) mOpenToolbarSet.clone(context, R.layout.open) mCloseToolbarSet.clone(context, R.layout.close) } } override fun onOffsetChanged(appBarLayout: AppBarLayout?, verticalOffset: Int) { if (mLastPosition == verticalOffset) { return } mLastPosition = verticalOffset val progress = Math.abs(verticalOffset / appBarLayout?.getHeight()?.toFloat()!!) val params = getLayoutParams() as AppBarLayout.LayoutParams params.topMargin = -verticalOffset setLayoutParams(params) if (mToolbarOpen && progress > mTransitionThreshold) { mCloseToolbarSet.applyTo(this) mToolbarOpen = false } else if (!mToolbarOpen && progress < mTransitionThreshold) { mOpenToolbarSet.applyTo(this) mToolbarOpen = true } } } Toggle the ConstraintSet 6 if (mToolbarOpen && progress > mTransitionThreshold) { mCloseToolbarSet.applyTo(this) mToolbarOpen = false } else if (!mToolbarOpen && progress < mTransitionThreshold) { mOpenToolbarSet.applyTo(this) mToolbarOpen = true }
  112. None
  113. None
  114. Adding some animation…

  115. Animating content

  116. class AnimationHelper(view : View){ var initialValue = 0 var target

    = view init { initialValue = target.left } fun evaluate() { if (initialValue != target.left) { var delta = (initialValue - target.left).toFloat() val anim = ObjectAnimator.ofFloat(target, "translationX", delta, 0f) anim.duration = 400 anim.start() initialValue = target.left } } }
  117. class AnimationHelper(view : View){ var initialValue = 0 var target

    = view init { initialValue = target.left } fun evaluate() { if (initialValue != target.left) { var delta = (initialValue - target.left).toFloat() val anim = ObjectAnimator.ofFloat(target, "translationX", delta, 0f) anim.duration = 400 anim.start() initialValue = target.left } } }
  118. class AnimationHelper(view : View){ var initialValue = 0 var target

    = view init { initialValue = target.left } fun evaluate() { if (initialValue != target.left) { var delta = (initialValue - target.left).toFloat() val anim = ObjectAnimator.ofFloat(target, "translationX", delta, 0f) anim.duration = 400 anim.start() initialValue = target.left } } }
  119. class AnimationHelper(view : View){ var initialValue = 0 var target

    = view init { initialValue = target.left } fun evaluate() { if (initialValue != target.left) { var delta = (initialValue - target.left).toFloat() val anim = ObjectAnimator.ofFloat(target, "translationX", delta, 0f) anim.duration = 400 anim.start() initialValue = target.left } } }
  120. Animating background

  121. Animating the alpha channel mBackground = findViewById(R.id.background) … showImageAnimator =

    ObjectAnimator.ofFloat(mBackground, "alpha", 0f, 1f) showImageAnimator?.duration = 600 … showImageAnimator?.start()
  122. Now all together!

  123. class CollapsibleConstraintLayout : ConstraintLayout, AppBarLayout.OnOffsetChangedListener { constructor(context: Context) : this(context,

    null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {} private var mLastPosition : Int = 0 private var mToolbarOpen = true private var mTransitionThreshold = 0.35f private val mOpenToolbarSet: ConstraintSet = ConstraintSet() private val mCloseToolbarSet: ConstraintSet = ConstraintSet() private var mBackground: ImageView? = null private var mTitle : TextView? = null private var mIcon : ImageView? = null private var mTranslationTitle : AnimationHelper? = null private var mTranslationIcon : AnimationHelper? = null private var showImageAnimator : Animator? = null private var hideImageAnimator : Animator? = null class AnimationHelper(view : View){ var initialValue = 0 var target = view init { initialValue = target.left } fun evaluate() { if (initialValue != target.left) { var delta = (initialValue - target.left).toFloat() val anim = ObjectAnimator.ofFloat(target, "translationX", delta, 0f) anim.duration = 400 anim.start() initialValue = target.left } } } override fun onAttachedToWindow() { super.onAttachedToWindow() if (false && parent is AppBarLayout) { var appBarLayout = parent as AppBarLayout appBarLayout.addOnOffsetChangedListener(this) mOpenToolbarSet.clone(context, R.layout.open) mCloseToolbarSet.clone(context, R.layout.close) mBackground = findViewById(R.id.background) mTitle = findViewById(R.id.name) mIcon = findViewById(R.id.icon) showImageAnimator = ObjectAnimator.ofFloat(mBackground, "alpha", 0f, 1f) showImageAnimator?.duration = 600 hideImageAnimator = ObjectAnimator.ofFloat(mBackground, "alpha", 1f, 0f) hideImageAnimator?.duration = 600 } } override fun onOffsetChanged(appBarLayout: AppBarLayout?, verticalOffset: Int) { if (mLastPosition == verticalOffset) { return } mLastPosition = verticalOffset val progress = Math.abs(verticalOffset / appBarLayout?.getHeight()?.toFloat()!!) val params = getLayoutParams() as AppBarLayout.LayoutParams params.topMargin = -verticalOffset setLayoutParams(params) if (mToolbarOpen && progress > mTransitionThreshold) { mCloseToolbarSet.applyTo(this) hideImageAnimator?.start() mToolbarOpen = false } else if (!mToolbarOpen && progress < mTransitionThreshold) { mOpenToolbarSet.applyTo(this) showImageAnimator?.start() mToolbarOpen = true } } override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { super.onLayout(changed, left, top, right, bottom) if (mTitle != null && mTranslationTitle == null) { mTranslationTitle = AnimationHelper(mTitle!!) } if (mIcon != null && mTranslationIcon == null) { mTranslationIcon = AnimationHelper(mIcon!!) } mTranslationTitle?.evaluate() mTranslationIcon?.evaluate() } } AnimationHelper 1
  124. class CollapsibleConstraintLayout : ConstraintLayout, AppBarLayout.OnOffsetChangedListener { constructor(context: Context) : this(context,

    null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {} private var mLastPosition : Int = 0 private var mToolbarOpen = true private var mTransitionThreshold = 0.35f private val mOpenToolbarSet: ConstraintSet = ConstraintSet() private val mCloseToolbarSet: ConstraintSet = ConstraintSet() private var mBackground: ImageView? = null private var mTitle : TextView? = null private var mIcon : ImageView? = null private var mTranslationTitle : AnimationHelper? = null private var mTranslationIcon : AnimationHelper? = null private var showImageAnimator : Animator? = null private var hideImageAnimator : Animator? = null class AnimationHelper(view : View){ var initialValue = 0 var target = view init { initialValue = target.left } fun evaluate() { if (initialValue != target.left) { var delta = (initialValue - target.left).toFloat() val anim = ObjectAnimator.ofFloat(target, "translationX", delta, 0f) anim.duration = 400 anim.start() initialValue = target.left } } } override fun onAttachedToWindow() { super.onAttachedToWindow() if (false && parent is AppBarLayout) { var appBarLayout = parent as AppBarLayout appBarLayout.addOnOffsetChangedListener(this) mOpenToolbarSet.clone(context, R.layout.open) mCloseToolbarSet.clone(context, R.layout.close) mBackground = findViewById(R.id.background) mTitle = findViewById(R.id.name) mIcon = findViewById(R.id.icon) showImageAnimator = ObjectAnimator.ofFloat(mBackground, "alpha", 0f, 1f) showImageAnimator?.duration = 600 hideImageAnimator = ObjectAnimator.ofFloat(mBackground, "alpha", 1f, 0f) hideImageAnimator?.duration = 600 } } override fun onOffsetChanged(appBarLayout: AppBarLayout?, verticalOffset: Int) { if (mLastPosition == verticalOffset) { return } mLastPosition = verticalOffset val progress = Math.abs(verticalOffset / appBarLayout?.getHeight()?.toFloat()!!) val params = getLayoutParams() as AppBarLayout.LayoutParams params.topMargin = -verticalOffset setLayoutParams(params) if (mToolbarOpen && progress > mTransitionThreshold) { mCloseToolbarSet.applyTo(this) hideImageAnimator?.start() mToolbarOpen = false } else if (!mToolbarOpen && progress < mTransitionThreshold) { mOpenToolbarSet.applyTo(this) showImageAnimator?.start() mToolbarOpen = true } } override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { super.onLayout(changed, left, top, right, bottom) if (mTitle != null && mTranslationTitle == null) { mTranslationTitle = AnimationHelper(mTitle!!) } if (mIcon != null && mTranslationIcon == null) { mTranslationIcon = AnimationHelper(mIcon!!) } mTranslationTitle?.evaluate() mTranslationIcon?.evaluate() } } onLayout 2 override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { super.onLayout(changed, left, top, right, bottom) if (mTitle != null && mTranslationTitle == null) { mTranslationTitle = AnimationHelper(mTitle!!) } if (mIcon != null && mTranslationIcon == null) { mTranslationIcon = AnimationHelper(mIcon!!) } mTranslationTitle?.evaluate() mTranslationIcon?.evaluate() }
  125. class CollapsibleConstraintLayout : ConstraintLayout, AppBarLayout.OnOffsetChangedListener { constructor(context: Context) : this(context,

    null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {} private var mLastPosition : Int = 0 private var mToolbarOpen = true private var mTransitionThreshold = 0.35f private val mOpenToolbarSet: ConstraintSet = ConstraintSet() private val mCloseToolbarSet: ConstraintSet = ConstraintSet() private var mBackground: ImageView? = null private var mTitle : TextView? = null private var mIcon : ImageView? = null private var mTranslationTitle : AnimationHelper? = null private var mTranslationIcon : AnimationHelper? = null private var showImageAnimator : Animator? = null private var hideImageAnimator : Animator? = null class AnimationHelper(view : View){ var initialValue = 0 var target = view init { initialValue = target.left } fun evaluate() { if (initialValue != target.left) { var delta = (initialValue - target.left).toFloat() val anim = ObjectAnimator.ofFloat(target, "translationX", delta, 0f) anim.duration = 400 anim.start() initialValue = target.left } } } override fun onAttachedToWindow() { super.onAttachedToWindow() if (false && parent is AppBarLayout) { var appBarLayout = parent as AppBarLayout appBarLayout.addOnOffsetChangedListener(this) mOpenToolbarSet.clone(context, R.layout.open) mCloseToolbarSet.clone(context, R.layout.close) mBackground = findViewById(R.id.background) mTitle = findViewById(R.id.name) mIcon = findViewById(R.id.icon) showImageAnimator = ObjectAnimator.ofFloat(mBackground, "alpha", 0f, 1f) showImageAnimator?.duration = 600 hideImageAnimator = ObjectAnimator.ofFloat(mBackground, "alpha", 1f, 0f) hideImageAnimator?.duration = 600 } } override fun onOffsetChanged(appBarLayout: AppBarLayout?, verticalOffset: Int) { if (mLastPosition == verticalOffset) { return } mLastPosition = verticalOffset val progress = Math.abs(verticalOffset / appBarLayout?.getHeight()?.toFloat()!!) val params = getLayoutParams() as AppBarLayout.LayoutParams params.topMargin = -verticalOffset setLayoutParams(params) if (mToolbarOpen && progress > mTransitionThreshold) { mCloseToolbarSet.applyTo(this) hideImageAnimator?.start() mToolbarOpen = false } else if (!mToolbarOpen && progress < mTransitionThreshold) { mOpenToolbarSet.applyTo(this) showImageAnimator?.start() mToolbarOpen = true } } override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { super.onLayout(changed, left, top, right, bottom) if (mTitle != null && mTranslationTitle == null) { mTranslationTitle = AnimationHelper(mTitle!!) } if (mIcon != null && mTranslationIcon == null) { mTranslationIcon = AnimationHelper(mIcon!!) } mTranslationTitle?.evaluate() mTranslationIcon?.evaluate() } } onLayout 2 override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { super.onLayout(changed, left, top, right, bottom) if (mTitle != null && mTranslationTitle == null) { mTranslationTitle = AnimationHelper(mTitle!!) } if (mIcon != null && mTranslationIcon == null) { mTranslationIcon = AnimationHelper(mIcon!!) } mTranslationTitle?.evaluate() mTranslationIcon?.evaluate() }
  126. class CollapsibleConstraintLayout : ConstraintLayout, AppBarLayout.OnOffsetChangedListener { constructor(context: Context) : this(context,

    null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {} private var mLastPosition : Int = 0 private var mToolbarOpen = true private var mTransitionThreshold = 0.35f private val mOpenToolbarSet: ConstraintSet = ConstraintSet() private val mCloseToolbarSet: ConstraintSet = ConstraintSet() private var mBackground: ImageView? = null private var mTitle : TextView? = null private var mIcon : ImageView? = null private var mTranslationTitle : AnimationHelper? = null private var mTranslationIcon : AnimationHelper? = null private var showImageAnimator : Animator? = null private var hideImageAnimator : Animator? = null class AnimationHelper(view : View){ var initialValue = 0 var target = view init { initialValue = target.left } fun evaluate() { if (initialValue != target.left) { var delta = (initialValue - target.left).toFloat() val anim = ObjectAnimator.ofFloat(target, "translationX", delta, 0f) anim.duration = 400 anim.start() initialValue = target.left } } } override fun onAttachedToWindow() { super.onAttachedToWindow() if (false && parent is AppBarLayout) { var appBarLayout = parent as AppBarLayout appBarLayout.addOnOffsetChangedListener(this) mOpenToolbarSet.clone(context, R.layout.open) mCloseToolbarSet.clone(context, R.layout.close) mBackground = findViewById(R.id.background) mTitle = findViewById(R.id.name) mIcon = findViewById(R.id.icon) showImageAnimator = ObjectAnimator.ofFloat(mBackground, "alpha", 0f, 1f) showImageAnimator?.duration = 600 hideImageAnimator = ObjectAnimator.ofFloat(mBackground, "alpha", 1f, 0f) hideImageAnimator?.duration = 600 } } override fun onOffsetChanged(appBarLayout: AppBarLayout?, verticalOffset: Int) { if (mLastPosition == verticalOffset) { return } mLastPosition = verticalOffset val progress = Math.abs(verticalOffset / appBarLayout?.getHeight()?.toFloat()!!) val params = getLayoutParams() as AppBarLayout.LayoutParams params.topMargin = -verticalOffset setLayoutParams(params) if (mToolbarOpen && progress > mTransitionThreshold) { mCloseToolbarSet.applyTo(this) hideImageAnimator?.start() mToolbarOpen = false } else if (!mToolbarOpen && progress < mTransitionThreshold) { mOpenToolbarSet.applyTo(this) showImageAnimator?.start() mToolbarOpen = true } } override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { super.onLayout(changed, left, top, right, bottom) if (mTitle != null && mTranslationTitle == null) { mTranslationTitle = AnimationHelper(mTitle!!) } if (mIcon != null && mTranslationIcon == null) { mTranslationIcon = AnimationHelper(mIcon!!) } mTranslationTitle?.evaluate() mTranslationIcon?.evaluate() } } onLayout 2 override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { super.onLayout(changed, left, top, right, bottom) if (mTitle != null && mTranslationTitle == null) { mTranslationTitle = AnimationHelper(mTitle!!) } if (mIcon != null && mTranslationIcon == null) { mTranslationIcon = AnimationHelper(mIcon!!) } mTranslationTitle?.evaluate() mTranslationIcon?.evaluate() }
  127. class CollapsibleConstraintLayout : ConstraintLayout, AppBarLayout.OnOffsetChangedListener { constructor(context: Context) : this(context,

    null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {} private var mLastPosition : Int = 0 private var mToolbarOpen = true private var mTransitionThreshold = 0.35f private val mOpenToolbarSet: ConstraintSet = ConstraintSet() private val mCloseToolbarSet: ConstraintSet = ConstraintSet() private var mBackground: ImageView? = null private var mTitle : TextView? = null private var mIcon : ImageView? = null private var mTranslationTitle : AnimationHelper? = null private var mTranslationIcon : AnimationHelper? = null private var showImageAnimator : Animator? = null private var hideImageAnimator : Animator? = null class AnimationHelper(view : View){ var initialValue = 0 var target = view init { initialValue = target.left } fun evaluate() { if (initialValue != target.left) { var delta = (initialValue - target.left).toFloat() val anim = ObjectAnimator.ofFloat(target, "translationX", delta, 0f) anim.duration = 400 anim.start() initialValue = target.left } } } override fun onAttachedToWindow() { super.onAttachedToWindow() if (false && parent is AppBarLayout) { var appBarLayout = parent as AppBarLayout appBarLayout.addOnOffsetChangedListener(this) mOpenToolbarSet.clone(context, R.layout.open) mCloseToolbarSet.clone(context, R.layout.close) mBackground = findViewById(R.id.background) mTitle = findViewById(R.id.name) mIcon = findViewById(R.id.icon) showImageAnimator = ObjectAnimator.ofFloat(mBackground, "alpha", 0f, 1f) showImageAnimator?.duration = 600 hideImageAnimator = ObjectAnimator.ofFloat(mBackground, "alpha", 1f, 0f) hideImageAnimator?.duration = 600 } } override fun onOffsetChanged(appBarLayout: AppBarLayout?, verticalOffset: Int) { if (mLastPosition == verticalOffset) { return } mLastPosition = verticalOffset val progress = Math.abs(verticalOffset / appBarLayout?.getHeight()?.toFloat()!!) val params = getLayoutParams() as AppBarLayout.LayoutParams params.topMargin = -verticalOffset setLayoutParams(params) if (mToolbarOpen && progress > mTransitionThreshold) { mCloseToolbarSet.applyTo(this) hideImageAnimator?.start() mToolbarOpen = false } else if (!mToolbarOpen && progress < mTransitionThreshold) { mOpenToolbarSet.applyTo(this) showImageAnimator?.start() mToolbarOpen = true } } override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { super.onLayout(changed, left, top, right, bottom) if (mTitle != null && mTranslationTitle == null) { mTranslationTitle = AnimationHelper(mTitle!!) } if (mIcon != null && mTranslationIcon == null) { mTranslationIcon = AnimationHelper(mIcon!!) } mTranslationTitle?.evaluate() mTranslationIcon?.evaluate() } } animating background alpha 3
  128. None
  129. None
  130. Collapsible Layout Summary • Use ConstraintLayout / ConstraintSet for flexibility

    • Piggy-back on CoordinatorLayout / AppBarLayout • Kotlin! • ObjectAnimator for property animation
  131. Helpers & Decorators

  132. Helpers ConstraintHelper

  133. Helpers ConstraintHelper View 1 View 2 View 3

  134. Helpers ConstraintHelper Barrier Group

  135. Example StaggeredAnimationGroup from Bartosz Lipiński https://github.com/blipinsk/StaggeredAnimationGroup

  136. Example StaggeredAnimationGroup from Bartosz Lipiński https://github.com/blipinsk/StaggeredAnimationGroup

  137. Decorators ConstraintHelper Decorator

  138. Decorator

  139. Decorator ConstraintHelper + View

  140. Decorator ConstraintHelper + View Gather draw operations in a single

    place
  141. Decorator ConstraintHelper + View Gather draw operations in a single

    place Draw something depending on multiple views
  142. Decorator public class MetaballsDecorator extends Decorator { public void updatePostLayout(ConstraintLayout

    container) { int[] ids = getReferencedIds(); final int count = ids.length; for (int i = 0; i < count; i++) { View view = container.getViewById(ids[i]); // do something } } @Override public void onDraw(Canvas canvas) { // do something } }
  143. Decorator Canvas ImageViews

  144. Decorator Canvas ImageViews

  145. Decorator Canvas ImageViews

  146. Decorator: Metaballs

  147. Decorator: Metaballs

  148. Decorator: Metaballs

  149. Decorator: Metaballs

  150. Let’s sum it all up…

  151. Let’s sum it all up… ConstraintLayout allows very flexible, declarative

    positioning
  152. Let’s sum it all up… ConstraintLayout allows very flexible, declarative

    positioning You can directly modify parameters to change your layout
  153. Let’s sum it all up… ConstraintLayout allows very flexible, declarative

    positioning You can directly modify parameters to change your layout Guidelines are a great entry point for customizing your layout
  154. Let’s sum it all up… ConstraintLayout allows very flexible, declarative

    positioning You can directly modify parameters to change your layout Guidelines are a great entry point for customizing your layout ConstraintSet allows you to easily manage multiple states of your layout
  155. Let’s sum it all up… ConstraintLayout allows very flexible, declarative

    positioning You can directly modify parameters to change your layout Guidelines are a great entry point for customizing your layout ConstraintSet allows you to easily manage multiple states of your layout TransitionManager will animate a lot for you automatically
  156. Let’s sum it all up… ConstraintLayout allows very flexible, declarative

    positioning You can directly modify parameters to change your layout Guidelines are a great entry point for customizing your layout ConstraintSet allows you to easily manage multiple states of your layout TransitionManager will animate a lot for you automatically Property animations still very useful!
  157. Let’s sum it all up… ConstraintLayout allows very flexible, declarative

    positioning You can directly modify parameters to change your layout Guidelines are a great entry point for customizing your layout ConstraintSet allows you to easily manage multiple states of your layout TransitionManager will animate a lot for you automatically Property animations still very useful! Decorators are great for capturing cross-views rendering
  158. Thank you! Nicolas Roard @camaelon John Hoford @johnhoford .droidconSF 2017

  159. Documentation • http://www.constraintlayout.com • https://developer.android.com/reference/android/support/ constraint/package-summary.html • https://developer.android.com/training/constraint-layout/index.html • https://codelabs.developers.google.com/codelabs/constraint-layout

    • https://medium.com/google-developers/building-interfaces-with- constraintlayout-3958fa38a9f7
  160. Documentation • http://www.constraintlayout.com • https://developer.android.com/reference/android/support/ constraint/package-summary.html • https://developer.android.com/training/constraint-layout/index.html • https://codelabs.developers.google.com/codelabs/constraint-layout

    • https://medium.com/google-developers/building-interfaces-with- constraintlayout-3958fa38a9f7