Passing Scroll event to another view - android

The Objective:
I am writing a view that behaves like a RecyclerView inside the ViewPager i-e; The view moves when the finger is moved horizontally and RecyclerView scrolls when the finger is moved vertically. Please note that my custom view has another objective while the finger is moving horizontally, so therefore I can't use viewpager here.
The Problem:
The overlay view handles the horizontal finger motion movement properly, however the vertical event when passed down to the RecyclerView doesn't result in scrolling.
The Question:
What should I do that the vertical finger movement is passed to the RecyclerView and result in Scrolling?
The Code:
mOverlayView.setOnTouchListener(new View.OnTouchListener() {
switch (action) {
case MotionEvent.ACTION_UP:
//some code
return true;
case MotionEvent.ACTION_DOWN:
mBackupTouchDownEvent = event;
// some code
return true;
case MotionEvent.ACTION_MOVE:
// some logic {
movingHorizontally = true;
}
return true;
} else
movingHorizontally = false;
break;
}
// executor came down here? This means finger moved vertically
mRecyclerView.requestFocus();
mRecyclerView.dispatchTouchEvent(mBackupTouchDownEvent); // don't cause scrolling
mRecyclerView.dispatchTouchEvent(event); // don't cause scrolling? Alternative solutoin?
return false;
}
XML:
<RelativeLayout
android:background="#color/colorPrimary"
android:layout_width="400dp"
android:layout_height="500dp"
android:layout_centerVertical="true"
>
<android.support.v7.widget.RecyclerView
android:layout_width="380dp"
android:layout_height="match_parent"
android:id="#+id/scrollView">
</android.support.v7.widget.RecyclerView>
<RelativeLayout
android:layout_width="300dp"
android:layout_height="match_parent"
android:background="#77555555"
android:id="#+id/overlayScreen"/>
</RelativeLayout>

Related

Horizontal Recyclerview in Navigation drawer

I'm trying to implement a horizontal recyclerview in a Navigation Drawer. It works fine aside from the fact that when I swipe to the left, the Navigation Drawer gets closed. Is there a way to prevent the Navigation Drawer from closing just when the user interacts with the recyclerview? I tried this way:
mNavigationRecyclerHourly.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View view, MotionEvent motionEvent) {
Log.d(TAG, "onTouch: ");
switch (motionEvent.getAction()) {
case MotionEvent.ACTION_DOWN: mDrawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_OPEN);
break;
case MotionEvent.ACTION_MOVE: mDrawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_OPEN);
break;
case MotionEvent.ACTION_UP: mDrawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
break;
}
return true;
}
});
Recyclerview xml:
<android.support.v7.widget.RecyclerView
android:id="#+id/recyclerview_navigation_hourly"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="#id/progress_navigation_uv"
android:scrollbars="horizontal"
android:overScrollMode="never"
android:scrollbarThumbHorizontal="#drawable/thumb_navigation"
android:layout_alignParentStart="true"
android:padding="8dp">
</android.support.v7.widget.RecyclerView>
Thanks for your help!

Android Layout: Horizontal Recyclerview inside a Vertical Recyclerview inside a Viewpager with Scroll Behaviors

This is the app I'm trying to build with all the elements mapped out below:
Everything works, however, I want the inner horizontal recyclerview not to capture any of the vertical scrolls. All vertical scrolls must go towards the outer vertical recyclerview, not the horizontal one, so that the vertical scroll would allow for the toolbar to exit out of view according to it's scrollFlag.
When I put my finger on the "StrawBerry Plant" part of the recyclerview and scroll up, it scroll out the toolbar:
If I put my finger on the horizontal scrollview and scroll up, it does not scroll out the toolbar at all.
The following is my xml layout code so far.
The Activity xml layout:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/fragment_container"
android:clipChildren="false">
<android.support.design.widget.CoordinatorLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/container"
>
<android.support.design.widget.AppBarLayout
android:id="#+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:minHeight="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_scrollFlags="scroll|enterAlways">
</android.support.v7.widget.Toolbar>
<android.support.design.widget.TabLayout
android:id="#+id/sliding_tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
style="#style/CustomTabLayout"
/>
</android.support.design.widget.AppBarLayout>
<android.support.v4.view.ViewPager
android:id="#+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
/>
</android.support.design.widget.CoordinatorLayout>
</FrameLayout>
The "Fruits" fragment xml layout (which is the code for the fragment - the fragment is labeled in the above picture):
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/progressBar"
android:visibility="gone"
android:layout_centerInParent="true"
android:indeterminate="true"/>
<!-- <android.support.v7.widget.RecyclerView-->
<com.example.simon.customshapes.VerticallyScrollRecyclerView
android:id="#+id/main_recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</RelativeLayout>
I have used a custom class called VerticallyScrollRecyclerView which follows google example of handling touch events in a viewgroup. Its aim is to intercept and consume all the vertical scroll events so that it will scroll in / out the toolbar: http://developer.android.com/training/gestures/viewgroup.html
The code for VerticallyScrollRecyclerView is below:
public class VerticallyScrollRecyclerView extends RecyclerView {
public VerticallyScrollRecyclerView(Context context) {
super(context);
}
public VerticallyScrollRecyclerView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public VerticallyScrollRecyclerView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
ViewConfiguration vc = ViewConfiguration.get(this.getContext());
private int mTouchSlop = vc.getScaledTouchSlop();
private boolean mIsScrolling;
private float startY;
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);
// Always handle the case of the touch gesture being complete.
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
// Release the scroll.
mIsScrolling = false;
startY = ev.getY();
return super.onInterceptTouchEvent(ev); // Do not intercept touch event, let the child handle it
}
switch (action) {
case MotionEvent.ACTION_MOVE: {
Log.e("VRecView", "its moving");
if (mIsScrolling) {
// We're currently scrolling, so yes, intercept the
// touch event!
return true;
}
// If the user has dragged her finger horizontally more than
// the touch slop, start the scroll
// left as an exercise for the reader
final float yDiff = calculateDistanceY(ev.getY());
Log.e("yDiff ", ""+yDiff);
// Touch slop should be calculated using ViewConfiguration
// constants.
if (Math.abs(yDiff) > 5) {
// Start scrolling!
Log.e("Scroll", "we are scrolling vertically");
mIsScrolling = true;
return true;
}
break;
}
}
return super.onInterceptTouchEvent(ev);
}
private float calculateDistanceY(float endY) {
return startY - endY;
}
}
The "Favourite" layout which is the recyclerview within the vertical recyclerview:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:android="http://schemas.android.com/apk/res/android"
android:background="#color/white"
xmlns:app="http://schemas.android.com/apk/res-auto">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Favourite"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:layout_marginLeft="16dp"
android:id="#+id/header_fav"/>
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_below="#+id/header_fav"
android:id="#+id/recyclerview_fav">
</android.support.v7.widget.RecyclerView>
</RelativeLayout>
This has been bugging me for a while now and I have not managed to come up with a solution. Does anyone know how to solve this problem?
5 points to Griffindor for the correct answer and of course, reputation points on SO.
Tested solution:
All you need is to call mInnerRecycler.setNestedScrollingEnabled(false); on your inner RecyclerViews
Explanation:
RecyclerView has support for nested scrolling introduced in API 21 through implementing the NestedScrollingChild interface. This is a valuable feature when you have a scrolling view inside another one that scrolls in the same direction and you want to scroll the inner View only when focused.
In any case, RecyclerView by default calls RecyclerView.setNestedScrollingEnabled(true); on itself when initializing. Now, back to the problem, since both of your RecyclerViews are within the same ViewPager that has the AppBarBehavior, the CoordinateLayout has to decide which scroll to respond to when you scroll from your inner RecyclerView; when your inner RecyclerView's nested scrolling is enabled, it gets the scrolling focus and the CoordinateLayout will choose to respond to its scrolling over the outer RecyclerView's scrolling. The thing is that, since your inner RecyclerViews don't scroll vertically, there is no vertical scroll change (from the CoordinateLayout's point of view), and if there is no change, the AppBarLayout doesn't change either.
In your case, because your inner RecyclerViews are scrolling in a different direction, you can disable it, thus causing the CoordinateLayout to disregard its scrolling and respond to the outer RecyclerView's scrolling.
Notice:
The xml attribute android:nestedScrollingEnabled="boolean" is not intended for use with the RecyclerView, and an attempt to use android:nestedScrollingEnabled="false" will result in a java.lang.NullPointerException so, at least for now, you will have to do it in code.
if any one still looking , try this :
private val Y_BUFFER = 10
private var preX = 0f
private var preY = 0f
mView.rv.addOnItemTouchListener(object : RecyclerView.OnItemTouchListener {
override fun onTouchEvent(p0: RecyclerView, p1: MotionEvent) {
}
override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
when (e.action) {
MotionEvent.ACTION_DOWN -> rv.parent.requestDisallowInterceptTouchEvent(true)
MotionEvent.ACTION_MOVE -> {
if (Math.abs(e.x - preX) > Math.abs(e.y - preY)) {
rv.parent.requestDisallowInterceptTouchEvent(true)
} else if (Math.abs(e.y - preY) > Y_BUFFER) {
rv.parent.requestDisallowInterceptTouchEvent(false)
}
}
}
preX = e.x
preY = e.y
return false
}
override fun onRequestDisallowInterceptTouchEvent(p0: Boolean) {
}
})
it checks if currently scrolling horizontal then don't allow parent to handel event
I am a bit late but this will defintly work for others facing the same problem
mRecyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
#Override
public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
int action = e.getAction();
// Toast.makeText(getActivity(),"HERE",Toast.LENGTH_SHORT).show();
switch (action) {
case MotionEvent.ACTION_POINTER_UP:
rv.getParent().requestDisallowInterceptTouchEvent(true);
break;
}
return false;
}
Tested solution, use a custom NestedScrollView().
Code:
public class CustomNestedScrollView extends NestedScrollView {
public CustomNestedScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
// Explicitly call computeScroll() to make the Scroller compute itself
computeScroll();
}
return super.onInterceptTouchEvent(ev);
}
}
try
public OuterRecyclerViewAdapter(List<Item> items) {
//Constructor stuff
viewPool = new RecyclerView.RecycledViewPool();
}
#Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
//Create viewHolder etc
holder.innerRecyclerView.setRecycledViewPool(viewPool);
}
inner recylerview will use the same viewpool and it'll be smoother
I would suggest you add the horizontal recyclerview inside fragments like the google app
i read through the offered answers of using setNestedScrollingEnabled to false and it was awful for me as it makes the recyclerview not recycle and you can get crashes in memory, performance issues maybe etc if you have a huge list. so i will give you a algorithm to make it work without any code.
have a listener on the vertical recyclerview, such as a scroll listener. anytime the list is scrolled you will get a callback that its being scrolled. you should also get a call back when its idle.
now when vertical recylerview is being scrolled, setNestedScrollingEnabled = false on the horizontal list
once vertical recyclerview is idle setNestedScrollingEnabled = true on the same horizontal list.
also initially set the horizontal recyclerview to setNestedScrollingEnabled = false in xml
this can also work great also with appbarlayout when coordinatorLayout gets confused with two recyclerviews in different directions, but thats another question.
lets take a look at another more simple way in kotlin, to get this done with a real example. here we will focus just on the horizontal recyclerView:
assume we have RecyclerViewVertical & RecyclerViewHorizontal:
RecyclerViewHorizontal.apply {
addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
isNestedScrollingEnabled = RecyclerView.SCROLL_STATE_IDLE != newState
}
})
}
what this code says is if RecyclerViewHorizontal is not idle then enable nestedScrolling, otherwise disable it. That means when its idle we can now use the RecyclerViewVertical in a coordinatorlayout without any interference from the RecyclerViewHorizontal since we have disabled it when its idle.

Android Viewpager as Header in ParallaxHeader Example does not scroll as expected

I am trying to implement the ParallaxHeaderViewPager like here. But instead using a KenBurnsSupportView and an ImageView as a Header i have used a FrameLayout that contains a ViewPager. My problem is that if i try to use vertical scroll on the Header the Event gets consumed but nothing happens. I can only scroll horizontally on the Header(ViewPager) and the Page changes correctly. But this is not what i want to accomplish. I want if i scroll vertically on the Header the event to be recognized as a ListView scroll and when i scroll horizontally (on the Header) as a ViewPager scroll. The scrolling is functioning correctly on the area that is not used from the header. Namely under the header where i have 4 Listviews I can scroll both vertically and horizontally. The Layout I am currently using is like the below code snippet:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<android.support.v4.view.ViewPager
android:id="#+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<FrameLayout
android:id="#+id/header"
android:layout_width="match_parent"
android:layout_height="#dimen/header_height" >
<FrameLayout
android:id="#+id/news_pager_content_area"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:background="#color/white"
/>
<com.astuetz.PagerSlidingTabStrip
android:id="#+id/tabs"
android:layout_width="match_parent"
android:layout_height="48dip"
android:layout_gravity="bottom"
android:background="#android:color/transparent" />
</FrameLayout>
I have tried to subclass both the Listviews and the Viewpager to intercept and handle touch Events but it seems that the default behavior documented in the Android Documentation is not applicable in my example. Thank you for your help.
I solved my question trying all the answers at similar topics on Stackoverflow and looking at the Android Documentation again. I created a custom FrameLayout for the root container of the above Layout file. I override the dispatchTouchEvent method.
public boolean dispatchTouchEvent(MotionEvent ev)
{
switch (ev.Action)
{
case MotionEventActions.Down:
_xDistance = _yDistance = 0f;
_lastX = ev.GetX();
_lastY = ev.GetY();
_startY = _lastY;
_isFirstTime = true;
_downEvent = MotionEvent.obtain(ev);
break;
case MotionEventActions.Move:
float curX = ev.GetX();
float curY = ev.GetY();
_xDistance += Math.abs(curX - _lastX);
_yDistance += Math.abs(curY - _lastY);
_lastX = curX;
_lastY = curY;
float yDeltatTotal = curY - _startY;
if (_yDistance > _xDistance && Math.abs(yDeltatTotal) > _touchSlop)
{
if (_isFirstTime)
{
_isFirstTime = false;
return childView.dispatchTouchEvent(_downEvent);
}
return childView.dispatchTouchEvent(ev);
}
}
return super.DispatchTouchEvent(ev);
}
Where as ChildView I set the ViewPager with id = pager from the layout above. To avoid pointerindex out of range Exception I used the obtain method as specified by Dmitry Zaitsev in the first comment at this link.

How to use a view pager inside a navigation drawer? [duplicate]

I want to use a seekbar in a Navigation Drawer
Basically I need to slide left and right to set the seekbar and, well, you guessed it, it slides the navigation drawer...
What I would like to do is to slide the seekbar when it's focused and the navigationdrawer when it's not.
How should I do that?
Here is my code when setting an item (I haven't set the seekbar yet)
private View onCritereView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View mView = inflater.inflate(R.layout.fragment_critere, container, false);
LinearLayout mLinearLayout = (LinearLayout) mView.findViewById(R.id.critere_linearlayout);
View mViewElement = inflater.inflate(R.layout.item_critere, null);
((TextView)mViewElement.findViewById(R.id.critere_title)).setText("Prix Maximum");
mLinearLayout.addView(mViewElement);
return mView;
}
Here is the item_critere.xml with the seekbar:
<?xml version="1.0" encoding="utf-8"?>
<TableLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/critere_item"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:stretchColumns="*">
<TableRow
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="Large Text"
android:id="#+id/critere_title"
android:layout_gravity="left|center_vertical"
android:layout_column="0"/>
<SeekBar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/critere_qualifier"
android:layout_column="1"
android:layout_span="4"/>
</TableRow>
<TableRow
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
android:text="Medium Text"
android:id="#+id/critere_value"
android:layout_span="4"/>
</TableRow>
</TableLayout>
Thanks in advance :)
Just add an onTouchListener. When you touch the screen on the seekbar (action_down) disallow parent to intercept the event.
seekbar.setOnTouchListener(new ListView.OnTouchListener()
{
#Override
public boolean onTouch(View v, MotionEvent event)
{
int action = event.getAction();
switch (action)
{
case MotionEvent.ACTION_DOWN:
// Disallow Drawer to intercept touch events.
v.getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_UP:
// Allow Drawer to intercept touch events.
v.getParent().requestDisallowInterceptTouchEvent(false);
break;
}
// Handle seekbar touch events.
v.onTouchEvent(event);
return true;
}
});
Use DrawerLayout.setDrawerLockMode
instead of ViewParent.requestDisallowInterceptTouchEvent.
In #dumazy answer requestDisallowInterceptTouchEvent solves this issue but it also creates another.
While you are scrolling on navigation drawer, If you touch on SeekBar, OnSeekBarChangeListener will trigger. So scrolling event will break and seekbar progress will start to change by moving your finger.
I modified the original answer a little bit to solve this issue using DrawerLockMode:
mSeekBar.setOnTouchListener(new SeekBar.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
// Disallow Drawer to intercept touch events.
// v.getParent().requestDisallowInterceptTouchEvent(true);
mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_OPEN);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
// Allow Drawer to intercept touch events.
// v.getParent().requestDisallowInterceptTouchEvent(false);
mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNDEFINED);
break;
}
// Handle SeekBar touch events.
v.onTouchEvent(event);
return true;
}
});

Using a Viewflipper with Gridview

I've created a calendar which works fine, using a GridView which has an OnClickListener.
Now I wrapped the two GridViews in a ViewFlipper. The ViewFlipper has an OnTouchListener which also works fine, I can change the view by using ontouch when dragging. The problem is though that I have to drag on the EMTPY space in the Activity in order to use the ViewFlipper. When I drag on the GridView, nothing happends at all. But I can click on the GridView for OnClickListener.
xml:
<ViewFlipper android:id="#+id/details"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="horizontal">
<GridView
android:id="#+id/weeks"
android:numColumns="1"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="8">
</GridView>
<GridView
android:id="#+id/calendar"
android:numColumns="7"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1">
</GridView>
</LinearLayout>
android code:
#Override
public boolean onTouch(View arg0, MotionEvent arg1) {
// Get the action that was done on this touch event
switch (arg1.getAction())
{
case MotionEvent.ACTION_DOWN:
{
// store the X value when the user's finger was pressed down
downXValue = arg1.getX();
break;
}
case MotionEvent.ACTION_UP:
{
// Get the X value when the user released his/her finger
currentX = arg1.getX();
// going backwards: pushing stuff to the right
if (currentX - downXValue < -(arg0.getWidth()/3))
{
mdh.nextMonth();
calendar.add(Calendar.MONTH, 1);
currentMonth.setText(new SimpleDateFormat("MMMM yyyy").format(calendar.getTime()));
cAdapter.notifyDataSetChanged();
updateWeeks();
// Set the animation
vf.setInAnimation(arg0.getContext(), R.anim.push_left_in);
vf.setOutAnimation(arg0.getContext(), R.anim.push_left_out);
// Flip!
vf.showPrevious();
}
// going forwards: pushing stuff to the left
if (currentX - downXValue > arg0.getWidth()/3)
{
mdh.previousMonth();
calendar.add(Calendar.MONTH, -1);
currentMonth.setText(new SimpleDateFormat("MMMM yyyy").format(calendar.getTime()));
cAdapter.notifyDataSetChanged();
updateWeeks();
// Set the animation
vf.setInAnimation(arg0.getContext(), R.anim.push_right_in);
vf.setOutAnimation(arg0.getContext(), R.anim.push_right_out);
// Flip!
vf.showNext();
}
break;
}
gridview.setOnTouchListener(new OnTouchListener(){
#Override
public boolean onTouch(View v, MotionEvent event) {
return detector.onTouchEvent(event);
}
});
I had the same issue with a ViewFlipper and ScrollViews.
Try adding the onTouchListener to your GridView aswell and it should work.

Categories

Resources