Skip to content

Commit 4bc0ef1

Browse files
committed
Add ViewPager2 fragment
Update DynamicFrameLayout to support nested scrolling in same direction inside a ViewPager2.
1 parent 874cfd5 commit 4bc0ef1

File tree

4 files changed

+369
-0
lines changed

4 files changed

+369
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/*
2+
* Copyright 2019 Pranav Pandey
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.pranavpandey.android.dynamic.support.fragment;
18+
19+
import android.os.Bundle;
20+
import android.view.LayoutInflater;
21+
import android.view.View;
22+
import android.view.ViewGroup;
23+
24+
import androidx.annotation.NonNull;
25+
import androidx.annotation.Nullable;
26+
import androidx.fragment.app.Fragment;
27+
import androidx.viewpager.widget.ViewPager;
28+
import androidx.viewpager2.widget.ViewPager2;
29+
30+
import com.google.android.material.tabs.TabLayout;
31+
import com.google.android.material.tabs.TabLayoutMediator;
32+
import com.pranavpandey.android.dynamic.support.R;
33+
import com.pranavpandey.android.dynamic.support.activity.DynamicActivity;
34+
import com.pranavpandey.android.dynamic.support.adapter.DynamicFragmentStateAdapter;
35+
import com.pranavpandey.android.dynamic.support.listener.DynamicViewPagerCallback;
36+
37+
/**
38+
* An abstract {@link ViewPager} fragment to display multiple fragments inside the view pager
39+
* along with the tabs.
40+
* <p>Just extend this fragment and implement the necessary methods to use it inside an activity.
41+
*/
42+
public abstract class DynamicViewPager2Fragment extends
43+
DynamicFragment implements DynamicViewPagerCallback {
44+
45+
/**
46+
* Fragment argument key to set the initial view pager page.
47+
*/
48+
public static String ADS_ARGS_VIEW_PAGER_PAGE = "ads_args_view_pager_page";
49+
50+
/**
51+
* View pager used by this fragment.
52+
*/
53+
private ViewPager2 mViewPager;
54+
55+
/**
56+
* Tab layout used by this fragment.
57+
*/
58+
private TabLayout mTabLayout;
59+
60+
/**
61+
* View pager adapter used by this fragment.
62+
*/
63+
private ViewPagerAdapter mAdapter;
64+
65+
@Override
66+
public void onCreate(@Nullable Bundle savedInstanceState) {
67+
super.onCreate(savedInstanceState);
68+
69+
setRetainInstance(false);
70+
}
71+
72+
@Override
73+
public View onCreateView(@NonNull LayoutInflater inflater,
74+
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
75+
return inflater.inflate(R.layout.ads_fragment_view_pager_2, container, false);
76+
}
77+
78+
@Override
79+
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
80+
super.onViewCreated(view, savedInstanceState);
81+
82+
mViewPager = view.findViewById(R.id.ads_view_pager);
83+
}
84+
85+
@Override
86+
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
87+
super.onActivityCreated(savedInstanceState);
88+
89+
((DynamicActivity) getActivity()).addHeader(R.layout.ads_tabs, true);
90+
mTabLayout = getActivity().findViewById(R.id.ads_tab_layout);
91+
92+
mAdapter = new ViewPagerAdapter(this, this);
93+
mViewPager.setOffscreenPageLimit(mAdapter.getItemCount());
94+
mViewPager.setAdapter(mAdapter);
95+
96+
new TabLayoutMediator(mTabLayout, mViewPager,
97+
new TabLayoutMediator.TabConfigurationStrategy() {
98+
@Override
99+
public void onConfigureTab(@NonNull TabLayout.Tab tab, int position) {
100+
tab.setText(getTitle(position));
101+
}
102+
}).attach();
103+
104+
if (getArguments() != null && getArguments().containsKey(ADS_ARGS_VIEW_PAGER_PAGE)) {
105+
setPage(getArguments().getInt(ADS_ARGS_VIEW_PAGER_PAGE));
106+
}
107+
}
108+
109+
/**
110+
* Get the tab layout used by this fragment.
111+
*
112+
* @return The tab layout used by this fragment.
113+
*/
114+
public TabLayout getTabLayout() {
115+
return mTabLayout;
116+
}
117+
118+
/**
119+
* Get the view pager used by this fragment.
120+
*
121+
* @return The view pager used by this fragment.
122+
*/
123+
public ViewPager2 getViewPager() {
124+
return mViewPager;
125+
}
126+
127+
/**
128+
* Returns the currently selected view pager page or position.
129+
*
130+
* @return The currently selected view pager page or position.
131+
*/
132+
public int getCurrentPage() {
133+
return mViewPager.getCurrentItem();
134+
}
135+
136+
/**
137+
* Set the current page or position for the view pager.
138+
*
139+
* @param page The current position for the view pager.
140+
*/
141+
public void setPage(final int page) {
142+
mViewPager.setCurrentItem(page);
143+
}
144+
145+
/**
146+
* View pager adapter to display the supplied fragments with tab titles.
147+
*/
148+
static class ViewPagerAdapter extends DynamicFragmentStateAdapter {
149+
150+
private final DynamicViewPagerCallback dynamicViewPagerCallback;
151+
152+
/**
153+
* Constructor to initialize an object of this class.
154+
*
155+
* @param fragment The fragment manager to get the child fragment manager.
156+
* @param dynamicViewPagerCallback The view pager callback to return the data.
157+
*/
158+
ViewPagerAdapter(@NonNull Fragment fragment,
159+
@NonNull DynamicViewPagerCallback dynamicViewPagerCallback) {
160+
super(fragment);
161+
162+
this.dynamicViewPagerCallback = dynamicViewPagerCallback;
163+
}
164+
165+
@Override
166+
public @NonNull Fragment createFragment(int position) {
167+
return dynamicViewPagerCallback.createFragment(position);
168+
}
169+
170+
@Override
171+
public int getItemCount() {
172+
return dynamicViewPagerCallback.getItemCount();
173+
}
174+
}
175+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright 2020 Pranav Pandey
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.pranavpandey.android.dynamic.support.listener;
18+
19+
import androidx.annotation.NonNull;
20+
import androidx.annotation.Nullable;
21+
import androidx.fragment.app.Fragment;
22+
23+
/**
24+
* An interface to return the data for {@link androidx.viewpager2.widget.ViewPager2} using
25+
* {@link com.pranavpandey.android.dynamic.support.adapter.DynamicFragmentStateAdapter}.
26+
*/
27+
public interface DynamicViewPagerCallback {
28+
29+
/**
30+
* This method will be called to return the tab title for the supplied position.
31+
*
32+
* @param position The current position of the adapter.
33+
*
34+
* @return The tab title for the supplied position.
35+
*/
36+
@Nullable String getTitle(int position);
37+
38+
/**
39+
* This method will be called on creating the fragment for the supplied position.
40+
*
41+
* @param position The current position of the adapter.
42+
*
43+
* @return The fragment for the supplied position.
44+
*/
45+
@NonNull Fragment createFragment(int position);
46+
47+
/**
48+
* This method will be called to get the item count for the adapter.
49+
*
50+
* @return The item count for the adapter.
51+
*/
52+
int getItemCount();
53+
}

dynamic-support/src/main/java/com/pranavpandey/android/dynamic/support/widget/DynamicFrameLayout.java

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,16 @@
2020
import android.content.res.TypedArray;
2121
import android.graphics.drawable.ColorDrawable;
2222
import android.util.AttributeSet;
23+
import android.view.MotionEvent;
24+
import android.view.View;
25+
import android.view.ViewConfiguration;
2326
import android.widget.FrameLayout;
2427

2528
import androidx.annotation.AttrRes;
2629
import androidx.annotation.ColorInt;
2730
import androidx.annotation.NonNull;
2831
import androidx.annotation.Nullable;
32+
import androidx.viewpager2.widget.ViewPager2;
2933

3034
import com.pranavpandey.android.dynamic.support.R;
3135
import com.pranavpandey.android.dynamic.support.theme.DynamicTheme;
@@ -77,6 +81,21 @@ public class DynamicFrameLayout extends FrameLayout implements DynamicWidget {
7781
*/
7882
private @Theme.BackgroundAware int mBackgroundAware;
7983

84+
/**
85+
* Scaled touch slop used by the view configuration.
86+
*/
87+
private int mTouchSlop;
88+
89+
/**
90+
* Initial horizontal position.
91+
*/
92+
private float mInitialX;
93+
94+
/**
95+
* Initial vertical position.
96+
*/
97+
private float mInitialY;
98+
8099
public DynamicFrameLayout(@NonNull Context context) {
81100
this(context, null);
82101
}
@@ -119,6 +138,7 @@ public void loadFromAttributes(@Nullable AttributeSet attrs) {
119138
a.recycle();
120139
}
121140

141+
mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
122142
initialize();
123143
}
124144

@@ -223,4 +243,103 @@ public void setColor() {
223243
DynamicDrawableUtils.setBackground(this, new ColorDrawable(mColor));
224244
}
225245
}
246+
247+
@Override
248+
public boolean onInterceptTouchEvent(MotionEvent ev) {
249+
handleInterceptTouchEvent(ev);
250+
return super.onInterceptTouchEvent(ev);
251+
}
252+
253+
/**
254+
* Checks whether a child can be scrolled.
255+
*
256+
* @param orientation The current orientation of the view.
257+
* @param delta The delta to check the scrolling.
258+
*
259+
* @return Returns {@code true} if a child can be scrolled.
260+
*/
261+
private boolean canChildScroll(int orientation, float delta) {
262+
if (getChildCount() <= 0) {
263+
return false;
264+
}
265+
266+
int direction = -((int) Math.signum(delta));
267+
switch (orientation) {
268+
default:
269+
throw new IllegalArgumentException();
270+
case ViewPager2.ORIENTATION_HORIZONTAL:
271+
return getChildAt(0).canScrollHorizontally(direction);
272+
case ViewPager2.ORIENTATION_VERTICAL:
273+
return getChildAt(0).canScrollVertically(direction);
274+
}
275+
}
276+
277+
/**
278+
* Returns the parent view pager for this view.
279+
*/
280+
private @Nullable ViewPager2 getParentViewPager() {
281+
if (!(getParent() instanceof View)) {
282+
return null;
283+
}
284+
285+
View view = (View) getParent();
286+
while (view != null && !(view instanceof ViewPager2)) {
287+
if (view.getParent() instanceof View) {
288+
view = (View) view.getParent();
289+
} else {
290+
view = null;
291+
}
292+
}
293+
294+
return (view instanceof ViewPager2) ? (ViewPager2) view : null;
295+
}
296+
297+
/**
298+
* Handle touch event to support nested {@link ViewPager2} scrolling.
299+
*
300+
* @param ev The motion event being dispatched down the hierarchy.
301+
*/
302+
private void handleInterceptTouchEvent(@Nullable MotionEvent ev) {
303+
ViewPager2 viewPager2 = getParentViewPager();
304+
305+
if (ev == null || viewPager2 == null) {
306+
return;
307+
}
308+
309+
int orientation = viewPager2.getOrientation();
310+
311+
// Early return if child can't scroll in same direction as parent.
312+
if (!canChildScroll(orientation, -1.0f) && !canChildScroll(orientation, 1.0f)) {
313+
return;
314+
}
315+
316+
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
317+
mInitialX = ev.getX();
318+
mInitialY = ev.getY();
319+
getParent().requestDisallowInterceptTouchEvent(true);
320+
} else if (ev.getAction() == MotionEvent.ACTION_MOVE) {
321+
float dx = ev.getX() - mInitialX;
322+
float dy = ev.getY() - mInitialY;
323+
boolean isVpHorizontal = orientation == ViewPager2.ORIENTATION_HORIZONTAL;
324+
325+
// Assuming ViewPager2 touch-slop is 2x touch-slop of child.
326+
float scaledDx = Math.abs(dx) * (isVpHorizontal ? 0.5f : 1.0f);
327+
float scaledDy = Math.abs(dy) * (isVpHorizontal ? 1.0f : 0.5f);
328+
329+
if (scaledDx > mTouchSlop || scaledDy > mTouchSlop) {
330+
if (isVpHorizontal == (scaledDy > scaledDx)) {
331+
// Gesture is perpendicular, allow all parents to intercept.
332+
getParent().requestDisallowInterceptTouchEvent(false);
333+
} else {
334+
if (canChildScroll(orientation, isVpHorizontal ? dx : dy)) {
335+
// Child can scroll, disallow all parents to intercept.
336+
getParent().requestDisallowInterceptTouchEvent(true);
337+
} else {
338+
// Child cannot scroll, allow all parents to intercept.
339+
getParent().requestDisallowInterceptTouchEvent(false);
340+
}
341+
}
342+
}
343+
}
344+
}
226345
}

0 commit comments

Comments
 (0)