I have to achieve a view with ViewPager style swiping effect but in vertical. Also, it has to be synchronized with tabs also in vertical form. Lucky enough, in my project, it doesn't have to deal with lots of dynamic data. And here's how I do it(with the solution from Stackoverflow).
See also:
Check the Demo Code on github.
Final result
Create a custom view and extends ViewPager. Then, use it as a normal ViewPager in my activity.
VerticalViewPager.java
public class VerticalViewPager extends ViewPager {
public VerticalViewPager(Context context) {
super(context);
init();
}
public VerticalViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
// for futher
}
}
Add custom view in layout
After created a custom VerticalViewPager, use it in the layout file.
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="me.tomazwang.project.verticalviewpager.MainActivity">
<me.tomazwang.project.verticalviewpager.view.VerticalViewPager
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/vertical_viewPager"/>
</RelativeLayout>
Create a simple ViewPagerAdpater
Then, create a simple ViewPagerAdapter for your data.( I use FragmentPagerAdapter in case I have to do complex layout handling in the future).
PageAdapter
public class PageAdapter extends FragmentPagerAdapter {
String[] data;
public PageAdapter(FragmentManager fm, String[] data) {
super(fm);
this.data = data;
}
@Override
public Fragment getItem(int position) {
return PagerFragmet.newInstance(data[position], position);
}
@Override
public int getCount() {
return data.length;
}
}
Put them together
MainActivity.java
public class MainActivity extends AppCompatActivity{
private String[] data;
ViewPager viewPager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
viewPager = (ViewPager) findViewById(R.id.vertical_viewPager);
initData();
initView();
}
private void initData() {
this.data = new String[]{
"Page 1",
"Page 2",
"Page 3",
"Page 4",
"Page 5",
"Page 6"
};
}
private void initView() {
PageAdapter viewPageAdapter = new PageAdapter(getSupportFragmentManager(), data);
viewPager.setAdapter(viewPageAdapter);
}
}
Now, we got a horizontal ViewPager just like what a ViewPager should be. Let's turn it into vertical!
The simplest way to make a ViewPager scrolls the way you wish is to implement your own PageTransformer. By doing so, you can overwrite the default behavior of ViewPager.
Set a PageTransformer
In our init()
method in VerticalViewPager, give this ViewPager a custom PageTransformer. Also, we disable the over-scroll shadow here.
...
private void init() {
// use PageTransformer to implement vertical viewpager
setPageTransformer(true, new VerticalViewPager.PageTransformer());
// disable over scroll shadow
setOverScrollMode(OVER_SCROLL_NEVER);
}
...
Implement PageTransformer as an inner-class of VerticalViewPager. This PageTransformer disable the horizontal scrolling of ViewPager and add Vertical scrolls to it. According to the "position" parameter.
public class PageTransformer implements ViewPager.PageTransformer {
@Override
public void transformPage(View page, float position) {
if (position < -1) {
// [-infinity, -1], view page is off-screen to the left
// hide the page.
page.setVisibility(View.INVISIBLE);
} else if (position <= 1) {
// [-1, 1], page is on screen
// show the page
page.setVisibility(View.VISIBLE);
// get page back to the center of screen since it will get swipe horizontally by default.
page.setTranslationX(page.getWidth() * -position);
// set Y position to swipe in vertical direction.
float y = position * page.getHeight();
page.setTranslationY(y);
} else {
// [1, +infinity], page is off-screen to the right
// hide the page.
page.setVisibility(View.INVISIBLE);
}
}
}
However, the "position" parameter in method transformPage(View page, float position)
is the x translation. Therefore, we have a ViewPager scrolls in vertical( which is what we want) but only when you swipe it horizontal.
Here's how we deal with this problem. We capture the touch event before we switch the x and y. Trick the ViewPager to believe that our vertical swipe is actually horizontal swipe, which makes the view scrolls vertically since it uses our custom PageTransformer.
VerticalViewPager.java
...
// swap x and y
private MotionEvent swapXY(MotionEvent ev) {
float width = getWidth();
float height = getHeight();
float newX = (ev.getY() / height) * width;
float newY = (ev.getX() / width) * height;
ev.setLocation(newX, newY);
return ev;
}
...
To capture touch event, we override onTouchEvent()
and onInterceptTouchEvent()
in VerticalViewPager.
...
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean interceped = super.onInterceptTouchEvent(swapXY(ev));
swapXY(ev); // swap x,y back for other touch events.
return interceped;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
return super.onTouchEvent(swapXY(ev));
}
// swap x and y
private MotionEvent swapXY(MotionEvent ev) {
float width = getWidth();
float height = getHeight();
float newX = (ev.getY() / height) * width;
float newY = (ev.getX() / width) * height;
ev.setLocation(newX, newY);
return ev;
}
...
And we got a ViewPager slides in vertical.
About the tabs, I use ListView( which is not the best solution, but it works).
Add a ListView in layout
activity_main.xml
...
<ListView
android:id="@+id/lv_tabs"
android:layout_width="100dp"
android:layout_height="match_parent"/>
<me.tomazwang.project.verticalviewpager.view.VerticalViewPager
android:layout_toRightOf="@id/lv_tabs"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/vertical_viewPager"/>
...
Make the ListView looks like tabs
To make the ListView act like tabs, I implement the adapter with select functions. And use liseners to synchronize it and ViewPager.
public class TabAdapter extends BaseAdapter implements AdapterView.OnItemClickListener{
private final String[] data;
private final ListView listView;
private OnItemClickListener listener;
private int currentSelected = 0;
public TabAdapter(String[] data, ListView listView, OnItemClickListener listener){
this.data = data;
this.listView = listView;
this.listener = listener;
listView.setOnItemClickListener(this);
}
...
// Override other Adpter method here.
...
@Override
public View getView(int i, View view, ViewGroup viewGroup) {
// Currently not using viewHolder pattern cause there aren't too many tabs in the demo project.
if(view == null){
view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item_tab, viewGroup, false);
}
TextView tabTitle = (TextView)view.findViewById(R.id.txt_tab_title);
tabTitle.setText((String)getItem(i));
if(i == currentSelected){
// change the appearance
}else{
// change the appearance
}
return view;
}
/**
* Return item view at the given position or null if position is not visible.
*/
public View getViewByPosition(int pos) {
if(listView == null){
return null;
}
final int firstListItemPosition = listView.getFirstVisiblePosition();
final int lastListItemPosition = firstListItemPosition + listView.getChildCount() - 1;
if (pos < firstListItemPosition || pos > lastListItemPosition ) {
return null;
} else {
final int childIndex = pos - firstListItemPosition;
return listView.getChildAt(childIndex);
}
}
private void select(int position){
if(currentSelected >= 0){
deselect(currentSelected);
}
View targetView = getViewByPosition(position);
if(targetView != null) {
// change the appearance
}
if(listener != null){
listener.selectItem(position);
}
currentSelected = position;
}
private void deselect(int position) {
if(getViewByPosition(position) != null){
View targetView = getViewByPosition(position);
if(targetView != null) {
// change the appearance
}
}
currentSelected = -1;
}
// OnClick Events
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
select(i);
}
public void OnItemClickListener(TabAdapter.OnItemClickListener listener){
this.listener = listener;
}
public void setCurrentSelected(int i) {
select(i);
}
public interface OnItemClickListener{
void selectItem(int position);
}
}
Syncronize VerticalViewPager and TabAdapter
Use OnItemClickListener
from TabAdapter and OnPageChangeListener
from ViewPager, we can notify one when the other is changing.
// Implements TabAdapter.OnItemClickListener
@Override
public void selectItem(int position) {
viewPager.setCurrentItem(position, true);
}
// Implements ViewPager.OnPageChangeListener
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}
@Override
public void onPageScrollStateChanged(int state) {}
@Override
public void onPageSelected(int position) {
if(tabAdapter != null){
tabAdapter.setCurrentSelected(position);
}
}
And we hava a ViewPager with tabs in vertical.
This article is also post on my Logdown Blog.