山寨风,高仿QQ附近的人筛选功能的滑动选择列表来袭!

今天在准备新项目的界面,偶然翻到了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,自适应的字体大小。更多精彩,等着你自己发挥。
各位看官,不管喜欢不喜欢,踩一下呗。。

End!~



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值