PullZoomView是个不错的伸缩效果头部的控件,可以使用ListView、ScrollView和RecyclerView做出头部伸缩效果,但是不支持RecyclerView瀑布流布局,自己改轮子兼容.
感谢Frank-Zhu的贡献,github地址
控件的使用很简单,两个自定义属性,headerView为头部view,zoomView为后面那种有缩放的图片
<com.ecloud.pulltozoomview.demo.PullToZoomRecyclerViewEx
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
custom:headerView="@layout/profile_head_view"
custom:zoomView="@layout/list_head_zoom_view" />
这是demo中使用GridLayoutManager配置的RecyclerView,效果如图
final GridLayoutManager manager = new GridLayoutManager(this, 2);
manager.setOrientation(GridLayoutManager.VERTICAL);
manager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
return mAdapter.getItemViewType(position) == RecyclerViewHeaderAdapter.INT_TYPE_HEADER ? 2 : 1;
}
});
listView.setAdapterAndLayoutManager(mAdapter, manager);
当你想换成StaggeredGridLayoutManager做瀑布流的时候,就留坑了,因为压根不支持,PullToZoomRecyclerViewEx的setAdapterAndLayoutManager参数只支持GridLayoutManager
public void setAdapterAndLayoutManager(RecyclerView.Adapter adapter, GridLayoutManager mLayoutManager) {
mRootView.setLayoutManager(mLayoutManager);
mRootView.setAdapter(adapter);
updateHeaderView();
}
好吧,既然写不出轮子就改造轮子吧,添加一个支持StaggeredGridLayoutManager方法看看咋样?
// PullToZoomRecyclerViewEx添加方法
public void setAdapterAndLayoutManager(RecyclerView.Adapter adapter, StaggeredGridLayoutManager mLayoutManager) {
mRootView.setLayoutManager(mLayoutManager);
mRootView.setAdapter(adapter);
updateHeaderView();
}
// PullToZoomRecyclerActivity改StaggeredGridLayoutManager
manager = new StaggeredGridLayoutManager(2,
StaggeredGridLayoutManager.VERTICAL);
listView.setAdapterAndLayoutManager(mAdapter, manager);
然后到作者封装的RecyclerViewHeaderAdapter的onBindViewHolder处理头部占据两格的操作,然后看到作者try catch住了设置瀑布流头部的代码,这里已经知道作者尝试过适配瀑布流然后没适配成,try catch用来调试的,既然这样还得看看效果是咋样的?再来改进
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (position >= headers.size() && (position - headers.size()) < getCount()) {
//noinspection unchecked
onBindView((V) holder, position - headers.size());
} else {
try {
final StaggeredGridLayoutManager.LayoutParams lp = (StaggeredGridLayoutManager.LayoutParams) holder.itemView.getLayoutParams();
lp.setFullSpan(true);
holder.itemView.setLayoutParams(lp);
} catch (Exception e) {
e.printStackTrace();
}
}
}
还需要设置头部可见适配StaggeredGridLayoutManager的情况
private boolean isFirstItemVisible() {
if (mRootView != null) {
final RecyclerView.Adapter adapter = mRootView.getAdapter();
RecyclerView.LayoutManager mLayoutmanager = null;
if (mRootView.getLayoutManager() instanceof StaggeredGridLayoutManager) {
mLayoutmanager = (StaggeredGridLayoutManager) mRootView.getLayoutManager();
} else if (mRootView.getLayoutManager() instanceof GridLayoutManager) {
mLayoutmanager = (GridLayoutManager) mRootView.getLayoutManager();
}
if (null == adapter || adapter.getItemCount() == 0) {
return true;
} else {
int[] into = {0, 0};
if (mLayoutmanager == null) return false;
// 这里需要加上StaggeredGridLayoutManager的情况来判断头部是否可见
if (mLayoutmanager instanceof
StaggeredGridLayoutManager) {
((StaggeredGridLayoutManager)
mLayoutmanager).findFirstVisibleItemPositions(into);
} else if (mLayoutmanager instanceof
GridLayoutManager) {
into[0] = ((GridLayoutManager)
mLayoutmanager).findFirstVisibleItemPosition();
}
if (into.length > 0 && into.length > 0 &&
into[0] <= 1) {
final View firstVisibleChild =
mRootView.getChildAt(0);
if (firstVisibleChild != null) {
return firstVisibleChild.getTop() >=
mRootView.getTop();
}
}
}
}
return false;
}
可以看到设置头部的操作压根不起作用,而LogCat中输出捕获到的log:
java.lang.ClassCastException: android.widget.AbsListView$LayoutParams cannot be cast to
android.support.v7.widget.StaggeredGridLayoutManager$LayoutParams
可以知道头部的布局属性不是StaggeredGridLayoutManager.LayoutParams,也就说明了其Parent不是RecyclerView,是不是很疑惑呢?明明是设置在RecyclerView显示的,咋拿不到瀑布流布局属性呢?
原因就在RecyclerViewHeaderAdapter的onCreateViewHolder这里,先说明
item是作者将布局类型和ViewHolder封装起来的类
public static class ExtraItem<V extends RecyclerView.ViewHolder> {
public final int type;
public final V view;
public ExtraItem(int type, V view) {
this.type = type;
this.view = view;
}
}
PullToZoomRecyclerViewEx继承自PullToZoomBase这个类,PullToZoomBase继承自线性布局实现IPullToZoom接口,它在初始化的时候会拿自定义属性,如果配置了头部和伸缩布局的话,它会填充布局到protected修饰符的mZoomView 和mHeaderView和调用IPullToZoom的handleStyledAttributes(TypedArray a)方法供PullToZoomRecyclerViewEx去将这两个布局添加到mHeaderContainer的帧布局中,
最后将mRootView添加到线性布局中,mRootView是泛型,PullToZoomRecyclerViewEx继承PullToZoomBase的时候设置的就是recyclerview,也就是将RecyclerView放到此线性布局中
if (attrs != null) {
LayoutInflater mLayoutInflater = LayoutInflater.from(getContext());
//初始化状态View
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.PullToZoomView);
int zoomViewResId = a.getResourceId(R.styleable.PullToZoomView_zoomView, 0);
if (zoomViewResId > 0) {
mZoomView = mLayoutInflater.inflate(zoomViewResId, null, false);
}
int headerViewResId = a.getResourceId(R.styleable.PullToZoomView_headerView, 0);
if (headerViewResId > 0) {
mHeaderView = mLayoutInflater.inflate(headerViewResId, null, false);
}
isParallax = a.getBoolean(R.styleable.PullToZoomView_isHeaderParallax, true);
// Let the derivative classes have a go at handling attributes, then
// recycle them...
handleStyledAttributes(a);
a.recycle();
}
addView(mRootView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
PullToZoomRecyclerViewEx回调handleStyledAttributes方法为
mHeaderContainer添加布局,ExtraItem在这里就用上了,
new RecyclerView.ViewHolder(mHeaderContainer)说明这里直接创建了一个ViewHolder,创建后使用mAdapter.addHeaderView(mExtraItem)刷新RecyclerView的item
@Override
public void handleStyledAttributes(TypedArray a) {
mHeaderContainer = new FrameLayout(getContext());
if (mZoomView != null) {
mHeaderContainer.addView(mZoomView);
}
if (mHeaderView != null) {
mHeaderContainer.addView(mHeaderView);
}
RecyclerViewHeaderAdapter<RecyclerView.ViewHolder> mAdapter = (RecyclerViewHeaderAdapter<RecyclerView.ViewHolder>) mRootView.getAdapter();
if (mAdapter != null) {
RecyclerViewHeaderAdapter.ExtraItem mExtraItem = new RecyclerViewHeaderAdapter.ExtraItem(RecyclerViewHeaderAdapter.INT_TYPE_HEADER, new RecyclerView.ViewHolder(mHeaderContainer) {
@Override
public String toString() {
return super.toString();
}
});
mAdapter.addHeaderView(mExtraItem);
}
}
知道以上步骤后,再来看看onCreateViewHolder,这里直接遍历headers根据类别直接返回之前ExtraItem创建的viewholder了,
然后不是INT_TYPE_HEADER和INT_TYPE_FOOTER的就属于普通的item,调用onCreateContentView给用户自己处理,这里有看出端倪了吗?没看出?对比下我们规范的onCreateViewHolder里的做法
这里的做法:
@Override
public final V onCreateViewHolder(ViewGroup parent, int viewType) {
for (ExtraItem<V> item : headers)
if (viewType == item.type)
return item.view;
for (ExtraItem<V> item : footers)
if (viewType == item.type)
return item.view;
return onCreateContentView(parent, viewType);
}
对比可知,这里的onCreateViewHolder在返回ExtraItem的ViewHolder时候没有用到Parent,因为在PullToZoomBase初始化的时候提前填充完view了,Parent为null,所以RecyclerView压根就不是它们的parent,所以拿到的布局属性就不会是瀑布流布局属性了,所以异常捕获是合情合理的。
PullToZoomBase的init方法里面过早填充了两个view,
private void init(Context context, AttributeSet attrs) {
...
mHeaderView = mLayoutInflater.inflate(headerViewResId, null, false);
...
mZoomView = mLayoutInflater.inflate(zoomViewResId, null, false);
...
}
知道了是inflate的时候参数parent导致的原因后,一切就好办了,在RecyclerViewHeaderAdapter的onCreateViewHolder,头部类型的照样调用onCreateContentView(parent, item.type, item.view.itemView)交给我们定义的RecyclerAdapterCustom去做填充,onCreateContentView添加一个type参数用于头部和普通item的处理
@Override
public final V onCreateViewHolder(ViewGroup parent, int viewType) {
/*for (ExtraItem<V> item : headers)
if (viewType == item.type)
return item.view;*/
for (ExtraItem<V> item : footers)
if (viewType == item.type)
return item.view;
for (ExtraItem<V> item : headers)
if (viewType == item.type)
// 这里返回item.view.itemView去给RecyclerAdapterCustom去处理
return onCreateContentView(parent, item.type, item.view.itemView);
return onCreateContentView(parent, viewType, null);
}
然后在RecyclerAdapterCustom的onCreateContentView,用header_container的布局包住mHeaderContainer,这样子mHeaderContainer的parent的布局属性就是瀑布流布局属性了,PS:之前的mtextview是直接创建的,也是Parent不为RecyclerView,这里换成item_text布局做。
onBindView这里设置mtextview高度不等看下瀑布流的效果
@Override
public ViewHolderRecyclerPullToZoom onCreateContentView(ViewGroup parent, int viewType, View itemView) {
ViewHolderRecyclerPullToZoom viewHolderRecyclerPullToZoom = null;
if (viewType != INT_TYPE_HEADER) {
// 内容布局
viewHolderRecyclerPullToZoom = new ViewHolderRecyclerPullToZoom(LayoutInflater.from(PullToZoomRecyclerActivity.this).inflate(R.layout.item_text, parent, false));
} else {
// 头部,这里给PullToZoomRecyclerViewEx的mHeaderContainer加一层布局,目的是让他作为recycleview的孩子,这样子才能调RecyclerViewHeaderAdapter的onBindViewHolder的else那一部分代码设置占一行
ViewGroup view = (ViewGroup) LayoutInflater.from(PullToZoomRecyclerActivity.this).inflate(R.layout.header_container, parent, false);
view.addView(itemView);
viewHolderRecyclerPullToZoom = new ViewHolderRecyclerPullToZoom(view);
}
return viewHolderRecyclerPullToZoom;//new ViewHolderRecyclerPullToZoom(new TextView(getContext()));
}
@Override
public void onBindView(ViewHolderRecyclerPullToZoom view, int position) {
Log.e("偶滴神", "位置:" + position + " " + view.mtextview + " " + view.mtextview.getLayoutParams() + " " + ((ViewGroup) view.mtextview.getParent()).getLayoutParams());
view.mtextview.setText(adapterData[position]);
view.mtextview.setHeight((int) (50 + Math.random() * 100));
}
这时再来看下效果,已经可以了,但是细心的小伙伴可以发现,往上推的时候没有上面那种图片随着缓慢往下移动的效果了,咋办呢?继续扒代码!
、
在PullToZoomRecyclerViewEx中,mRootView即RecyclerView设置了滚动的监听,
f 为头部的高度减去mHeaderContainer的bottom得到的值,也就是头部的底部距recyclerview顶部的距离,
然后当f大于0小于头部高度的时候,i 为据顶部距离的0.65值,mHeaderContainer.scrollTo(0, -i)即mHeaderContainer里面的内容即mHeaderView和mZoomView往Y轴正方向即往下移动,这样就形成了Recyclerview往上推的时候头部缓慢往下移动的效果。
这里就可以进行分析了,mHeaderContainer.getBottom()得到的是相对父布局的顶部的距离,现在我们在之前的onCreateContentView那里为mHeaderContainer包了层布局,所以Recyclerview往上推的时候mHeaderContainer.getBottom()是相对于父布局而不是对于RecyclerView来说,所以肯定为mHeaderHeight ,所以f 肯定等于0,对于下面的判断就无意义了,所以没有移动的效果。
public PullToZoomRecyclerViewEx(Context context, AttributeSet attrs) {
...
mRootView.setOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (mZoomView != null && !isHideHeader() &&
isPullToZoomEnabled()) {
float f = mHeaderHeight -
mHeaderContainer.getBottom();
Log.d(TAG, "onScroll --> f = " + f);
if (isParallax()) {
if ((f > 0.0F) && (f < mHeaderHeight)) {
int i = (int) (0.65D * f);
mHeaderContainer.scrollTo(0, -i);
} else if (mHeaderContainer.getScrollY()
!= 0) {
mHeaderContainer.scrollTo(0, 0);
}
}
}
}
...
});
...
}
改进相当简单,将mHeaderContainer换为mHeaderContainer.getParent()即可
public PullToZoomRecyclerViewEx(Context context, AttributeSet attrs) {
mRootView.setOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
ViewGroup viewGroup = null;
viewGroup = mHeaderContainer.getParent() != null ? (ViewGroup)
mHeaderContainer.getParent() : mHeaderContainer;
if (mZoomView != null && !isHideHeader()
&& isPullToZoomEnabled()) {
float f = mHeaderHeight - viewGroup.getBottom();
Log.d(TAG, "onScroll --> f = " + f);
if (isParallax()) {
if ((f > 0.0F) && (f < mHeaderHeight)) {
int i = (int) (0.65D * f);
viewGroup.scrollTo(0, -i);
} else if (viewGroup.getScrollY() != 0) {
viewGroup.scrollTo(0, 0);
}
}
}
}
...
});
...
}
最后我们来看下效果,已经完美实现了,至于之前的GridLayoutManager的兼容处理就不弄了,也就是简单的判断处理
最后提供改进后的代码下载试试吧,讲得不好请见谅,自己跑一下看下源码就很好懂了。
下载地址:PullToZoomView兼容RecyclerView瀑布流