Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

Copenhagen Android meetup

Copenhagen Android meetup

ConstraintLayout & Animation

Nicolas Roard

November 15, 2017
Tweet

More Decks by Nicolas Roard

Other Decks in Programming

Transcript

  1. 1.0 Relative positioning Center positioning & bias Guidelines - helper

    objects Chains Dimension constraints: min/max, Ratio ConstraintSet
  2. Different Chain Styles A B C Spread A B C

    Spread Inside A B C Weighted A B C Packed
  3. Different Chain Styles A B C Spread A B C

    Spread Inside A B C Weighted A B C Packed + bias
  4. Barriers Groups : apply visibility to a set of widgets

    Placeholder Percent Dimensions Circular Constraints beta 3 1.1.0
  5. Barriers Groups : apply visibility to a set of widgets

    Placeholder Percent Dimensions Circular Constraints beta 3 1.1.0
  6. Barriers Groups : apply visibility to a set of widgets

    Placeholder Percent Dimensions Circular Constraints beta 3 1.1.0
  7. Barriers Groups : apply visibility to a set of widgets

    Placeholder Percent Dimensions Circular Constraints beta 3 1.1.0
  8. Barriers Groups : apply visibility to a set of widgets

    Placeholder Percent Dimensions Circular Constraints beta 3 1.1.0 ?
  9. class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) } fun select(v: View) { TransitionManager.beginDelayedTransition(main_layout) placeholder.setContentId(v.id) main_title.text= v.tag as CharSequence?;""; } } Placeholder
  10. class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) } fun select(v: View) { TransitionManager.beginDelayedTransition(main_layout) placeholder.setContentId(v.id) main_title.text= v.tag as CharSequence?;""; } } Placeholder
  11. Barriers Groups : apply visibility to a set of widgets

    Placeholder Percent Dimensions Circular Constraints beta 3 1.1.0
  12. Percent Dimension • width or height set to 0dp (“match_constraint”)

    • relative to the container • control behavior with: • app:layout_constraintWidth_percent=“0.20” • app:layout_constraintHeight_percent=“0.20”
  13. Barriers Groups : apply visibility to a set of widgets

    Placeholder Percent Dimensions Circular Constraints beta 3 1.1.0
  14. Case 2 1. Center connection 2. Bias set to 0

    3. app:layout_constrainedWidth=“true”
  15. Case 3 1. Horizontal chain between (1) and (2) 2.

    Chain is a packed chain 3. Horizontal bias is set to 0 4. (1) has app:layout_constrainedWidth=“true”
  16. Case 3 1. Horizontal chain between (1) and (2) 2.

    Chain is a packed chain 3. Horizontal bias is set to 0 4. (1) has app:layout_constrainedWidth=“true” 1 2
  17. Case 4 • Using nested ConstraintLayout can be a powerful

    layout tool • Think: layout components
  18. Direct Manipulation //Find the view you are over and set

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

    minimum height View child = layout.getChildAt(current); B child.setMinimumHeight(400); C
  20. 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
  21. 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
  22. 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
  23. 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/
  24. Direct Manipulation : Guideline Guideline are powerful elements Specify exact

    positioning or percent value Parameterize a layout via guidelines
  25. 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();
  26. 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();
  27. 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();
  28. 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
  29. base.setOnTouchListener(new View.OnTouchListener() { public boolean onTouch(View view, MotionEvent motionEvent) {

    switch (motionEvent.getActionMasked( )) { case MotionEvent.ACTION_MOVE: break; } A return true; } B });
  30. 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 });
  31. 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} })
  32. 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} })
  33. 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} })
  34. ConstraintSet ConstraintSet mConstraintSet1 = new ConstraintSet(); // create a Constraint

    Set ConstraintSet mConstraintSet2 = new ConstraintSet(); // create a Constraint Set Initialization
  35. 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
  36. 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
  37. 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
  38. 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); }
  39. 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); }
  40. 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); }
  41. 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); }
  42. 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)); } }
  43. 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)); } }
  44. 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>
  45. <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
  46. <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
  47. 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 } } }
  48. 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
  49. 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
  50. 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)
  51. 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 }
  52. 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 } } }
  53. 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 } } }
  54. 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 } } }
  55. 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 } } }
  56. Animating the alpha channel mBackground = findViewById(R.id.background) … showImageAnimator =

    ObjectAnimator.ofFloat(mBackground, "alpha", 0f, 1f) showImageAnimator?.duration = 600 … showImageAnimator?.start()
  57. 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
  58. 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() }
  59. 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() }
  60. 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() }
  61. 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
  62. Collapsible Layout Summary • Use ConstraintLayout / ConstraintSet for flexibility

    • Piggy-back on CoordinatorLayout / AppBarLayout • Kotlin! • ObjectAnimator for property animation
  63. Decorator ConstraintHelper + View Gather draw operations in a single

    place Draw something depending on multiple views
  64. 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 } }
  65. Let’s sum it all up… ConstraintLayout allows very flexible, declarative

    positioning You can directly modify parameters to change your layout
  66. 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
  67. 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
  68. 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
  69. 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!
  70. 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