Animated UI state change based on clipping path for canvas. Part 1.
This part is just a simple “base workpiece” for recyclerView
's item transformation on scroll which I am going to use in my next articles.
In current article I have intention to turn to an issue of this fancy UI components’ state changing like illustrated above with binding clipping path of canvas to the horizontal scrolling and without duplicating View
objects.
Part 1: current story.
Part 2: add child view with custom scrolling behavior.
Part 3: apply fling and auto scrolling.
Part 4: RecyclerView item with custom scrolling behavior.
Part 5: Add parallax effect -coming soon
Part 6: Add snowing animation to the winter state — in progress
Tricky Main Idea
At first we have to create screenshot of current UI state of parentLayout(ViewGroup) for which we want to apply this state transformation:
Bitmap screenshotBitmap = Bitmap
.createBitmap(viewGroup.getMeasuredWidth(),
viewGroup.getMeasuredHeight(),
Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(screenshotBitmap);
viewGroup.draw(canvas);
Now we can set this bitmap to the ImageView
to display fake children views:
fakeImageView.setImageBitmap(bitmap);
fakeImageView.setVisibility(View.VISIBLE);
And finally make changes to parentLayout children and start drawing them with clipping to any shape we want, then during scrolling or running animation — gradually change clipping shape to make it grow up to the size of parent layout. So simple and adorably. I was inspired by android Telegram app where this trick is used when user changes day/night theme.
Starter code:
- simple main layout which includes
transform_item.xml
; - gradle dependency for any circular view because i don’t want to overload this article with creating it out of scratch;
transform_item.xml
has root layoutAnimatedLayout
which extendsConstraintLayout
and currently nothing more. Actually it does not matter what layout to use… the idea will be same;- children view’s of this
layout
are:imageView
for summer/winter background and circular avatarImageView
for JohnSnow/NightKing png; - title
textView
and background shapes for it:drawable/summer_text_background.xml
anddrawable/winter_text_background.xml;
- summer and winter jpeg files I have taken from here.
Base customization of AnimatedLayout class:
I have left simple commentary text so there is nothing more to say:
If run the project there should be summer but if change private int idleState = WINTER_STATE
it will be winter UI state. Just for checking.
Overriding onTouch():
- add new filed variable to store last touch X coordinate;
case MotionEvent.ACTION_DOWN:
do not start any preparation like creating screenshot for bitmap or clipping canvas or changing attributes of UI elements. User might just perform a simple touch without moving his finger at all.
Only savemPreviousTouchX
position for computingdx.
because we are interesting in next motion events;
return true- in
case MotionEvent.ACTION_MOVE:
-computedx
and then store newmPreviousTouchX
position;
if dx is 0 there is no scrolling — returnfalse
;
-check whether it is possible to scroll in current direction, for example ifmOffsetValue
is 0 — scroll from left to right is impossible and when offset is equals to layout width scrolling to the right is impossible because there is nothing more to scroll. Return false in this case;
-handle situation whendx
is greater than the possible scrolling to the end of distance and becausemOffsetValue
can not be lesser than 0 or greater than layout width — clamp it to the 0 or getWidth() instead of going to the incorrect values like negativemOffsetValue
or greater than layout width.
Now we can handle preparation logic when user just has started to scroll andmOffsetValue
is 0 or getWidth() and it can not have intermediate values. It is just the beginning of scrolling afterACTION_DOWN
event.
So define constants for animation state:FROM_LEFT_TO_RIGHT
,FROM_RIGHT_TO_LEFT
,IDLE_ANIMATION_STATE
and filed variable to hold current animation state:private int currentAnimation = IDLE_ANIMATION_STATE;
-Turn back toonTouch()
method, thecase ACTION_MOVE,
where we stopped. Detect the beginning of scrolling by checking whenmCurrentAnimation
is equal toIDLE_ANIMATION_STATE.
- In such situation updatemCurrentAnimation
value depending on scrolling direction and apply new state to the UI elements.
- UpdatemOffsetValue
during scroll and callinvalidate()
to trigger redrawing with new clipping path. - in cases
MotionEvent.ACTION_CANCE
andMotionEvent.ACTION_UP
:
set back themCurrentAnimation
toIDLE_ANIMATION_STATE
,
erasemPreviousTouchX
to store nothing and depending whether scrolling position is more close to the beginning(0) or to the end (getWidth()) apply appropriate summer or winter to the UI elements;
Call invalidate for redrawing.
Clipping path to rectangle
By horizontal scrolling we change the mOffsetValue
. Now it is time to use it for drawing. I do clipping in drawChild(…)
because it gives me possibility to exclude from clipping any child view I want.
Override drawChild(…)
method. Check for mCurrentAnimation
and if it is scrolling animation state — we have to apply clipping to canvas
with path
which depends on mOffsetValue.
If it is FROM_LEFT_TO_RIGHT
scrolling animation we clip from the left edge to mOffsetValue
with rectangle and when FROM_RIGHT_TO_LEFT
- we clip from mOffsetValue
to the right edge of AnimatedLayout.
During MOTION_DOWN
event animation state is still idle and offset is still 0 or getWidth()
— so no clipping is going to be happen in drawChild()
method.
“Screenshot” trick
And finally, when the transformation is going to start, we have to make screenshot of current state and use it as background. We can add to layout one more ImageView
under whole children views and set screenshot background to it but I will handle drawing of fake screenshot inside my custom AnimationLayout
without using additional View object.
Define new field variable : Bitmap mScreenShotBitmap
for holding screenshot and initialize it from onSizeChanged()
because bitmap’s constructor needs width and height.
Then create static nested class ScreenShotCanvas
which extends Canvas
. This is just workaround to mark it. Like a marker interface.
Define method performScreenShot()
which creates the instance of our “marked” ScreenShotCanvas
and calls custom layout to draw its state on it.
Call this method when user just have started transformation
and previous state is still fully displayed before clipping and scrolling is going to be run. This place is in the MotionEvent.ACTION_MOVE
case inside if (mCurrentAnimation == IDLE_ANIMATION_STATE){...}
block.
Calling performScreenShot()
will trigger AnimatedLayout.onDraw(Canvas canvas)
sequentially but instead of regular canvas for rendering the input onDraw
’s argument is the instance of ScreenShotCanvas.
In this case just draw on it but when it is regular canvas in case of scrolling clipping animation - use screenshot (screenShotBitmap) as a fake UI background for our layout.
And finally do not clip ScreenShotCanvas object inside drawChild method — we need full width fake screen bitmap.
Final code of AnimatedLayout.class
is: