Share
Embed
Advanced Animations & ConstraintLayout
Advanced animations with ConstraintLayout
Nicolas Roard
November 06, 2017
Transcript
160 slides
-
Where are we now Android Studio 3.0 just shipped! ConstraintLayout
1.0.2 stable ConstraintLayout 1.1 beta 3 -
Constraints Vocabulary • Relative positioning • Center + bias •
Dimension (e.g. ratio) • Group behavior (chains) • Helper objects (guidelines) -
Direct Manipulation //Find the view you are over and set
minimum height View child = layout.getChildAt(current); B -
Direct Manipulation //Find the view you are over and set
minimum height View child = layout.getChildAt(current); B child.setMinimumHeight(400); C -
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 -
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 -
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 -
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/ -
Direct Manipulation : Guideline Guideline are powerful elements Specify exact
positioning or percent value -
Direct Manipulation : Guideline Guideline are powerful elements Specify exact
positioning or percent value Parameterize a layout via guidelines -
final Guideline guideline = findViewById(R.id.guideline); final int end = ((LayoutParams)
guideline.getLayoutParams()).guideEnd; -
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(); -
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(); -
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(); -
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 -
base.setOnTouchListener(new View.OnTouchListener() { public boolean onTouch(View view, MotionEvent motionEvent) {
switch (motionEvent.getActionMasked( )) { case MotionEvent.ACTION_MOVE: break; } A return true; } B }); -
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 }); -
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView?, dx: Int,
dy: Int) { super.onScrolled(recyclerView, dx, dy) a} }) -
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} }) -
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} }) -
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} }) -
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) -
ConstraintSet ConstraintSet mConstraintSet1 = new ConstraintSet(); // create a Constraint
Set ConstraintSet mConstraintSet2 = new ConstraintSet(); // create a Constraint Set Initialization -
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 -
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 -
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 -
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); } -
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); } -
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); } -
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); } -
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)); } } -
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)); } } -
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> -
<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 -
<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 -
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 } } } -
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 -
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 -
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) -
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 } -
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 } } } -
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 } } } -
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 } } } -
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 } } } -
Animating the alpha channel mBackground = findViewById(R.id.background) … showImageAnimator =
ObjectAnimator.ofFloat(mBackground, "alpha", 0f, 1f) showImageAnimator?.duration = 600 … showImageAnimator?.start() -
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 -
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() } -
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() } -
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() } -
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 -
Collapsible Layout Summary • Use ConstraintLayout / ConstraintSet for flexibility
• Piggy-back on CoordinatorLayout / AppBarLayout • Kotlin! • ObjectAnimator for property animation -
Decorator ConstraintHelper + View Gather draw operations in a single
place Draw something depending on multiple views -
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 } } -
Let’s sum it all up… ConstraintLayout allows very flexible, declarative
positioning You can directly modify parameters to change your layout -
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 -
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 -
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 -
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! -
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 -
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 -
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