RecycleView实现画廊效果
先说项目ui需求图:
- 滑动到第一项的时候,不可再向左滑动,并且左侧显示:“左侧没有内容”。
- 滑动到最后一项的时候,不可再向右滑动,并且右侧显示:“右侧没有内容”。
- 用户在开始滑动的时候,“左侧没有内容”或是“右侧没有内容”的view要立即消失。
- 当前item需要有放大效果,并且有边框。
- 不是当前的item需要有缩小效果,无边框。
如下图所展示:
需求实现:
难点如下:
1. 怎么用RecycelView实现这种画廊效果。
2. 选中item的边框怎么处理。
3. 最左侧和最右侧的无内容view怎么展示和隐藏的问题。
先说第一个难点怎么解决:
我曾经试图尝试过用viewpager做,但是效果不理想,然后在github上查找到了一款非常符合我们项目需求的控件:
DiscreteScrollView,这个控件基本上满足了我们的需求,只需要细节调整即可。文档中有描述怎么使用,参照导入就好。
第二个难点:选中item的边框处理:
在Activity中实现 DiscreteScrollView.OnItemChangedListener,在这个方法中,可以拿到当前、前一个、后一个RecyclerView.ViewHolder来做背景边框特殊处理。
当然,我这种方法可能不是最好的,因为activit中展示的时候,只能看到3个item,如果需求设计要看到5个item,处理起来就更麻烦一点了。
@Override
public void onCurrentItemChanged(@Nullable RecyclerView.ViewHolder viewHolder, int adapterPosition) {
Timber.d("onCurrentItemChanged %d", adapterPosition);
StepAdapter.ViewHolder holder = (StepAdapter.ViewHolder) viewHolder;//当前选中的
holder.rlLayout.setBackgroundResource(R.drawable.bg_step_shape);
if (adapterPosition == 0) {
StepAdapter.ViewHolder viewHolder0 = (StepAdapter.ViewHolder) mDsvStep.getViewHolder(adapterPosition + 1);
viewHolder0.rlLayout.setBackgroundResource(0);
} else if (adapterPosition > 0 && adapterPosition < mStepBeanList.size() - 1) {//还没有滑动到最后:
StepAdapter.ViewHolder viewHolder1 = (StepAdapter.ViewHolder) mDsvStep.getViewHolder(adapterPosition - 1);
viewHolder1.rlLayout.setBackgroundResource(0);
StepAdapter.ViewHolder viewHolder2 = (StepAdapter.ViewHolder) mDsvStep.getViewHolder(adapterPosition + 1);
viewHolder2.rlLayout.setBackgroundResource(0);
} else {
StepAdapter.ViewHolder viewHolder3 = (StepAdapter.ViewHolder) mDsvStep.getViewHolder(adapterPosition - 1);
viewHolder3.rlLayout.setBackgroundResource(0);
}
onItemChanged(adapterPosition);
showLeftRightView(adapterPosition);
}
这个边框的问题就基本处理完毕了,但是还有一个特殊的地方:我们的app是一款可以语音控制的产品,比如说当前在第一步,用户可以说到第十步。那就是从第一步直接跳转到第十步,调用DiscreteScrollView的scrollToPosition(position)方法可以跳转到第十步,但是第十步却没有边框,因为这个方法不会回调onCurrentItemChanged,所以需要我们对这种情况也要做下边框特殊处理。
我开始的方法是通过position,用DiscreteScrollView拿到当前的viewholder, 仿照onCurrentItemChanged中的方法来做,但是结果是不行的,因为通过这个拿到的viewholder有时候是空的。这里涉及到了viewholder的缓存机制问题:用户在第一步,能看到的只有第二项,第十项根本在屏幕之外,所以这个时候去拿第十项的viewholder肯定是空的,那么如何处理呢?
有个小技巧:如果去拿viewholer是空的,那么说明这个viewholder还不在缓存之列,但是肯定会走Adapter中的onBindViewHolder方法,因此,可以在这里配合adapter做处理,总结就是如下:
//这个是在activity中:
/**
* scrollveiw 滑动到对应的页面并播报处理
*
* @param position:角标从0开始算起,
*/
private void onItemVoiceChanged(int position) {
showLeftRightView(position);
Timber.d("onItemVoiceChanged %d", position);
mTvStep.setText((position + 1) + " / " + mStepBeanList.size());
String text = mStepBeanList.get(position).getText();
mTvContent.setText(text);
mDsvStep.scrollToPosition(position);
TtsVoiceManager.getInstance().ttsVoice(text);
//设置背景颜色
RecyclerView.ViewHolder viewHolder = mDsvStep.getViewHolder(position);
StepAdapter.ViewHolder holder = (StepAdapter.ViewHolder) viewHolder;//当前选中的
if (holder != null) {
holder.rlLayout.setBackgroundResource(R.drawable.bg_step_shape);
}else{
Timber.d("holder 为空");
//通知:adapterbindholer 设置背景为方框
mStepAdapter.setChangeBackround(true);
}
if (position == 0) {
StepAdapter.ViewHolder viewHolder0 = (StepAdapter.ViewHolder) mDsvStep.getViewHolder(position + 1);
if (viewHolder0 != null) {
viewHolder0.rlLayout.setBackgroundResource(0);
}else{
mStepAdapter.setChangeBackround(false);
}
} else if (position > 0 && position < mStepBeanList.size() - 1) {//还没有滑动到最后:
StepAdapter.ViewHolder viewHolder1 = (StepAdapter.ViewHolder) mDsvStep.getViewHolder(position - 1);
if (viewHolder1 != null) {
viewHolder1.rlLayout.setBackgroundResource(0);
}else{
mStepAdapter.setChangeBackround(false);
}
StepAdapter.ViewHolder viewHolder2 = (StepAdapter.ViewHolder) mDsvStep.getViewHolder(position + 1);
if (viewHolder2 != null) {
viewHolder2.rlLayout.setBackgroundResource(0);
}else{
mStepAdapter.setChangeBackround(false);
}
} else {
StepAdapter.ViewHolder viewHolder3 = (StepAdapter.ViewHolder) mDsvStep.getViewHolder(position - 1);
if (viewHolder3 != null) {
viewHolder3.rlLayout.setBackgroundResource(0);
}else{
mStepAdapter.setChangeBackround(false);
}
}
}
//这个是在adapter中
@Override
public void onBindViewHolder(@NonNull StepAdapter.ViewHolder holder, int position) {
Timber.d("position%d",position);
RecipeBean.StepsBean stepsBean = data.get(position);
ImageLoaderUtil.getInstance().loadImage(holder.ivImage,R.drawable.list_no_image_1,stepsBean.getPic());
if(isShowBackground){
holder.rlLayout.setBackgroundResource(R.drawable.bg_step_shape);
}else{
holder.rlLayout.setBackgroundResource(0);
}
holder.tvIndex.setText((position+1)+"");
}
通过以上方法,就可以做到手势滑动和语音滑动一模一样的效果了。
最后一个难题:最左侧和最右侧的无内容view怎么展示和隐藏
开始思路是将无内容的view添加到adapter中处理,结果不行,因为要处理DiscreteScrollView的滑动问题,比较复杂了。最后想出在activity布局中增加这两个布局,和DiscreteScrollView重叠在一起,如下:
<com.yarolegovich.discretescrollview.DiscreteScrollView
android:id="@+id/dsv_step"
android:layout_width="match_parent"
android:layout_height="560dp"
android:layout_below="@id/tv_step"
app:dsv_orientation="horizontal"
android:layout_marginTop="10dp"
/>
.........
.........
.........
<RelativeLayout
android:id="@+id/rl_left"
android:layout_width="400dp"
android:layout_height="560dp"
android:layout_below="@id/tv_step"
android:layout_marginTop="10dp"
android:layout_marginRight="148dp"
>
<ImageView
android:id="@+id/iv_left"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/steps_cor"
android:layout_centerVertical="true"
android:layout_alignParentRight="true"
/>
<TextView
android:id="@+id/tv_left"
android:layout_width="25dp"
android:layout_height="match_parent"
android:text="左侧没有内容啦"
android:gravity="center"
android:textSize="24sp"
android:textColor="#B7B7B7"
android:layout_centerVertical="true"
android:layout_toLeftOf="@id/iv_left"
android:layout_marginRight="90dp"
/>
</RelativeLayout>
剩下的就是监听DiscreteScrollView的滑动和空白view的展示和隐藏问题。幸运的是DiscreteScrollView提供了多个监听滑动的方法:在acitivity中实现DiscreteScrollView.ScrollStateChangeListener
@Override
public void onScrollStart(@NonNull RecyclerView.ViewHolder currentItemHolder, int adapterPosition) {
Timber.d("onScrollStart %d", adapterPosition);
if (adapterPosition == 0) {//如果是0的位置就开始滑动:那么左侧的图片不要显示
mRlLeft.setVisibility(View.GONE);
} else if (adapterPosition == mStepBeanList.size() - 1) {//从最后一个开始滑动
mRlRight.setVisibility(View.GONE);
}
}
@Override
public void onScrollEnd(@NonNull RecyclerView.ViewHolder currentItemHolder, int adapterPosition) {
Timber.d("onScrollEnd %d", adapterPosition);
}
@Override
public void onScroll(float scrollPosition, int currentPosition, int newPosition, @Nullable RecyclerView.ViewHolder currentHolder, @Nullable RecyclerView.ViewHolder newCurrent) {
Log.d("onScroll", "scrollPosition:" + scrollPosition + "currentPosition:" + currentPosition +
"newPosition:" + newPosition);
}
另外,需要注意的是:onScrollEnd之后,会回调onCurrentItemChanged的方法,所以这里 还需要根据adapterPosition最终停留的位置做左侧和右侧空白view的展示处理,以便能有更好的体验效果:
@Override
public void onCurrentItemChanged(@Nullable RecyclerView.ViewHolder viewHolder, int adapterPosition) {
............
............
............
showLeftRightView(adapterPosition);//控制左侧/右侧空白view的展示:
}
/**
* 左右空布局的展示
* adapterPosition
*/
public void showLeftRightView(int adapterPosition) {
if (adapterPosition == 0) {
mRlLeft.setVisibility(View.VISIBLE);
mRlRight.setVisibility(View.GONE);
} else if (adapterPosition == mStepBeanList.size() - 1) {
mRlLeft.setVisibility(View.GONE);
mRlRight.setVisibility(View.VISIBLE);
} else {
mRlLeft.setVisibility(View.GONE);
mRlRight.setVisibility(View.GONE);
}
}
通过DiscreteScrollView,配合一些监听和技巧以及细节特殊化处理,就能完美实现ui效果图了。
真实效果图如下: