@@ -8,59 +8,91 @@ import android.os.Bundle
88import android.util.AttributeSet
99import android.util.TypedValue
1010import android.view.View
11+ import android.view.Window
1112import android.view.WindowManager
1213import androidx.core.view.ViewCompat
1314import androidx.core.view.WindowInsetsCompat
1415import com.example.androidstudio.motionlayoutintegrations.databinding.ActivityCollapsingToolbarBinding
1516import com.google.android.material.appbar.AppBarLayout
17+ import kotlinx.android.synthetic.main.activity_entrance.*
1618
1719class CollapsingToolbar : AppCompatActivity () {
1820
1921 override fun onCreate (savedInstanceState : Bundle ? ) {
2022 super .onCreate(savedInstanceState)
21- window.apply {
22- if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .KITKAT ) {
23- clearFlags(WindowManager .LayoutParams .FLAG_TRANSLUCENT_STATUS )
24- }
25- if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .LOLLIPOP ) {
26- addFlags(WindowManager .LayoutParams .FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS )
27- statusBarColor = Color .TRANSPARENT
28- }
29- decorView.systemUiVisibility = View .SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
30- }
23+
24+ window.goEdgeToEdge()
3125
3226 val binding = ActivityCollapsingToolbarBinding .inflate(layoutInflater)
3327 setContentView(binding.root)
3428
29+ // When the AppBarLayout progress changes, snap MotionLayout to the current progress
3530 val listener = AppBarLayout .OnOffsetChangedListener { appBar, verticalOffset ->
31+ // convert offset into % scrolled
3632 val seekPosition = - verticalOffset / appBar.totalScrollRange.toFloat()
33+ // inform both both MotionLayout and CutoutImage of the animation progress.
3734 binding.motionLayout.progress = seekPosition
3835 binding.background.translationProgress = (100 * seekPosition).toInt()
3936 }
4037 binding.appbarLayout.addOnOffsetChangedListener(listener)
4138
39+ // get the collapsed height from the motion layout specified in XML
40+ val desiredToolbarHeight = binding.motionLayout.minHeight
41+
42+ // Set two guidelines in the collapsed state for displaying a scrim based on the inset. Also
43+ // resize the MotionLayout when collapsed to add the inset height.
44+ //
45+ // You could also set a similar inset guide for the expanded state if your animation uses
46+ // the top of the screen when expanded.
4247 ViewCompat .setOnApplyWindowInsetsListener(binding.motionLayout) { _, insets: WindowInsetsCompat ->
43- val collapsedTop = TypedValue .applyDimension(TypedValue .COMPLEX_UNIT_DIP , 100f , resources.displayMetrics).toInt()
48+ // resize the motionLayout in collapsed state to add the needed inset height
49+ val insetTopHeight = insets.systemWindowInsetTop
50+ binding.motionLayout.minimumHeight = desiredToolbarHeight + insetTopHeight
51+
52+ // modify the end ConstraintSet to set a guideline at the top and bottom of inset
4453 val endConstraintSet = binding.motionLayout.getConstraintSet(R .id.end)
45- endConstraintSet.setGuidelineEnd(R .id.collapsed_top, collapsedTop)
46- endConstraintSet.setGuidelineEnd(R .id.inset, collapsedTop - insets.systemWindowInsetTop)
54+ // this guideline is the bottom of the inset area
55+ endConstraintSet.setGuidelineEnd(R .id.inset, desiredToolbarHeight)
56+ // this guideline is the top of the inset area (top of screen)
57+ endConstraintSet.setGuidelineEnd(R .id.collapsed_top, desiredToolbarHeight + insetTopHeight)
58+
4759 insets
4860 }
4961 }
50- }
5162
52- private val INFLECTION_PART = 8
53- private val PI_OVER_2 = Math .PI / 2
63+ private fun Window.goEdgeToEdge () {
64+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .KITKAT ) {
65+ clearFlags(WindowManager .LayoutParams .FLAG_TRANSLUCENT_STATUS )
66+ }
67+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .LOLLIPOP ) {
68+ addFlags(WindowManager .LayoutParams .FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS )
69+ statusBarColor = Color .TRANSPARENT
70+ }
71+ decorView.systemUiVisibility = View .SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
72+ }
73+ }
5474
75+ /* *
76+ * A custom view to display a circular cutout on an image that can be controlled by MotionLayout.
77+ *
78+ * Animation of this view is driven by motionLayout controlling [bottomCutSize] and [endCutSize]
79+ * and [translationProgress].
80+ *
81+ * This View will overwrite scaleType from XML to be matrix to allow custom translation.
82+ */
5583class CutoutImage @JvmOverloads constructor(
56- context : Context , attrs : AttributeSet ? = null , defStyleAttr : Int = 0
84+ context : Context , attrs : AttributeSet ? = null , defStyleAttr : Int = 0
5785) : androidx.appcompat.widget.AppCompatImageView(context, attrs, defStyleAttr) {
5886
5987 private val scratchRect = RectF ()
6088
6189 private var _bottomCutSize : Float
6290
63- // this is just to make Kotlin happy
91+ /* *
92+ * Set the size of the bottomCut.
93+ *
94+ * This can directly be called by MotionLayout to animate the size.
95+ */
6496 var bottomCutSize: Float
6597 get() = _bottomCutSize
6698 set(value) {
@@ -69,13 +101,27 @@ class CutoutImage @JvmOverloads constructor(
69101 }
70102
71103 private var _endCutSize : Float
104+
105+ /* *
106+ * Set the size of the endCut.
107+ *
108+ * This can directly be called by MotionLayout to animate the size.
109+ */
72110 var endCutSize: Float
73111 get() = _endCutSize
74112 set(value) {
75113 _endCutSize = value
76114 invalidate()
77115 }
78116
117+ /* *
118+ * Fixed image translation progress to make the image scroll as animation progresses.
119+ *
120+ * This uses a Matrix to scale then translate the image based on the current progress.
121+ *
122+ * This can be directly called by MotionLayout, or be called in response to progress change like
123+ * we do in this sample.
124+ */
79125 var translationProgress: Int = 0
80126 set(value) {
81127 field = value
@@ -90,10 +136,13 @@ class CutoutImage @JvmOverloads constructor(
90136 private val painter = Paint ()
91137
92138 private val grayPainter = Paint ().also {
93- it.color = 0x33000000 .toInt()
139+ it.color = 0x33000000
94140 it.strokeWidth = dpToF(1 )
95141 }
96142
143+ /* *
144+ * Read the endCut, bottomCut, and cutoutColor from XML
145+ */
97146 init {
98147 val typedArray = context.theme.obtainStyledAttributes(
99148 attrs,
@@ -108,6 +157,9 @@ class CutoutImage @JvmOverloads constructor(
108157 typedArray.recycle()
109158 }
110159
160+ /* *
161+ * Force the scaleType to matrix
162+ */
111163 init {
112164 scaleType = ScaleType .MATRIX // ignore any other scale types
113165 }
@@ -118,45 +170,39 @@ class CutoutImage @JvmOverloads constructor(
118170 resources.displayMetrics
119171 )
120172
173+ /* *
174+ * Draw the image with current cutouts applied
175+ */
121176 override fun onDraw (canvas : Canvas ? ) {
122177 // let the parent draw the bitmap
123178 super .onDraw(canvas)
124179
180+ // draw the bottom circle at the correct position and size
125181 canvas?.drawCircle(
126- width.toFloat() / 2 ,
127- height.toFloat(),
128- _bottomCutSize / 2 ,
182+ width.toFloat() / 2 , // midpoint of view
183+ height.toFloat(), // bottom of view
184+ _bottomCutSize / 2 , // radius from diameter
129185 painter
130186 )
131187
188+ // draw the end circle at the correct position and size
132189 val margin = dpToF(16 )
133- if (height.toFloat() <= _endCutSize ) {
134- // this is to fill in the area to the right of the circle to avoid showing a small
135- // triangle of background in (bottom right & top left) during expansion
136- val centerV = 2 * height.toFloat() / 3
137- scratchRect.set(
138- width - margin,
139- centerV - _endCutSize / 2 ,
140- width.toFloat(),
141- centerV + _endCutSize / 2
142- )
143- canvas?.drawRect(scratchRect, painter)
144- }
145-
146190 canvas?.drawCircle(
147- width - margin,
148- 2 * height.toFloat() / 3 ,
149- _endCutSize / 2 ,
191+ width - margin, // end of view, with custom margin applied
192+ 2 * height.toFloat() / 3 , // 2/3 down on view (determined by designer)
193+ _endCutSize / 2 , // radius from diameter
150194 painter
151195 )
152196
153- // add a 1px gray line to the bottom of the circle region so it clearly divides from
154- // surrounding region
197+ // add a 1px gray line to the bottom of the end circle region so it clearly divides from
198+ // surrounding region (this effectively brings the shadow in early on the end circle)
155199 canvas?.drawLine(
200+ // start at the left edge of circle (this could do trig to calculate intersection
201+ // between circle and bottom, but visually this works fine)
156202 width - margin - _endCutSize / 2 ,
157- height.toFloat(),
158- width.toFloat(),
159- height.toFloat(),
203+ height.toFloat(), // bottom of view
204+ width.toFloat(), // to end of view X
205+ height.toFloat(), // bottom of view
160206 grayPainter
161207 )
162208 }
0 commit comments