Custom layout. Popup + Parallax Scrolling

    Hello colleagues.

    Today I wanted to talk about how you can create a custom layout manager and breathe life into it using animations.
    In DataArt, we often need to implement custom components for customer applications, so I have gained some experience in this matter, which I decided to share.
    As an example, I decided to implement an analogue of the panel that often occurs on social networks from the bottom. Usually this technique is used if necessary to show content, for example, a photo, and add the ability to comment on an additional panel, which the user can stretch from the bottom. In this case, the main content usually also floats up, but a little slower than the main panel. This is called parallax scrolling.
    Especially for this article, I decided to implement a similar component from scratch. I want to note right away that this is not a complete, stable and production-ready code, but just a demo written in a couple of hours to show the basic techniques.



    Extending an existing component


    For ease of implementation, I decided not to extend ViewGroup from scratch, but to inherit from FrameLayout. This will eliminate the need to implement basic routine things, such as measuring children, layout, etc., but at the same time provide enough flexibility to implement our venture.
    So, create the class DraggablePanelLayout.
    The first thing we want to do is modify the layout procedure so that the top layer is shifted down and only a part of it peeps out. To do this, redefine onLayout:
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    	super.onLayout(changed, left, top, right, bottom);
    	if (getChildCount() != 2) {
    	    throw new IllegalStateException("DraggedPanelLayout must have 2 children!");
    	}
    	bottomPanel = getChildAt(0);
    	bottomPanel.layout(left, top, right, bottom - bottomPanelPeekHeight);
    	slidingPanel = getChildAt(1);
    	if (!opened) {
    	    int panelMeasuredHeight = slidingPanel.getMeasuredHeight();
    	    slidingPanel.layout(left, bottom - bottomPanelPeekHeight, right, bottom - bottomPanelPeekHeight
    		    + panelMeasuredHeight);
    	}
    }
    


    Everything is simple here: we limit our layout so that it can store only two descendants. Then force the upper descendant down and compress from the bottom to the bottom. Let's make a simple layout and see what we get:





    As you can see, the bottom panel has successfully shifted down. What we need.

    Add a drag and drop


    To implement dragging a socket with a finger, you need to override the onTouchEvent method. Here, when we press with our finger (ACTION_DOWN) we will remember where the user clicked, then with a finger (ACTION_MOVE), we will move our panels, and finally, with ACTION_UP, we will complete the action. Completing the action is perhaps the most interesting task, but more on that later.

    @Override
    public boolean onTouchEvent(MotionEvent event) {
    	if (event.getAction() == MotionEvent.ACTION_DOWN) {
    	    startDragging(event);
    	} else if (event.getAction() == MotionEvent.ACTION_MOVE) {
    	    if (touching) {
    		float translation = event.getY() - touchY;
    		translation = boundTranslation(translation);
    		slidingPanel.setTranslationY(translation);
    		bottomPanel
    			.setTranslationY((float) (opened ? -(getMeasuredHeight() - bottomPanelPeekHeight - translation)
    				* parallaxFactor : translation * parallaxFactor));
    	    }
    	} else if (event.getAction() == MotionEvent.ACTION_UP) {
    	    isBeingDragged = false;
    	    touching = false;
    	}
    	return true;
     }
    


    Everything is simple here. The boundTranslation method restricts the movement of the panel with a finger within the screen; setTranslation sets the offset.

    Here I want to make a small digression and talk about layout and translation. Layout is the layout process of your layout, i.e., for each View, its size and position on the screen are recursively determined. As it’s not hard to guess, this is a costly operation. That is why it is highly discouraged to perform this procedure during animation, unless you just want to get the effect of a slowdown animation. The translation property, in turn, allows you to set the cheap offset of an element relative to a given position without performing the layout of the entire hierarchy. This is very useful for animations. In addition to translation, View has properties such as Rotation, Scale. It is also possible to do more advanced transformations by subclassing the desired component and performing the necessary canvas transformations.


    Once again, but briefly and in caps. The main rule for animations is DO NOT PERFORM LAYOUT !!!

    Gesture completion


    So, we learned how to move our panel with our fingers, now we need to add the completion of the gesture. That is, if the user removes his finger, we will adhere to the following logic:
    1. If the panel speed is high enough - bring the panel to the end and put the component in the opposite state.
    2. If the speed is not high, check if the user has drawn the panel through half the distance. If so, continue driving at a fixed speed; otherwise, return the panel to its original state.


    public void finishAnimateToFinalPosition(float velocityY) {
    	final boolean flinging = Math.abs(velocityY) > 0.5;
    	boolean opening;
    	float distY;
    	long duration;
    	if (flinging) {
    	    opening = velocityY < 0;
    	    distY = calculateDistance(opening);
    	    duration = Math.abs(Math.round(distY / velocityY));
    	    animatePanel(opening, distY, duration);
    	} else {
    	    boolean halfway = Math.abs(slidingPanel.getTranslationY()) >= (getMeasuredHeight() - bottomPanelPeekHeight) / 2;
    	    opening = opened ? !halfway : halfway;
    	    distY = calculateDistance(opening);
    	    duration = Math.round(300 * (double) Math.abs((double) slidingPanel.getTranslationY())
    		    / (double) (getMeasuredHeight() - bottomPanelPeekHeight));
    	}
    	animatePanel(opening, distY, duration);
    }
    


    The method above implements this logic. To calculate the speed, use the built-in class VelocityTracker.
    Finally, create an ObjectAnimator and complete the animation:

    public void animatePanel(final boolean opening, float distY, long duration) {
    	ObjectAnimator slidingPanelAnimator = ObjectAnimator.ofFloat(slidingPanel, View.TRANSLATION_Y,
    		slidingPanel.getTranslationY(), slidingPanel.getTranslationY() + distY);
    	ObjectAnimator bottomPanelAnimator = ObjectAnimator.ofFloat(bottomPanel, View.TRANSLATION_Y,
    		bottomPanel.getTranslationY(), bottomPanel.getTranslationY() + (float) (distY * parallaxFactor));
    	AnimatorSet set = new AnimatorSet();
    	set.playTogether(slidingPanelAnimator, bottomPanelAnimator);
    	set.setDuration(duration);
    	set.setInterpolator(sDecelerator);
    	set.addListener(new MyAnimListener(opening));
    	set.start();
    }
    


    At the end of the animation, we translate the component to a new state, zero the offset and execute the layout.

    @Override
    public void onAnimationEnd(Animator animation) {
    	    setOpenedState(opening);
    	    bottomPanel.setTranslationY(0);
    	    slidingPanel.setTranslationY(0);
    	    requestLayout();
    }
    


    Interception of touch'a from other elements


    Now, if we put, for example, a button on our panel, we will see that if we try to pull the panel by clicking on the button with our finger, we will not be able to do this. The button will be pressed, but our panel will remain motionless. This is because the button
    “steals” the touch event near the panel and processes it itself.
    The standard approach is to intercept the event, make sure that we really pull the panel, and not just click on the button, and take control of the button, completely capturing it with our component. Specifically for this, View has an onInterceptTouchEvent method. The logic of the operation of this method and interaction with onTouchEvent is very nontrivial, but well-written in the documentation.

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
    	if (event.getAction() == MotionEvent.ACTION_DOWN) {
    	    touchY = event.getY();
    	} else if (event.getAction() == MotionEvent.ACTION_MOVE) {
    	    if (Math.abs(touchY - event.getY()) > touchSlop) {
    		isBeingDragged = true;
    		startDragging(event);
    	    }
    	} else if (event.getAction() == MotionEvent.ACTION_UP) {
    	    isBeingDragged = false;
    	}	
    	return isBeingDragged;
    }
    


    In our implementation, we check to see if the user has shifted his finger enough (touchSlop) before returning true (which means that we have taken control).
    Done, now the user can click on the button and start moving the panel anywhere. The button simply does not register the click, but receives the ACTION_CANCEL event.

    Completion


    This article describes a basic approach to implementing animated markup. Of course, a full-fledged implementation will require taking into account some additional factors, but starting with what is described here, it is easy to adapt it to your needs.

    All component sources are available on github . In addition to what is described in the article, the implementation adds:
    1. drawing a shadow between panels;
    2. custom attributes;
    3. Using hardware layers to speed up animation.

    Thanks for attention.

    Also popular now: