Animated UI state change based on clipping path for canvas. Part 1.

Yuriy Skul
5 min readJul 8, 2020

--

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.

Solution code part 1

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 statein 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 layout AnimatedLayout which extends ConstraintLayout 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 avatar ImageView for JohnSnow/NightKing png;
  • title textView and background shapes for it: drawable/summer_text_background.xml and drawable/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 save mPreviousTouchX position for computing dx.
    return true
    because we are interesting in next motion events;
  • in case MotionEvent.ACTION_MOVE:
    -
    compute dx and then store new mPreviousTouchX position;
    if dx is 0 there is no scrolling — return false;
    -check whether it is possible to scroll in current direction, for example if mOffsetValue 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 when dx is greater than the possible scrolling to the end of distance and because mOffsetValue 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 negative mOffsetValue or greater than layout width.
    Now we can handle preparation logic when user just has started to scroll and mOffsetValue is 0 or getWidth() and it can not have intermediate values. It is just the beginning of scrolling after ACTION_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 to onTouch() method, the case ACTION_MOVE, where we stopped. Detect the beginning of scrolling by checking when mCurrentAnimation is equal toIDLE_ANIMATION_STATE.
    - In such situation update mCurrentAnimation value depending on scrolling direction and apply new state to the UI elements.
    - Update mOffsetValue during scroll and call invalidate() to trigger redrawing with new clipping path.
  • in cases MotionEvent.ACTION_CANCE andMotionEvent.ACTION_UP:
    set back the mCurrentAnimation to IDLE_ANIMATION_STATE,
    erase mPreviousTouchX 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:

--

--