Sticky navigation layout Android Image Source unsplash.com.

Here is the GitHub link for this project sticky-nav-android.

Introduction

In this article, we will be creating a sticky navigation view in android. We will view it by scrolling up to hide the header and display the tab layout below it fully. I will be using the Java programming language for the development of this android project. There are two ways of implementing it.

Firstly, we will use ScrollView embedded software introduction +ViewPager in ScrollView. This method is purely native and does not involve custom controls, but this nesting involves measurement and event conflict handling. It is quite easy to imagine, but it is actually quite laborious to do it. Second, we will focus on transforming ScrollView as an external layer into a custom, inherited from LinearLayout, called StickyNavLayout. Tabbed activity will be used since provides a way to display tabs horizontally. When used together with a ViewPager, a TabLayout can provide a familiar interface for navigating between pages in a swipe view.

This project will offer insights into the workings of a sticky navigation view in Android and will help the reader to get a better understanding of the inner-machinery that goes behind implementing such features.

Photo Preview

Glossary

StickyScrollView uses an android scroll view. All you have to do is changing the XML layout from the default constraint layout to scroll view layout. This will specify the architecture indicators that need to be attached.

Steps:

Step 1: Creating the activity_main.xml file

We have said above that the reason for the second method is to look at the ease of use. This is done using the custom id resource file, shown below:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="id_stickynavlayout_topview" type="id"/>
<item name="id_stickynavlayout_viewpager" type="id"/>
<item name="id_stickynavlayout_indicator" type="id"/>
<item name="id_stickynavlayout_innerscrollview" type="id"/>
</resources>

Many id tools have been described, especially for ease of use. They are in use when make-up is announced. You should be able to use it by looking at the word. If you can’t use it, that’s fine. Then I will attach a layout file. This is not really a method to use, but you will see it below, so post it in advance.

Now, go to activity_main.xml file. Change your default to Relative Layout and add other attributes such as:

<com.quick.lib.StickyNavLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<RelativeLayout
android:id="@id/id_stickynavlayout_topview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#FFFF00" >
<TextView
android:layout_width="match_parent"
android:layout_height="240dp"
android:gravity="center"
android:text="Sticky Header"
android:textSize="30sp"
android:textStyle="bold" />
</RelativeLayout>
<com.quick.lib.SimpleViewPagerIndicator
android:id="@id/id_stickynavlayout_indicator"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="#ffffffff" >
</com.quick.lib.SimpleViewPagerIndicator>
<android.support.v4.view.ViewPager
android:id="@id/id_stickynavlayout_viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#44ff0000" >
</android.support.v4.view.ViewPager>
</com.quick.lib.StickyNavLayout>
view raw activity_main.xml hosted with ❤ by GitHub

Step 3: Creating the StickyNavLayout file

The external layer is our custom control StickyNavLayout, then a top-level content, Vp index, and ViewPager. According to the translation, just write it right. Note that some IDs use our pre-set ID resources. Because our StickyNavLayout needs access to id control to perform certain calculations.

3.1. Structure

public class StickyNavLayout extends LinearLayout
{
private View mTop;
private View mNav;
private ViewPager mViewPager;
private int mTopViewHeight;
private ScrollView mInnerScrollView;
private boolean isTopHidden = false;
private OverScroller mScroller;
private VelocityTracker mVelocityTracker;
private int mTouchSlop;
private int mMaximumVelocity, mMinimumVelocity;
private float mLastY;
private boolean mDragging;
public StickyNavLayout(Context context, AttributeSet attrs)
{
super(context, attrs);
setOrientation(LinearLayout.VERTICAL);
mScroller = new OverScroller(context);
mVelocityTracker = VelocityTracker.obtain();
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
mMaximumVelocity = ViewConfiguration.get(context)
.getScaledMaximumFlingVelocity();
mMinimumVelocity = ViewConfiguration.get(context)
.getScaledMinimumFlingVelocity();
}
@Override
protected void onFinishInflate()
{
super.onFinishInflate();
mTop = findViewById(R.id.id_stickynavlayout_topview);
mNav = findViewById(R.id.id_stickynavlayout_indicator);
View view = findViewById(R.id.id_stickynavlayout_viewpager);
if (!(view instanceof ViewPager))
{
throw new RuntimeException(
"id_stickynavlayout_viewpager show used by ViewPager !");
}
mViewPager = (ViewPager) view;
}
view raw StickyNavLayout.java hosted with ❤ by GitHub

First look at the flexibility of the members and the implementation of our flexibility. MTop, mNav, and mViewPager represent the three major components of our structure. Activation completed in onFinishInflate. You see, it’s okay to read directly about our id source. Next, there are othermTouchSlop, mScroller, mVelocity, mMaximumVelocity, Tracker, mLastY, mMinimumVelocity, mDragging. Obviously, everyone would think this is related to a cell phone. OverScroller is an auxiliary class that helps us deal with the mathematical component when we customize the movement. There are many variations related to mVelocityTracker. Yes, to calculate the need for automatic navigation. IMTouchSlop helps me distinguish whether a user clicks or drags. There are many features to install on Android for integration. In ViewConfiguration, everyone can learn about them if they find them interesting. The reason for using these issues is not only to save ourselves to explain but to maintain compliance with systemic behaviour.

3.2. OnMeasure

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
ViewGroup.LayoutParams params = mViewPager.getLayoutParams();
params.height = getMeasuredHeight() - mNav.getMeasuredHeight();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh)
{
super.onSizeChanged(w, h, oldw, oldh);
mTopViewHeight = mTop.getMeasuredHeight();
}
view raw StickyNavLayout.java hosted with ❤ by GitHub

After reading the layout, because we use LinearLayout, setOrientation directly (LinearLayout.VERTICAL); no need to go to the building, all controls are set up. Later we need to do some processing on onMeasure.

The key is to set the ViewPager height and give it a fixed value. When ViewPager scales itself, if you do not give it a fixed value, the rating result may differ significantly from what you expect. For example, you set its WRAP_CONTENT, and you want him to calculate the child’s height to set his own, and then think more. You can view the ViewPager rating source code:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// For simple implementation, our internal size is always 0.
// We depend on the container to specify the layout size of
// our view. We can't really know what it is since we will be
// adding and removing different arbitrary views and do not
// want the layout to change as this happens.
setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
getDefaultSize(0, heightMeasureSpec));
}
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
view raw StickyNavLayout.java hosted with ❤ by GitHub

It can be seen that in both AT_MOST and VERY methods. Henceforth, the incoming value of the parent category is directly calculated, i.e., it will not measure the height of its child. Where the mode is: UNCERTIFIED, then the length is straight to 0. If you have made this example, you should be able to meet this situation. When ViewPager is installed in ScrollView, the rating mode is not checked, Vp is not displayed directly, and the reason is here.

We go, come back, we move on.

After setting the Vp value, in theory, our display is already normal, and the controls are displayed as expected, but, what? We are now in Custom LinearLayout, so should we write the phone alone?

Navigation code is very simple, as everyone knows, just get a straight dx and scrollBy. sticky navigation layout

3.3. OnTouchEvent

@Override
public boolean onTouchEvent(MotionEvent event)
{
mVelocityTracker.addMovement(event);
int action = event.getAction();
float y = event.getY();
switch (action)
{
case MotionEvent.ACTION_DOWN:
if (!mScroller.isFinished())
mScroller.abortAnimation();
mVelocityTracker.clear();
mVelocityTracker.addMovement(event);
mLastY = y;
return true;
case MotionEvent.ACTION_MOVE:
float dy = y - mLastY;
if (!mDragging && Math.abs(dy) > mTouchSlop)
{
mDragging = true;
}
if (mDragging)
{
scrollBy(0, (int) -dy);
mLastY = y;
}
break;
case MotionEvent.ACTION_CANCEL:
mDragging = false;
if (!mScroller.isFinished())
{
mScroller.abortAnimation();
}
break;
case MotionEvent.ACTION_UP:
mDragging = false;
mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int velocityY = (int) mVelocityTracker.getYVelocity();
if (Math.abs(velocityY) > mMinimumVelocity)
{
fling(-velocityY);
}
mVelocityTracker.clear();
break;
}
return super.onTouchEvent(event);
}
view raw StickyNavLayout.java hosted with ❤ by GitHub

It’s really easy because we only need to judge the index y. Therefore, we record the value of y when we’re down, and then we get the dy when we go, just go straight to scrollBy, of course, we’re in the whole process. addMovement (event); So, when we go up, we get velocityY on the v side and we call to move.

Fortunately, the key fling code, OverScroller, available to us, is very good.

Everyone should be clear that when we use auxiliary classes like Scroller, they help us complete the calculation of information and statistics. As for the default, we still need to do it ourselves.

How do you do it, where do you do it? This is nothing but a rewrite of the computeScroll method, in which it is judged whether the scroll is complete, if not, scrollTo is clicked, and finally remember to create the appropriate code:

public void fling(int velocityY)
{
mScroller.fling(0, getScrollY(), 0, velocityY, 0, 0, 0, mTopViewHeight);
invalidate();
}
@Override
public void scrollTo(int x, int y)
{
if (y < 0)
{
y = 0;
}
if (y > mTopViewHeight)
{
y = mTopViewHeight;
}
if (y != getScrollY())
{
super.scrollTo(x, y);
}
isTopHidden = getScrollY() == mTopViewHeight;
}
@Override
public void computeScroll()
{
if (mScroller.computeScrollOffset())
{
scrollTo(0, mScroller.getCurrY());
invalidate();
}
}
view raw StickyNavLayout.java hosted with ❤ by GitHub

ok, at this point, our onTouchEvent is done but, don’t be proud, why do you say that? Because of what you’ve done, View information manages to drag up and down. Don’t forget, our current StickyNavLayout has ScrollView inside. After that according to the event transfer method, this internal ScrollView will deal with the drag-and-drop mode, that is, our events will be captured by it.

3.4. onInterceptTouchEvent

Okay, next time we have to deal with prevention. For restrictions, we must clearly know when to enter and when not to. Our current example:

1. If our top view is not completely hidden, then directly reduce the pull-up and down;

2. There is another area that needs to be caught, that is when the top view is completely hidden, our inner sc should be able to slide up and down, but if the sticky navigation slides up and down, it should be caught again. You need to slide the top view.

Finally the analysis is complete, look at the code, this is called sour:

@Override
public boolean onInterceptTouchEvent(MotionEvent ev)
{
int action = ev.getAction();
float y = ev.getY();
switch (action)
{
case MotionEvent.ACTION_DOWN:
mLastY = y;
break;
case MotionEvent.ACTION_MOVE:
float dy = y - mLastY;
getCurrentScrollView();
if (Math.abs(dy) > mTouchSlop)
{
mDragging = true;
if (!isTopHidden
|| (mInnerScrollView.getScrollY() == 0 && isTopHidden && dy > 0))
{
return true;
}
}
break;
}
return super.onInterceptTouchEvent(ev);
}
view raw StickyNavLayout.java hosted with ❤ by GitHub

okay, go decide the two cases above.

Note: If you want to know how ListView can replace the ScrollView mentioned above, please note that ListView support has been added to the source code, so you can download it and view it yourself.

package com.quick.lib;
import android.content.Context;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.app.FragmentStatePagerAdapter;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.OverScroller;
import android.widget.ScrollView;
public class StickyNavLayout extends LinearLayout {
private View mTop;
private View mNav;
private ViewPager mViewPager;
private int mTopViewHeight;
OverScroller mScroller;
private boolean isTopHidden = false;
private ScrollView mInnerScrollView;
private VelocityTracker mVelocityTracker;
private int mMaximumVelocity, mMinimumVelocity;
float lastX=0;
float lastY = 0;
float dy = 0;
float dx=0;
private int mTouchSlop;
boolean onIntercept;
public StickyNavLayout(Context context, AttributeSet attrs) {
super(context, attrs);
setOrientation(LinearLayout.VERTICAL);
mScroller = new OverScroller(context);
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();//获得能够进行手势滑动的距离
mMaximumVelocity = ViewConfiguration.get(context)
.getScaledMaximumFlingVelocity();
mMinimumVelocity = ViewConfiguration.get(context)
.getScaledMinimumFlingVelocity();
}
private void getCurrentScrollView() {
int currentItem = mViewPager.getCurrentItem();
PagerAdapter a = mViewPager.getAdapter();
if (a instanceof FragmentPagerAdapter) {
FragmentPagerAdapter fadapter = (FragmentPagerAdapter) a;
Fragment item = (Fragment) fadapter.instantiateItem(mViewPager,
currentItem);
mInnerScrollView = (ScrollView) (item.getView()
.findViewById(R.id.id_stickynavlayout_innerscrollview));
} else if (a instanceof FragmentStatePagerAdapter) {
FragmentStatePagerAdapter fsAdapter = (FragmentStatePagerAdapter) a;
Fragment item = (Fragment) fsAdapter.instantiateItem(mViewPager,
currentItem);
mInnerScrollView = (ScrollView) (item.getView()
.findViewById(R.id.id_stickynavlayout_innerscrollview));
}
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mTop = findViewById(R.id.id_stickynavlayout_topview);
mNav = findViewById(R.id.id_stickynavlayout_indicator);
View view = findViewById(R.id.id_stickynavlayout_viewpager);
if (!(view instanceof ViewPager)) {
throw new RuntimeException(
"id_stickynavlayout_viewpager show used by ViewPager !");
}
mViewPager = (ViewPager) view;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
ViewGroup.LayoutParams params = mViewPager.getLayoutParams();
/**Because StickyNavLayout is match_parent, getMeasuredHeight() is equal to screen height-status bar height
*
*/
params.height = getMeasuredHeight() - mNav.getMeasuredHeight();
mTopViewHeight = mTop.getMeasuredHeight();
}
/**
* When is it necessary to intercept and when does it need to be intercepted?
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int action = ev.getAction();
float x = ev.getX();
float y = ev.getY();
switch (action) {
case MotionEvent.ACTION_DOWN:
lastX=x;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
dx=x-lastX;
dy = y - lastY;
lastX = x;
lastY = y;
getCurrentScrollView();
if(onIntercept){
//If it is horizontal sliding
if(Math.abs(dx)>Math.abs(dy)){
onIntercept =false;
}else{
// If the topView is hidden and slides up, the current event is changed to ACTION_DOWN
if (isTopHidden && dy < 0) {
onIntercept = false;
ev.setAction(MotionEvent.ACTION_DOWN); //Manually call the dispatch event
dispatchTouchEvent(ev);//Send events manually
}
}
}else{
//Vertical sliding
if(Math.abs(dy)>Math.abs(dx)){
// If topView is not hidden
// Or sc listView at the top && topView hide && drop down, then intercept
if(!isTopHidden||(isTopHidden&&dy > 0 && mInnerScrollView.getScrollY() == 0 )){
onIntercept=true;
ev.setAction(MotionEvent.ACTION_DOWN);
dispatchTouchEvent(ev);
}
}
}
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_MOVE:
if (onIntercept) {
return true;
}
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(event);
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
scrollBy(0, (int) -dy);
break;
case MotionEvent.ACTION_CANCEL:
recycleVelocityTracker();
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
case MotionEvent.ACTION_UP:
mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);//How many pixels are moved in a unit time of 1 second
int velocityY = (int) mVelocityTracker.getYVelocity();//Get speed in y direction
if (Math.abs(velocityY) > mMinimumVelocity) {
fling(-velocityY);
}
recycleVelocityTracker();
break;
}
return true;
}
public void fling(int velocityY) {
/*
fling(int startX, int startY, int velocityX, int velocityY,int minX, int maxX, int minY, int maxY)
*/
mScroller.fling(0, getScrollY(), 0, velocityY, 0, 0, 0, mTopViewHeight);
invalidate();
}
@Override
public void scrollTo(int x, int y) {
super.scrollTo(x, y);
if (getScrollY() > mTopViewHeight) {
setScrollY(mTopViewHeight);
}
if (getScrollY() < 0) {
setScrollY(0);
}
isTopHidden = (getScrollY() == mTopViewHeight);
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {
scrollTo(0, mScroller.getCurrY());
invalidate();
}
}
private void initVelocityTrackerIfNotExists() {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
}
private void recycleVelocityTracker() {
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
}
view raw StickyNavLayout.java hosted with ❤ by GitHub

Step 4: Creating the MainActivity.java file

After that in our MainActivity, start ViewPager. There is no complicated code, the main thing to start our Vp as shown below;

public class MainActivity extends FragmentActivity {
private String[] mTitles = new String[] { "Tab 1", "Tab 2", "Tab 3" };
private SimpleViewPagerIndicator mIndicator;
private ViewPager mViewPager;
private FragmentPagerAdapter mAdapter;
private TabFragment[] mFragments = new TabFragment[mTitles.length];
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initViews();
initDatas();
initEvents();
}
view raw MainActivity.java hosted with ❤ by GitHub

By the way, I wrote this guide for a while, and it can be considered a cultural control. Especially going with Vp. Please see the detailed typing method: Android teaches you to build a cool ViewPagerIndicator. Not only is MIUI a top impersonation, but the triangle change becomes underscore. Therefore, it’s not the focus of this article, you can ignore it for a while.

Each page in our Vp is a Fragment, we will not send a Fragment code. The layout is ScrollView as root layout, and the interior is filled with customization. For details, please refer to the source code.

After the introduction of the application, I feel a little happier. Basically write the composition according to the procedure, and the result is seen automatically.

Eventually, MainActivity.java will be like this.

package com.quick.lib;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v4.view.ViewPager.OnPageChangeListener;
public class MainActivity extends FragmentActivity {
private String[] mTitles = new String[] { "Tab 1", "Tab 2", "Tab 3" };
private SimpleViewPagerIndicator mIndicator;
private ViewPager mViewPager;
private FragmentPagerAdapter mAdapter;
private TabFragment[] mFragments = new TabFragment[mTitles.length];
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initViews();
initDatas();
initEvents();
}
private void initEvents() {
mViewPager.setOnPageChangeListener(new OnPageChangeListener() {
@Override
public void onPageSelected(int position) {
}
@Override
public void onPageScrolled(int position, float positionOffset,
int positionOffsetPixels) {
mIndicator.scroll(position, positionOffset);
}
@Override
public void onPageScrollStateChanged(int state) {
}
});
}
private void initDatas() {
mIndicator.setTitles(mTitles);
for (int i = 0; i < mTitles.length; i++) {
mFragments[i] = (TabFragment) TabFragment.newInstance(mTitles[i]);
}
mAdapter = new FragmentPagerAdapter(getSupportFragmentManager()) {
@Override
public int getCount() {
return mTitles.length;
}
@Override
public Fragment getItem(int position) {
return mFragments[position];
}
};
mViewPager.setAdapter(mAdapter);
mViewPager.setCurrentItem(0);
}
private void initViews() {
mIndicator = (SimpleViewPagerIndicator) findViewById(R.id.id_stickynavlayout_indicator);
mViewPager = (ViewPager) findViewById(R.id.id_stickynavlayout_viewpager);
}
}
view raw MainActivity.java hosted with ❤ by GitHub

Future Directions

Finally, future indicators will include showing sticky navigation structures from different functions. Different capabilities can be enabled in the app by enabling the use of different adhesive properties.

Learning Strategies and Tools

Sticky navigation layout has been made possible with the use of Android studio and Java experience. Finally, we learned how your app can use the sticky navigation layout on Android. Tasks now have to use different functions in their proper roles to implement sticky navigation layout on android. I used the android documentation to learn more about layout fundamentals. The page provides an overview of the Layout Editor.

Reflective Analysis

Finding out how to use ScrollView administrators on Android was a great learning experience. An amazing tool that makes ScrollView comprehension areas easier. Convert ScrollView as an external layer to a custom controller, acquired as a LinearLayout, called StickyNavLayout. Android apps.

In conclusion, I spent 48 hours completing the project and the blog. Finally, check out the source code here in the GitHub repository.

Link to previous posts: https://blog.learningdollars.com/2020/09/16/how-to-check-internet-connection-programatically-on-android-from-a-button-click-in-kotlin

That’s all about this lesson!