今天在准备新项目的界面,偶然翻到了QQ附近的人那个筛选功能,嗯,觉得效果还不错,效果大概是这样子的。QQ的原图我就不上了,我就上我做的效果图。
觉得so easy是吧,但是我整整做了4个多小时,个多小时,多小时,小时,时。。。。
唉,苦话不多说,先分析一下界面。
主要功能分析
1.一个带选择框的recyclerView
2.根据位置不同透明度以及大小的item
3.停止滑动的时候会将中点对应的item适应到选择框内。
4.非手势滑动同样需要改变item的透明度及大小
界面分析
这个就没啥好说了,一个RecyclerView而已,如果有人不知道什么是RecyclerView,那么没关系,你只要知道这是android官方推出的listview的升级版就够了。什么?你不知道什么是listview?那你还是先去了解完android基础后再来看看吧。。
功能实现
看一眼布局还有MainActivity
MainActivity
public class MainActivity extends AppCompatActivity
{
FocusRecyclerView mRecyclerView;
private List<String> mDatas = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mDatas = new ArrayList<>();
for (int i = 0; i < 30; i++)
{
mDatas.add("嘻嘻:" + i);
}
mRecyclerView = (FocusRecyclerView) findViewById(R.id.recyclerView);
mRecyclerView.setAdapter(new RecyclerView.Adapter<MyViewHolder>()
{
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
{
View view = LayoutInflater.from(MainActivity.this).inflate(R.layout
.recyclerview_item, parent, false);
MyViewHolder viewHolder = new MyViewHolder(view);
return viewHolder;
}
@Override
public void onBindViewHolder(MyViewHolder holder, int position)
{
holder.textView.setText(mDatas.get(position));
}
@Override
public int getItemCount()
{
return mDatas.size();
}
});
mRecyclerView.setLayoutManager(new SnappingLinearLayoutManager(this, LinearLayoutManager.VERTICAL,
false));
}
class MyViewHolder extends RecyclerView.ViewHolder
{
private TextView textView;
public MyViewHolder(View itemView)
{
super(itemView);
textView = (TextView) itemView.findViewById(R.id.tv);
}
}
}
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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.chenantao.main.MainActivity">
<com.chenantao.main.FocusRecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</RelativeLayout>
最后再看一下item的布局
recyclerview_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/tv"
android:text="嘻嘻"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="25sp"/>
</LinearLayout>
这个虽然我外面包了LinearLayout,但其实用Merge包着更好。
自定义RecyclerView
重头戏来了。今天下午刚看到这个效果的时候,网上搜了一下,没搜到啥,不过看到有个哥们用ScrollView实现了类似的效果,不过ScrollView没有复用机制,不适合加载比较多的数据,那我们还是用RecyclerView来实现吧。先看一下需要的变量以及在构造方法中初始化它们。
public static final int DISPLAY_ITEM_COUNT = 5;//只能同时展示5条数据,注意,只能展示奇数条数据
private int mMaxTextSize = 30, mMinTextSize = 10;//sp,textview字体大小最大以及最小限制
private double mMaxTextAlpha = 0.9, mMinTextAlpha = 0.4;//textview透明度最大以及最小限制
private Paint mSelectedBorderPaint;//绘制选择框的paint
private SnappingLinearLayoutManager mLayoutManager;//自定义的布局管理器,可以缓慢滑动到指定位置
private double mTextSizeScale, mTextAlphaScale;//字体大小以及透明度的梯度值
public FocusRecyclerView(Context context, AttributeSet attrs)
{
super(context, attrs);
//将dp转换为px
mMaxTextSize = (int) DimensionUtils.sp2px(mMaxTextSize, getResources().getDisplayMetrics
());
mMinTextSize = (int) DimensionUtils.sp2px(mMinTextSize, getResources().getDisplayMetrics
());
mSelectedBorderPaint = new Paint();
mSelectedBorderPaint.setColor(0x6600BFFF);
mSelectedBorderPaint.setAntiAlias(true);
mSelectedBorderPaint.setStyle(Paint.Style.STROKE);
mSelectedBorderPaint.setStrokeWidth(3);
}
为了提高灵活性,我们把控制透明度以及字体大小的变量提取出来,同样你也可以抽取出自定义属性供外部设置,这里我就不弄了。还有,关于梯度值如果有童鞋不知道的,你看完下面的代码就会知道了。
绘制选择框
细心的童鞋估计已经发现了,这选择框是固定在中间的,同时它的大小跟每一项item的高度是一样的。那么绘制这种事肯定是放在OnDraw()方法里面完成了,很简单,就是绘制个矩形而已,当然高度需要根据item的高度动态计算。
/**
* 画出选择框
*
* @param c
*/
@Override
public void onDraw(Canvas c)
{
super.onDraw(c);
int width = getMeasuredWidth();
int height = getMeasuredHeight();
int itemHeight = height / DISPLAY_ITEM_COUNT;
int t = (height - itemHeight) / 2;
Rect rect = new Rect(0, t, width, t + itemHeight);
c.drawRect(rect, mSelectedBorderPaint);
}
根据屏幕的高度以及需要展示的item的数量,就能得出每个item的高度了,然后根据这个高度绘制矩形就够了,这个太简单。
滑动到中点对应的item
嗯,就是这个比较难了。首先,你需要得到在
滑动停止的时候得到
中点对应的view。然后,你要得到这个view在RecyclerView中的
位置。最后,根据这个位置缓慢滑动到对应的位置,当然,这个位置肯定不是中点view的位置,RecyclerView并没有提供滑动到中点的方法。要滑动到的目标位置是中点的位置centerPos-(DISPLAY_ITEM_COUNT-1)/2,因为我把展示的item数量定义为奇数,所以这些条目在屏幕中其实是对称的,那么这样计算就能拿要滑动的位置了。
说起来太复杂,看代码吧。
/**
* 在滑动停止的时候设置选中的条目
*
* @param state
*/
@Override
public void onScrollStateChanged(int state)
{
super.onScrollStateChanged(state);
if (state == SCROLL_STATE_IDLE)
{
scrollToSelectedView();
}
}
/**
* 滑动到目前选择框选中的item
*/
private void scrollToSelectedView()
{
//取得中点对应的item,类似于listview的pointToPosition
View selectView = findChildViewUnder(getMeasuredWidth() / 2, getMeasuredHeight
() / 2);
int pos = getChildAdapterPosition(selectView);
mLayoutManager.smoothScrollToPosition(this, null, pos - ((DISPLAY_ITEM_COUNT - 1) / 2));
}
看起来挺简单是吧,但是实际上你这样做的时候不行,为什么呢?你如果做过实际测试你会发现,有时滑动的时候,第一个view的顶部并不会对齐parent的顶部,这是非常严重的问题,这会导致中点的item不能很好的装进选择框里面。那么有什么改变方法呢?还好,RecyclerView的布局管理器提供了一个非常方便的方法:scrollToPositionWithOffset(pos,offset)。这是个神马方法呢,这个方法提供两个参数,一个是滑动到的位置,一个是偏移量。什么意思呢,就是如果我指定一个我要滑动到的目标位置,同时指定偏移量为0,那么这个目标位置的view的顶部便会紧紧的对齐parent的顶部。修改后的代码是下面这样。
/**
* 滑动到目前选择框选中的item
*/
private void scrollToSelectedView()
{
//取得中点对应的item,类似于listview的pointToPosition
View selectView = findChildViewUnder(getMeasuredWidth() / 2, getMeasuredHeight
() / 2);
int pos = getChildAdapterPosition(selectView);
// mLayoutManager.smoothScrollToPosition(this, null, pos - ((DISPLAY_ITEM_COUNT - 1) / 2));
mLayoutManager.scrollToPositionWithOffset(pos - ((DISPLAY_ITEM_COUNT - 1) / 2),0);
}
到这里,你可以试试,差不多就能达到效果了,这里我就不放效果图了。但是,有一个问题。这尼玛这瞬间滑动是什么鬼?这一点也不优雅,太突兀了吧,那么是否有提供类似listview的smoothScrollTo()的方法呢?木有,真的木有。。那么怎么办呢?我查了好多资料,终于在StackOverFlow发现了一位大婶的解决方法,解决方法就是自定义布局管理器,在里面可以随意的改变它的滑动速度,代码如下
public class SnappingLinearLayoutManager extends LinearLayoutManager
{
private static final float MILLISECONDS_PER_INCH = 500f;
private Context mContext;
public SnappingLinearLayoutManager(Context context, int orientation, boolean reverseLayout)
{
super(context, orientation, reverseLayout);
}
@Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
int position)
{
RecyclerView.SmoothScroller smoothScroller = new TopSnappedSmoothScroller(recyclerView
.getContext())
{
//This controls the direction in which smoothScroll looks for your view
@Override
public PointF computeScrollVectorForPosition(int targetPosition)
{
return new PointF(0, 1);
}
//This returns the milliseconds it takes to scroll one pixel.
@Override
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics)
{
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
}
};
smoothScroller.setTargetPosition(position);
startSmoothScroll(smoothScroller);
}
private class TopSnappedSmoothScroller extends LinearSmoothScroller
{
public TopSnappedSmoothScroller(Context context)
{
super(context);
}
@Override
public PointF computeScrollVectorForPosition(int targetPosition)
{
return SnappingLinearLayoutManager.this
.computeScrollVectorForPosition(targetPosition);
}
@Override
protected int getVerticalSnapPreference()
{
return SNAP_TO_START;
}
}
}
嗯,在MainActivity对RecyclerView应用这个布局管理器后,再重新修改一下代码,如下
/**
* 滑动到目前选择框选中的item
*/
private void scrollToSelectedView()
{
//取得中点对应的item,类似于listview的pointToPosition
View selectView = findChildViewUnder(getMeasuredWidth() / 2, getMeasuredHeight
() / 2);
int pos = getChildAdapterPosition(selectView);
mLayoutManager.smoothScrollToPosition(this, null, pos - ((DISPLAY_ITEM_COUNT - 1) / 2));
}
其实就跟第一次的时候一样了,到这里就完成了,可以自动将中点的item装入到选择框。那么接下来的事情就简单了,无非就是改改透明度字体大小。
随着距离而改变的item
分析一下这个需求,我们首先得得到视野内的所有item,然后根据item距离中点的距离来设置字体大小以及透明度。在这个例子中我们的需求是,距离中点越远,字体越小,透明度越低。Ok,明白这些后,我们看一下代码。
/**
* 设置可视recyclerView可视item的属性(字体大小、透明度)
*/
public void setItemStyle()
{
int firstVisiblePos = mLayoutManager.findFirstVisibleItemPosition();
int lastVisiblePos = mLayoutManager.findLastVisibleItemPosition();
for (int i = firstVisiblePos; i <= lastVisiblePos; i++)
{
View view = mLayoutManager.findViewByPosition(i);
int itemHeight = getMeasuredHeight() / DISPLAY_ITEM_COUNT;
int x = (int) view.getX();
int y = (int) view.getY();
//itemCenter:item的中点,相对于parent的y距离
int itemCenter = y + itemHeight / 2;
if (itemCenter < 0) itemCenter = 0;
//distanceToCenter:item的中点距离parent的y距离
int distanceToCenter = Math.abs(getMeasuredHeight() / 2 - itemCenter);
TextView textview = (TextView) view.findViewById(R.id.tv);
//距离中点最近的时候透明度最大,字体也最大
//设置textview字体大小随着距离中点距离的改变而改变
textview.setTextSize((float) (mMaxTextSize - mTextSizeScale * distanceToCenter));
//设置textview透明度随着距离中点距离的改变而改变
textview.setAlpha((float) (mMaxTextAlpha - mTextAlphaScale * distanceToCenter));
}
}
嗯,根据第一个可视item的位置以及最后一个最后可视item的位置来遍历可视的view。然后计算这些view距离中点的距离,在结合前面定义的梯度值来设置字体大小与透明度,关于这个梯度值,你看最后两行你也明白了,其实也可以理解成根据距离来决定变化幅度的因子,这个你想怎么决定就怎么决定,没有什么要求的。ok,到这里基本就完成了,还有什么得注意的呢?在哪里调用这个setItemStyle方法呢?我们滑动的时候这些item的样子也要跟着变化吧?首次加载显式出来的时候也要有样子吧?
那么写在onTouchEvent方法中可以吗?答案是:
不可以,如果写在onTouchEvent,那么你大力一滑,然后松手,页面是有在继续滚动,只是。。 item的样式貌似没变吧。。那么应该写在哪里呢,有人早就知道啦,那就是写在onScroll方法里了。这样只要一滑动,item的样式就都会更新了。那么,看代码
@Override
public void onScrolled(int dx, int dy)
{
super.onScrolled(dx, dy);
setItemStyle();
}
那么还没滑动的时候,设置样式要在哪里设置呢?那么我们想一下,设置这些需要LayoutManager,以及所有子view的纵坐标,那么写在哪里最合适呢,我觉得是onLayout吧。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b)
{
super.onLayout(changed, l, t, r, b);
if (changed)
{
if (mLayoutManager != null)
{
setItemStyle();
}
}
}
注意这里要判断一下布局管理器设置进来了没,因为你就算还没设置布局管理器,它还是会调用onMeasure、onLayout的。这样可能会空指针异常。Ok,到这里就完了。
完结
看起来没多少行代码是吧,我也觉得是,但是我
真 !心 !写 !了 !好!久。不过我写后滑了几下,越来越感觉不对劲,这咋尼玛这么像一个原生控件,想了一下,这难道不是datapicker?唉,本来得先去看一下它怎么实现的。。不过都写好了,心好累,懒得看了。
觉得这样算完了吗?没完呢,你还可以改进的地方有,支持显式偶数个item,自适应的字体大小。更多精彩,等着你自己发挥。
各位看官,不管喜欢不喜欢,踩一下呗。。