多TAB可悬浮头控件

强势插入:不想看文章的同学可以直接去下载源代码,代码地址如下:https://github.com/FreeSunny/RefreashTabView

      在前一期中,我们做了悬浮头部的两个tab切换和下拉刷新效果,后来项目中要求改成三个tab,当时就能估量了一下,如果从之前的改,也不是不可以,但是要互相记住的状态就太多了,很容易出现错误。就决定重新实现一下这个效果,为此先写了一个demo,这期间项目都已经又更新了两个版本了。demo还木有变成文章。


     之前的版本中是采用了一个可以下拉刷新的listview,之后在listview中添加了两个头部,并且在该布局上的上面用了一个一模一样的切换tab,如果没有看过前面版本的,可以看看前一个版本,Listview多Tab上滑悬浮。


     我们来看看新的实现,至少2个或者更多的tab,Tab之间可以相互滑动切换,想到这儿估计大家都想到了,这个效果可以采用ViewPager来实现,这样就可以自由滑动了,其次有切换tab,想想易信和微信的主界面,都是可以点击切换和滑动切换的,这两个都采用一个开源控件,PagerSlidingTabStrip,这样就将Viewpager与tab结合在一起了,但是上划悬浮,改控件还没有,我们可以在这个效果上改出我们要的效果。


    基于上述思路我们先来看看页面布局:main_activity

 

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/color_gray_eaeaea" >

<android.support.v4.view.ViewPager
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent" />

<LinearLayout
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:orientation="vertical" >

<ImageView
android:id="@+id/show_event_detail_bg"
android:layout_width="fill_parent"
android:layout_height="135dip"
android:contentDescription="@string/empty"
android:scaleType="fitXY"
android:src="@drawable/header_default_bk" />

<TextView
android:id="@+id/show_event_detail_desc"
android:layout_width="wrap_content"
android:layout_height="104dip"
android:paddingBottom="24dip"
android:layout_marginLeft="15dip"
android:layout_marginRight="15dip"
android:paddingTop="25dip"
android:text="@string/head_title_desc"
android:textColor="@color/color_black_333333"
android:textSize="14sp" />

<View style="@style/horizontal_gray_divider" />

<View style="@style/horizontal_gray_divider" />

<com.example.refreashtabview.sliding.PagerSlidingTabStrip
android:id="@+id/show_tabs"
android:layout_width="match_parent"
android:layout_height="44dip"
android:background="@color/white" />
</LinearLayout>

</RelativeLayout>

页面采用了两层,后面一层为Viewpager,前面为悬浮头与tab切换,在这大家应该都想到了会怎么样实现,Viewpager中添加已经fragment,每个fragment里面加入一个可下拉刷新的Listview,根据ListView的滑动来控制前一帧页面的位置。


来看看页面代码吧,MainAcitivity.java

 

public class MainActivity extends ActionBarActivity implements OnPageChangeListener, ScrollTabHolder {

private PagerSlidingTabStrip tabs;

private ViewPager viewPager;

private SlidingPagerAdapter adapter;

private LinearLayout header;

private int headerHeight;
private int headerTranslationDis;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_activity);
getHeaderHeight();
findViews();
setupPager();
setupTabs();
}

private void findViews() {
tabs = (PagerSlidingTabStrip) findViewById(R.id.show_tabs);
viewPager = (ViewPager) findViewById(R.id.pager);
header = (LinearLayout) findViewById(R.id.header);
}

private void getHeaderHeight() {
headerHeight = getResources().getDimensionPixelSize(R.dimen.max_header_height);
headerTranslationDis = -getResources().getDimensionPixelSize(R.dimen.header_offset_dis);
}

private void setupPager() {
adapter = new SlidingPagerAdapter(getSupportFragmentManager(), this, viewPager);
adapter.setTabHolderScrollingListener(this);//控制页面上滑
viewPager.setOffscreenPageLimit(adapter.getCacheCount());
viewPager.setAdapter(adapter);
viewPager.setOnPageChangeListener(this);
}

private void setupTabs() {
tabs.setShouldExpand(true);
tabs.setIndicatorColorResource(R.color.color_purple_bd6aff);
tabs.setUnderlineColorResource(R.color.color_purple_bd6aff);
tabs.setCheckedTextColorResource(R.color.color_purple_bd6aff);
tabs.setViewPager(viewPager);
}

@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
tabs.onPageScrolled(position, positionOffset, positionOffsetPixels);
}

@Override
public void onPageSelected(int position) {
tabs.onPageSelected(position);
reLocation = true;
SparseArrayCompat<ScrollTabHolder> scrollTabHolders = adapter.getScrollTabHolders();
ScrollTabHolder currentHolder = scrollTabHolders.valueAt(position);
if (NEED_RELAYOUT) {
currentHolder.adjustScroll((int) (header.getHeight() + headerTop));// 修正滚出去的偏移量
} else {
currentHolder.adjustScroll((int) (header.getHeight() + ViewHelper.getTranslationY(header)));// 修正滚出去的偏移量
}
}

@Override
public void onPageScrollStateChanged(int state) {
tabs.onPageScrollStateChanged(state);
}

@Override
public void adjustScroll(int scrollHeight) {

}

private boolean reLocation = false;

private int headerScrollSize = 0;

public static final boolean NEED_RELAYOUT = Integer.valueOf(Build.VERSION.SDK).intValue() < Build.VERSION_CODES.HONEYCOMB;

private int headerTop = 0;

// 刷新头部显示时,没有onScroll回调,只有当刷新时会有
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount,
int pagePosition) {
if (viewPager.getCurrentItem() != pagePosition) {
return;
}
if (headerScrollSize == 0 && reLocation) {
reLocation = false;
return;
}
reLocation = false;
int scrollY = Math.max(-getScrollY(view), headerTranslationDis);
if (NEED_RELAYOUT) {
headerTop = scrollY;
header.post(new Runnable() {

@Override
public void run() {
Log.e("Main", "scorry1="+ headerTop);
header.layout(0, headerTop, header.getWidth(), headerTop + header.getHeight());
}
});
} else {
ViewHelper.setTranslationY(header, scrollY);
}
}

/**
* 主要算这玩意,PullToRefreshListView插入了一个刷新头部,因此要根据不同的情况计算当前的偏移量</br>
*
* 当刷新时: 刷新头部显示,因此偏移量要加上刷新头的数值 未刷新时: 偏移量不计算头部。
*
* firstVisiblePosition >1时,listview中的项开始显示,姑且认为每一项等高来计算偏移量(其实只要显示一个项,向上偏移
* 量已经大于头部的最大偏移量,因此不准确也没有关系)
*
* @param view
* @return
*/
public int getScrollY(AbsListView view) {
View c = view.getChildAt(0);
if (c == null) {
return 0;
}
int top = c.getTop();
int firstVisiblePosition = view.getFirstVisiblePosition();
if (firstVisiblePosition == 0) {
return -top + headerScrollSize;
} else if (firstVisiblePosition == 1) {
return -top;
} else {
return -top + (firstVisiblePosition - 2) * c.getHeight() + headerHeight;
}
}

// 与onHeadScroll互斥,不能同时执行
@Override
public void onHeaderScroll(boolean isRefreashing, int value, int pagePosition) {
if (viewPager.getCurrentItem() != pagePosition) {
return;
}
headerScrollSize = value;
if (NEED_RELAYOUT) {
header.post(new Runnable() {

@Override
public void run() {
Log.e("Main", "scorry="+ (-headerScrollSize));
header.layout(0, -headerScrollSize, header.getWidth(), -headerScrollSize + header.getHeight());
}
});
}else{
ViewHelper.setTranslationY(header, -value);
}
}

}


解释一下上面的代码,界面中后一层为Viewpager,里面加入了多个fragment,每个fragment里面占用一个Listivew,Listview中添加一个与悬浮头高度完全一样的header,这样可显示区域就为能看到的区域,之后监听Listview的onScorll,根据当前显示的listview的item的高度来控制前一层的悬浮的位置,这个地方要注意的时,每次切换tab时,要将当前已经偏移的位置通知到当前切换的tab,比如tab1,向上滑动,影藏了悬浮头,当从tab1切换到tab2时,这是tab2的位置要向上修正,修正距离为悬浮头滑出去的距离。其他的部分代码页比较简单,看看就可以了,其次开源控件PullToRefreshListView中我修改了当在刷新时偏移的距离,当改距离通知到界面,这样在下拉刷新时,将整个头部向下偏移,


ps:上面的代码中也看到了,我们针对了不同的版本采用了不同的动画,这是由于在3.0以前,位移动画看起来移动了位置,可是实际上控件还在初始位置,为此要针对不同的版本处理不同的动画,否则tab上的点击事件在2.X版本上还是在初始位置。上面的动画采用了nineold控件,也可以自己写,这个部分动画还是比较简单的。


上面的Viewpager中添加了Fragment,我们来看看Tab1ListFragment.java

 

public class Tab1ListFragment extends ScrollTabHolderFragment {

private PullToRefreshListView listView;

private View placeHolderView;

private ArrayAdapter<String> adapter;

private ArrayList<String> listItems;

private Handler handler;

public Tab1ListFragment() {
this.setFragmentId(PageAdapterTab.PAGE_TAB1.fragmentId);
}

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.page_tab_fragment_layout, container, false);
}

@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
findViews();
initListView();
}

@SuppressLint("InflateParams")
private void findViews() {
handler = new Handler(Looper.getMainLooper());
listView = (PullToRefreshListView) getView().findViewById(R.id.page_tab_listview);
}

private void initListView() {
setListViewListener();
listViewAddHeader();
listViewLoadData();
}

private void setListViewListener() {
listView.setOnRefreshListener(new OnRefreshListener2<ListView>() {

@Override
public void onPullDownToRefresh(PullToRefreshBase<ListView> refreshView) {
loadNews();
}

@Override
public void onPullUpToRefresh(PullToRefreshBase<ListView> refreshView) {
loadOlds();
}

});

listView.setOnScrollListener(new OnScrollListener() {

@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}

@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (scrollTabHolder != null) {
scrollTabHolder.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount, getFragmentId());
}
}
});
listView.setOnHeaderScrollListener(new OnHeaderScrollListener() {

@Override
public void onHeaderScroll(boolean isRefreashing, boolean istop, int value) {
if (scrollTabHolder != null && istop) {
scrollTabHolder.onHeaderScroll(isRefreashing, value, getFragmentId());
}
}
});
}

private void listViewAddHeader() {
placeHolderView = new LinearLayout(getActivity());
AbsListView.LayoutParams params = new LayoutParams(AbsListView.LayoutParams.MATCH_PARENT, getResources()
.getDimensionPixelSize(R.dimen.max_header_height));
placeHolderView.setLayoutParams(params);
listView.getRefreshableView().addHeaderView(placeHolderView);
}

protected void listViewLoadData() {
listItems = new ArrayList<String>();
for (int i = 1; i <= 50; i++) {
listItems.add("currnet page: " + (getFragmentId() + 1) + " item --" + i);
}
adapter = new ArrayAdapter<String>(getActivity(), R.layout.list_item, android.R.id.text1, listItems);
listView.setAdapter(adapter);
loadNews();
}

/**
* 下拉清空旧的数据
*/
private void loadNews() {
handler.postDelayed(new Runnable() {// 模拟远程获取数据

@Override
public void run() {
stopRefresh();
// listItems.clear();
// for (int i = 1; i <= 50; i++) {
// listItems.add("currnet page: " + (getFragmentId() +
// 1) + " item --" + i);
// }
// notifyAdpterdataChanged();
}
}, 300);
}

private void notifyAdpterdataChanged() {
if (adapter != null) {
adapter.notifyDataSetChanged();
}
}

protected void loadOlds() {
handler.postDelayed(new Runnable() {// 模拟远程获取数据

@Override
public void run() {
stopRefresh();
int size = listItems.size() + 1;
for (int i = size; i < size + 50; ++i) {
listItems.add("currnet page: " + (getFragmentId() + 1) + " item --" + i);
}
notifyAdpterdataChanged();
}
}, 300);
}

// PullToRefreshListView 自动添加了一个头部
@Override
public void adjustScroll(int scrollHeight) {
if (scrollHeight == 0 && listView.getRefreshableView().getFirstVisiblePosition() >= 2) {
return;
}
//Log.d(getTag(), "scrollHeight:" + scrollHeight);
listView.getRefreshableView().setSelectionFromTop(2, scrollHeight);
// Log.d(getTag(), "getScrollY:" + getScrollY(listView.getRefreshableView()));
// handler.postDelayed(new Runnable() {
//
// @Override
// public void run() {
// Log.d(getTag(), "getScrollY:" + getScrollY(listView.getRefreshableView()));
// }
// }, 5000);
}

public int getScrollY(AbsListView view) {
View c = view.getChildAt(0);
if (c == null) {
return 0;
}
int top = c.getTop();
int firstVisiblePosition = view.getFirstVisiblePosition();
if (firstVisiblePosition == 0) {
return -top;
} else if (firstVisiblePosition == 1) {
return top;
} else {
return -top + (firstVisiblePosition - 2) * c.getHeight() + 683;
}
}

protected void updateListView() {
if (adapter != null) {
adapter.notifyDataSetChanged();
}
}

protected void stopRefresh() {
listView.onRefreshComplete();
}

}

上面代码中的界面就是xml中包含了一个PullToRefreshListView,比较简单这个地方就不贴出来了,我们看到在listViewAddHeader中,这个地方添加了一个与悬浮头等高的头部,这样就可以将内容区域给呈现出来,不会被悬浮头遮挡,其次在list的listener中我们将onScorll传到了主界面,这样Listview滚动,就可以将当前滚动的距离计算出来,修正悬浮头的距离。


我们再贴出上面剩下的代码ScrollTabHolderFragment.java与ScrollTabHolder.java

 

public abstract class ScrollTabHolderFragment extends Fragment implements ScrollTabHolder {

private int fragmentId;

protected ScrollTabHolder scrollTabHolder;

public void setScrollTabHolder(ScrollTabHolder scrollTabHolder) {
this.scrollTabHolder = scrollTabHolder;
}

@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount,
int pagePosition) {
// nothing
}

@Override
public void onHeaderScroll(boolean isRefreashing, int value, int pagePosition) {

}

public int getFragmentId() {
return fragmentId;
}

public void setFragmentId(int fragmentId) {
this.fragmentId = fragmentId;
}
}


public interface ScrollTabHolder {

void adjustScroll(int scrollHeight);

void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount, int pagePosition);

void onHeaderScroll(boolean isRefreashing, int value, int pagePosition);
}

最后我们来看看adaper

 

public class SlidingPagerAdapter extends FragmentPagerAdapter {

protected final ScrollTabHolderFragment[] fragments;

protected final Context context;

private SparseArrayCompat<ScrollTabHolder> mScrollTabHolders;
private ScrollTabHolder mListener;

public int getCacheCount() {
return PageAdapterTab.values().length;
}

public SlidingPagerAdapter(FragmentManager fm, Context context, ViewPager pager) {
super(fm);
fragments = new ScrollTabHolderFragment[PageAdapterTab.values().length];
this.context = context;
mScrollTabHolders = new SparseArrayCompat<ScrollTabHolder>();
init(fm);
}

private void init(FragmentManager fm) {
for (PageAdapterTab tab : PageAdapterTab.values()) {
try {
ScrollTabHolderFragment fragment = null;

List<Fragment> fs = fm.getFragments();
if (fs != null) {
for (Fragment f : fs) {
if (f.getClass() == tab.clazz) {
fragment = (ScrollTabHolderFragment) f;
break;
}
}
}

if (fragment == null) {
fragment = (ScrollTabHolderFragment) tab.clazz.newInstance();
}

fragments[tab.tabIndex] = fragment;
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}

public void setTabHolderScrollingListener(ScrollTabHolder listener) {
mListener = listener;
}

@Override
public ScrollTabHolderFragment getItem(int pos) {
ScrollTabHolderFragment fragment = fragments[pos];
mScrollTabHolders.put(pos, fragment);
if (mListener != null) {
fragment.setScrollTabHolder(mListener);
}
return fragment;
}

public SparseArrayCompat<ScrollTabHolder> getScrollTabHolders() {
return mScrollTabHolders;
}

@Override
public int getCount() {
return PageAdapterTab.values().length;
}

@Override
public CharSequence getPageTitle(int position) {
PageAdapterTab tab = PageAdapterTab.fromTabIndex(position);
int resId = tab != null ? tab.resId : 0;
return resId != 0 ? context.getText(resId) : "";
}

}


SlidingPagerAdapter 继承自FragmentPagerAdapter,从主界面传递了一个callback,将在callback传递给每个fragment,这样就将fragment与activity联系起来了。其实还有很多种方式,比如在fragment的attach中获取activity中的回调。上面代码中还有一个PageAdapterTab,它又是干什么的呐?来看看代码

 

public enum PageAdapterTab {
PAGE_TAB1(0, Tab1ListFragment.class, R.string.page_tab1),

PAGE_TAB2(1, Tab2ListFragment.class, R.string.page_tab2),

PAGE_TAB3(2, Tab3ListFragment.class, R.string.page_tab3),
;

public final int tabIndex;

public final Class<? extends Fragment> clazz;

public final int resId;

public final int fragmentId;

private PageAdapterTab(int index, Class<? extends Fragment> clazz, int resId) {
this.tabIndex = index;
this.clazz = clazz;
this.resId = resId;
this.fragmentId = index;
}

public static final PageAdapterTab fromTabIndex(int tabIndex) {
for (PageAdapterTab value : PageAdapterTab.values()) {
if (value.tabIndex == tabIndex) {
return value;
}
}

return null;
}
}


就是一个枚举类,配置了当前要显示的fragment,这样以后就要增加就可以只修改改枚举就ok了


到此整个工程就结束了,我们截几张图看看效果:

多TAB可悬浮头控件 - Ivy - Tvy 多TAB可悬浮头控件 - Ivy - Tvy 多TAB可悬浮头控件 - Ivy - Tvy
  

最后在回顾一下,布局为两层,厚一层为一个Viewpager,里面包含了多个fragment,前一层为一个悬浮头与切换tab,当滑动listview时将当前显示的位置传递到主界面,同时更改主界面的位置。


良心的公众号,更多精品文章,不要忘记关注哈

《Android和Java技术栈》


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值