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

Advanced Animations & ConstraintLayout

Advanced Animations & ConstraintLayout

Advanced animations with ConstraintLayout

Nicolas Roard

November 06, 2017
Tweet

More Decks by Nicolas Roard

Other Decks in Programming

Transcript

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

    Dimension (e.g. ratio) • Group behavior (chains) • Helper objects (guidelines)
  2. Direct Manipulation //Find the view you are over and set

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

    minimum height View child = layout.getChildAt(current); B child.setMinimumHeight(400); C
  4. 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
  5. 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
  6. 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
  7. 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/
  8. Direct Manipulation : Guideline Guideline are powerful elements Specify exact

    positioning or percent value Parameterize a layout via guidelines
  9. 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();
  10. 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();
  11. 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();
  12. 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
  13. base.setOnTouchListener(new View.OnTouchListener() { public boolean onTouch(View view, MotionEvent motionEvent) {

    switch (motionEvent.getActionMasked( )) { case MotionEvent.ACTION_MOVE: break; } A return true; } B });
  14. 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 });
  15. 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} })
  16. 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} })
  17. 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} })
  18. ConstraintSet ConstraintSet mConstraintSet1 = new ConstraintSet(); // create a Constraint

    Set ConstraintSet mConstraintSet2 = new ConstraintSet(); // create a Constraint Set Initialization
  19. 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
  20. 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
  21. 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
  22. 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); }
  23. 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); }
  24. 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); }
  25. 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); }
  26. 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)); } }
  27. 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)); } }
  28. 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>
  29. <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
  30. <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
  31. 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 } } }
  32. 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
  33. 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
  34. 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)
  35. 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 }
  36. 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 } } }
  37. 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 } } }
  38. 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 } } }
  39. 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 } } }
  40. Animating the alpha channel mBackground = findViewById(R.id.background) … showImageAnimator =

    ObjectAnimator.ofFloat(mBackground, "alpha", 0f, 1f) showImageAnimator?.duration = 600 … showImageAnimator?.start()
  41. 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
  42. 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() }
  43. 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() }
  44. 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() }
  45. 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
  46. Collapsible Layout Summary • Use ConstraintLayout / ConstraintSet for flexibility

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

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

    positioning You can directly modify parameters to change your layout
  50. 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
  51. 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
  52. 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
  53. 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!
  54. 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