A residing tab like WhatsApp's camera tab.
Whatsapp's layout vs Residing Tab layout
Having searched online for a way to design a tab like the WhatsApp's camera tab, with little to no success, I decided to implement my own solution to the problem. It involves placing/residing the View for the tab beneath the ViewPager (like the SurfaceView for the camera's preview), and returning a Fragment with a null View for that tabs position (revealing position) on the ViewPager. The Views returned by the other Fragments would have a painted background to completely cover the residing View.
Another View which serves as the foreground for the residing View (like the camera's interface) is placed above it, but beneath the ViewPager. It slides in and out of position with the null View Fragment. When the null view Fragment slides into position, the transparency of the ViewPager then reveals the residing view and its foreground beneath it.
Download demo application.
The Following implementations were carried out in the layout and logic.
The layout was divided into two for simplicity. The activity_main.xml
, containing the residing view
and content_main.xml
, containing the ViewPager
and Residing foreground
.
The layout attribute android:fitsSystemWindows
is used to determine which ViewGroups should draw under system decorations (="false"
) and which views should not (="true"
).
1. activity_main.xml: The base View of this layout should be set to draw under system decorations.
The first child in the ViewGroup is the residing View (like the SurfaceView for the camera's preview).
The second child is the container (FrameLayout) for the main content. It should not be allowed to draw under system's decorations.
Within it the main_content
layout is added using an include
tag.
2. content_main.xml: This is the main content of the layout. Its base View is a CoordinatorLayout to handle various materials design behaviours like ScrollingView behaviour and AppBarLayout scroll flags.
The first child in the residing foreground. It is drawn above the residing View, just like the WhatsApp camera's buttons.
The second child is a modified ViewPager (RevealViewPager
) having the AppBarLayout.ScrollingViewBehaviour
, and a negative top margin, enough to place its top at the top of the AppBar.
The fragments are made to appear below the appBar by placing a positive top padding equal to the top margin of the ViewPager on the individual fragment.
The fourth child is a FloatingActionButton.
The structure of the logic can be divided into the implementation of two separate classes.
1. A FragmentPagerAdapter: This overrides methods for returning a Fragment instance for every position on the ViewPager. For the position where the camera tab would be located, a Fragment instance returning a null View in its onCreateView method will be returned. The Views returned by the other fragments should have a colored background to completely cover the residing view.
2. A modified ViewPager class (RevealViewPager
):
This is a subclass of the ViewPager
class. It has an inner class that implements ViewPager.OnPageChangeListener
. It serves the purpose of animating the AppBarLayout, the ResideForeground and FloatingActionButton. It is also responsible for setting the window states. It exposes an interface for different visibility changes of the Residing View.
The class is applied internally to the ViewPager using its addOnPageChangeListener() method.
The limitations of the current version include:
- Touch events: The Residing View and its foreground are placed below the ViewPager. This is to ensure proper scrolling behaviour for the ViewPager.
When the ViewPager is in Reveal position, it dispatches touch events to the residing foreground and then the residing view.
As a result of the scrolling behaviour of the ViewPager, only simple click and longClick events might respond properly.
For instance a HorizontalScrollView would not be able to scroll properly in the horizontal direction but a VerticalScrollView can scroll in the vertical direction.
Views with complex touch implementations should be avoided in this version.
To use this in developing a similar layout, you can fork this project and use the template provided.
Step 1:
Include the RevealViewPager
class in your project either by;
1. Forking the latest version and adding it directly to your project (Might be necessary if you want to make some changes to the class).
2. Getting it from jitpack
by including the maven url for jitpack in your project level build.gradle
file.
allprojects {
repositories {
maven { url 'https://jitpack.io'}
// ...
}
}
and including the following dependency to your app level build.gradle
file.
dependencies {
implementation 'com.github.Goody-h:ResidingTab:v1.0.0'
// ...
}
Step 2:
Design a layout using the provided design template. Make sure to use the RevealViewPager
implementation of the ViewPager
.
Step 3:
Create a new Activity and set a reference variable for the RevealViewPager
and add a constant which would hold the position index of the residing view reveal tab.
// The reveal viewPager variable
private var mRevealViewPager: RevealViewPager? = null
// Constant holding the reveal position
private val REVEAL_POSITION: Int = 0
Step 4:
Get the reference to the RevealViewPager
in the activity's onCreate( )
method.
Pass a reference of the residing View and its reveal position to the ViewPager by calling setResidingView( )
.
Set the views to be transformed; the appBar, residing foreground, FloatingActionButton, by calling bindTransformedViews( )
.
Set a ResideTabVisibilityChangeListener
on the RevealViewPager by calling setOnResideTabVisibilityChangeListener( )
to make changes to the residing view and its foreground based on its visibility.
override fun onCreate(savedInstanceState: Bundle?) {
mRevealViewPager = findViewById(R.id.container)
mRevealViewpager?.apply{
// ...
setOnResideTabVisibilityChangeListener( object : RevealViewPager.OnResideTabVisibilityChangeListener {
// ...
})
setResidingView(reside_content, REVEAL_POSITION)
bindTransformedViews(appbar, reside_view_foreground, fab)
// ...
}
}
Step 5:
Call the initTransformer( )
method of the RevealViewPager after setting the initial tab position,
and pass the savedInstanceState
from the onCreate( )
method to it, including an optional boolean parameter to perform initial transformations.
override fun onCreate(savedInstanceState: Bundle?) {
mRevealViewpager?.apply{
// ...
bindTransformedViews(appbar, reside_view_foreground, fab)
initTransformer(savedInstanceState, false)
}
}
Step 6:
Set activity callbacks on the RevealViewPager. In the activity's onSavedInstanceState( )
call the saveState( )
method off the RevealVIewPager, to persist transformation state.
In the activity's onResume( )
or onWindowFocusChanged( )
call the updateUIVisibility( )
method off the RevealVIewPager, to make sure the systemUIVisibility is always correct.
override fun onSaveInstanceState(outState: Bundle?) {
super.onSaveInstanceState(outState)
mRevealViewPager?.saveState(outState)
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
mRevealViewPager?.updateUIVisibility()
}
}
Step 7:
Create at least two fragments. One would return a null View in its onCreateView( )
method.
This is the Residing tab reveal fragment.
class ResideRevealFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return null
}
}
Other fragments should return a View with a colored background and a positive top padding equal to the negative top margin of the ViewPager, in order to place its children below the appBar.
<!-- replace ViewGroup with required class -->
<ViewGroup
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragment_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
android:paddingTop="120dp">
<!-- children -->
</ViewGroup>
Step 8:
Create a ViewPagerAdapter class and override its getItem( )
method.
Check if the requested position equals the position index of the residing tab reveal and return the null View Fragment else return any other Fragment.
class SectionsPagerAdapter(fm: FragmentManager) : FragmentPagerAdapter(fm) {
override fun getItem(position: Int): Fragment {
// Return a ResideRevealFragment if position = 0 else
// Return a PlaceHolderFragment (defined as a static inner class below).
return if (position == REVEAL_POSITION) ResideRevealFragment()
else PlaceholderFragment.newInstance(position + 1)
}
// ...
}
Set the adapter for the ViewPager in the activity's onCreate( )
override fun onCreate() {
mRevealViewpager?.apply{
adapter = mSectionPagerAdapter
// ...
}
}
Contributions to this projects are welcomed. Contributions should be focused mainly on the current limitations of the project.
Create a pull request indicating the new implementations to be made.
This project is licensed under the Apache License Version 2.0, January 2004. Read the license statement for more information.
- Support: [email protected]
- Developer: [email protected]
- Website: https://orsteg.com