ListView实现类似WheelView效果的探究
Posted ykb19891230
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ListView实现类似WheelView效果的探究相关的知识,希望对你有一定的参考价值。
不得不说,作为一名安卓码农,总是会有蛋蛋的忧伤,因为CP常说的就是:你看,人家ios的那个效果好炫酷,比如下面这样的
代码已上Github,部分代码有所调整,以Github上代码为准
作为一名合格的码农,实在不能忍,最后还是实现了这个效果,虽然没有ios的厉害。。。
实现的思路还是不复杂的,主要分两个方向:WheelView类似的思想(github一大堆)、ClipToPadding和ClipChildren取巧。因为我是用的取巧,所以我们下面只谈第二种方法。
难点有两个,一是精确地控制listview的item滚动到悬浮框内。因为大多数时候都不会是某个item刚好在悬浮框内的,但是唯一要考虑的情况也只有一种,即悬浮框内同时出现2个或多个item(设计需要,item的高度都是大于等于悬浮框的高度的,所以悬浮框内最多同时出现2个item,小于悬浮框的高度就会出现问题),取出最适合停留的item(这是个相对概念,大家可以修改代码扩展),我目前需要的就是item中间位置的y值和悬浮视图中间位置的y值最接近的一项,后面的代码里大家会看到如何处理的;
二是当listview只有很少的项时,怎么让listview可以滚动呢?这就需要ClipToPadding和ClipChildren来帮忙了。不知道这两个属性的童鞋可以上网查一查,我相信你会受益良多。ClipToPadding=false,我对这个属性简单理解就是,当你为listview设置了padingTop、padingBottom属性,listview会有pading的效果,但是当你滑动listview到顶部或底部时,listview的顶部或底部却不会出现pading的那部分区域,即listview的内容不再被pading的区域遮盖。ClipChildren=false,就是子视图可以超出父视图区域进行绘制。有了这两个属性,就可以解决listview item比较少时不能滚动的问题了。
由于设计需要滚动时item视图的放大和缩小,所以把处理放到了onscrolllistener的onscroll方法里,最后处理listview精确滚动的代码则要放到onscrolllistener的onScrollStateChanged方法里,这些都是可以修改的。
因为要实现这个效果需要adapter配合,所以代码也比较乱,但是注释还是比较详细的,大家慢慢看。
先看看WheelListView
package com.ykbjson.demo.customview.listview;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.AbsListView;
import android.widget.ListView;
import com.ykbjson.demo.tools.SLog;
/**
* 包名:com.ykbjson.demo.customview.listview
* 描述:类似WheelView的ListView
* 创建者:yankebin
* 日期:2016/6/1
*/
public class WheelListView extends ListView
private static final int MAX_Y_OVERSCROLL_DISTANCE = 200;
private final Object SCROLL_LOCK = new Object();
//坐标都是相对于手机屏幕
private int topY;//悬浮框顶部坐标
private int middleY;//悬浮框中间坐标
private int bottomY;//悬浮框底部坐标
private int selectPosition;//滚动时adapter当前选中的position
private boolean fromTouch;//当调用smoothScrollToPositionFromTop()方法时也会触发onScroll,需要屏蔽掉
private WheelAdapter wheelAdapter;//数据适配器
private OnSelectCallback callback;//滚动时的回调接口
private int mMaxYOverScrollDistance;
public interface OnSelectCallback
void onHandleScroll(int selectPosition);
void onHandleIdle(WheelListView wheelListView, int selectPosition);
public WheelListView(Context context)
this(context, null);
public WheelListView(Context context, AttributeSet attrs)
this(context, attrs, 0);
public WheelListView(Context context, AttributeSet attrs, int defStyleAttr)
super(context, attrs, defStyleAttr);
// initBounceListView();
setClipChildren(false);
setClipToPadding(false);
/**
* 阻尼效果实现
*/
// private void initBounceListView()
// //get the density of the screen and do some maths with it on the max overscroll distance
// //variable so that you get similar behaviors no matter what the screen size
// final DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
// final float density = metrics.density;
// mMaxYOverScrollDistance = (int) (density * MAX_Y_OVERSCROLL_DISTANCE);
//
//
// @Override
// protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent)
// //This is where the magic happens, we have replaced the incoming maxOverScrollY with our own custom variable mMaxYOverScrollDistance;
// return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, mMaxYOverScrollDistance, isTouchEvent);
//
public void setAdapter(WheelAdapter adapter)
super.setAdapter(adapter);
wheelAdapter = adapter;
setCallback(adapter);
private void setCallback(OnSelectCallback callback)
this.callback = callback;
/**
* 初始化
*
* @param selectView
* @param rootView
*/
protected void setUp(View selectView, View rootView)
if (null == selectView || null == rootView)
return;
//让listview现实的区域刚好和悬浮框重合
setPadding(0, selectView.getTop() , 0, rootView.getBottom() - selectView.getBottom() );
int location1[] = new int[2];
selectView.getLocationOnScreen(location1);
topY = location1[1];
middleY = topY + selectView.getMeasuredHeight() / 2;
bottomY = topY + selectView.getMeasuredHeight();
setUpScroll();
/**
* 设置滚动监听
*/
private void setUpScroll()
setOnScrollListener(new AbsListView.OnScrollListener()
@Override
public void onScrollStateChanged(AbsListView view, int scrollState)
if (null == callback)
return;
if (scrollState == SCROLL_STATE_IDLE)
if (!fromTouch)
return;
fromTouch = false;
SLog.d("SCROLL_STATE_IDLE");
//adapter实现了callback接口
callback.onHandleIdle(WheelListView.this,selectPosition);
else if (scrollState == SCROLL_STATE_TOUCH_SCROLL)
fromTouch = true;
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)
if (null == callback)
return;
if (!fromTouch)
return;
synchronized (SCROLL_LOCK)
handleScroll(firstVisibleItem);
);
/**
* 处理滚动
*
* @param firstVisibleItem
*/
private void handleScroll(int firstVisibleItem)
//取出与中线距离最近的item
int tempD = -1;
//本次滑动计算出的position
int tempP = -1;
//遍历listview当前可见的item,不是所有item
for (int i = 0; i < getChildCount(); i++)
//计算每个item相对于屏幕的坐标值
View child = getChildAt(i);
int location2[] = new int[2];
child.getLocationOnScreen(location2);
int childBottom = location2[1] + child.getMeasuredHeight();
int childTop = location2[1];
// SLog.d("bottomY : " + bottomY + " topY : " + topY + " middleY : " + middleY + " childBottom : " + childBottom + " childTop : " + childTop);
//在悬浮框区域外的,排除掉
if (childBottom < topY || childTop > bottomY)
continue;
//找到item中线离悬浮框中线最近的item,比距离即可
int childMiddleY = childBottom - child.getMeasuredHeight() / 2;
int position = firstVisibleItem + i;//当前item真正的position
int distance = Math.abs(middleY - childMiddleY);
if (tempD == -1)
tempD = distance;
tempP = position;
else if (tempD > distance)
tempD = distance;
tempP = position;
if (tempP < 0)
tempP = 0;
else if (tempP > wheelAdapter.getCount() - 1)
tempP = wheelAdapter.getCount() - 1;
//防止多次notify同一个position
if (selectPosition == tempP)
return;
selectPosition = tempP;
callback.onHandleScroll(selectPosition);
再看看WheelAdapter
package com.ykbjson.demo.customview.listview;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import com.drivingassisstantHouse.library.base.SimpleAdapterHolder;
import java.util.List;
/**
* 包名:com.ykbjson.demo.customview.listview
* 描述:类似WheelView的ListView的adapter
* 创建者:yankebin
* 日期:2016/6/1
*/
public abstract class WheelAdapter<T> extends BaseAdapter implements WheelListView.OnSelectCallback
/**
* 数据源
*/
private List<T> mData;
/**
* 上下文
*/
private Context mContext;
/**
* item布局索引
*/
private int layoutId;
public Context getmContext()
return mContext;
public void setmContext(Context mContext)
this.mContext = mContext;
public List<T> getmData()
return mData;
public void setmData(List<T> data)
this.mData = data;
public int getLayoutId()
return layoutId;
public void setLayoutId(int layoutId)
this.layoutId = layoutId;
/**
* @param context 上下文
* @param data 数据源
* @param id item的布局资源文件
*/
public WheelAdapter(Context context, List<T> data, int id)
this.mContext = context;
this.mData = data;
this.layoutId = id;
/**
* 数据源改变,刷新界面
*
* @param data
*/
public void refersh(List<T> data)
this.mData = data;
notifyDataSetChanged();
@Override
public int getCount()
if (mData != null)
return mData.size();
return 0;
@Override
public T getItem(int position)
if (mData != null)
return mData.get(position);
return null;
@Override
public long getItemId(int position)
return position;
@Override
public View getView(int position, View convertView, ViewGroup parent)
SimpleAdapterHolder holder = SimpleAdapterHolder.get(convertView, parent, layoutId,
position);
covertView(holder, position, mData, getItem(position));
return holder.getmConvertView();
/**
* 子类可重写此方法实现不同的滚动效果
* @param wheelListView
* @param selectPosition
*/
@Override
public void onHandleIdle(WheelListView wheelListView, int selectPosition)
notifyDataSetChanged();
//精确滚动到某个item的方法,其他的请看api
wheelListView.smoothScrollToPositionFromTop(selectPosition, 0, 400);
public abstract void covertView(SimpleAdapterHolder holder, int position, List<T> dataSource, T data);
SimpleAdapterHolder其实就是一般的viewHolder
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
/**
* 包名:com.ykbjson.demo.customview.listview
* 描述:通用viewholder
* 创建者:yankebin
* 日期:2016/6/1
*/
public class SimpleAdapterHolder
@SuppressWarnings("unused")
private int mPosition;
private View mConvertView;
SparseArray<View> mMembers;
public View getmConvertView()
return mConvertView;
public SimpleAdapterHolder()
private SimpleAdapterHolder(ViewGroup parent, int layoutId, int position)
this.mPosition = position;
this.mMembers = new SparseArray<View>();
mConvertView = LayoutInflater.from(parent.getContext()).inflate(
layoutId, parent, false);
mConvertView.setTag(this);
public static SimpleAdapterHolder get(View convertView, ViewGroup parent,
int layoutId, int position)
if (convertView == null)
return new SimpleAdapterHolder(parent, layoutId, position);
else
return (SimpleAdapterHolder) convertView.getTag();
@SuppressWarnings("unchecked")
public <T extends View> T getView(int viewId)
View view = mMembers.get(viewId);
if (view == null)
view = mConvertView.findViewById(viewId);
mMembers.put(viewId, view);
return (T) view;
为了使用方便,我在外面包装了一层WheelView
package com.ykbjson.demo.customview.listview;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageView;
import com.ykbjson.demo.R;
import com.ykbjson.demo.tools.SLog;
/**
* 包名:com.ykbjson.demo.customview.otherview
* 描述:滚轮视图
* 创建者:yankebin
* 日期:2016/6/2
*/
public class WheelView extends FrameLayout
private WheelListView wheelListView;
private View mSelectView;
public WheelView(Context context)
this(context, null);
public WheelView(Context context, AttributeSet attrs)
this(context, attrs, 0);
public WheelView(Context context, AttributeSet attrs, int defStyleAttr)
super(context, attrs, defStyleAttr);
initView(context, attrs);
/**
* 初始化视图
*
* @param context
* @param attrs
*/
private void initView(Context context, AttributeSet attrs)
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.WheelView);
int floatId = typedArray.getResourceId(R.styleable.WheelView_float_layout, -1);
int bgId = typedArray.getResourceId(R.styleable.WheelView_background_resources, -1);
SLog.d("bgId : " + bgId);
typedArray.recycle();
if (-1 == floatId)
throw new IllegalArgumentException("resId is invalid");
//背景
ImageView imageView = new ImageView(context);
imageView.setScaleType(ImageView.ScaleType.FIT_XY);
try
imageView.setLayerType(LAYER_TYPE_SOFTWARE, null);
catch (Exception e)
e.printStackTrace();
if (-1 != bgId)
imageView.setImageResource(bgId);
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(-1, -1);
addView(imageView, params);
//listview
wheelListView = new WheelListView(context);
wheelListView.setBackgroundColor(Color.TRANSPARENT);
wheelListView.setClipToPadding(false);
wheelListView.setClipChildren(false);
params = new FrameLayout.LayoutParams(-1, -1);
params.gravity = Gravity.CENTER;
addView(wheelListView, params);
//悬浮视图
mSelectView = LayoutInflater.from(context).inflate(floatId, this, false);
params = new FrameLayout.LayoutParams(-1, -2);
params.gravity = Gravity.CENTER;
addView(mSelectView, params);
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom)
super.onLayout(changed, left, top, right, bottom);
wheelListView.setUp(mSelectView, this);
public void setAdapter(WheelAdapter adapter)
wheelListView.setAdapter(adapter);
public WheelListView getWheelListView()
return wheelListView;
定义的属性
<!-- 仿wheelview的listvie-->
<declare-styleable name="WheelView">
<attr name="float_layout" format="reference" />
<attr name="background_resources" format="reference|color" />
</declare-styleable>
最后的使用
在xml里
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.ykbjson.demo.customview.listview.WheelView
app:float_layout="@layout/layout_scroll_list"
app:background_resources="@drawable/trip_bg"
android:id="@+id/wheel"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
在代码里
package com.ykbjson.demo.activity;
import android.content.Context;
import android.os.Bundle;
import android.os.Message;
import android.view.View;
import android.widget.TextView;
import com.drivingassisstantHouse.library.base.BaseActivity;
import com.drivingassisstantHouse.library.base.SimpleAdapterHolder;
import com.nineoldandroids.view.ViewHelper;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.ykbjson.demo.R;
import com.ykbjson.demo.bean.AssistantManager;
import com.ykbjson.demo.customview.listview.WheelAdapter;
import com.ykbjson.demo.customview.listview.WheelView;
import com.ykbjson.demo.customview.otherview.CircleImageView;
import java.util.ArrayList;
import java.util.List;
import butterknife.Bind;
/**
* 包名:com.ykbjson.demo.activity
* 描述:
* 创建者:yankebin
* 日期:2016/5/25
*/
public class TestTravelListActivity extends BaseActivity
@Bind(R.id.wheel)
WheelView wheel;
private ArrayList<AssistantManager> assistantManagers = new ArrayList<>();
private TravelListAdapter adapter;
private void dataChange()
adapter = new TravelListAdapter(this, assistantManagers, R.layout.item_assistant_avatar);
wheel.setAdapter(adapter);
@Override
public int bindLayout()
return R.layout.activity_scroll_list;
@Override
public void initParms(Bundle parms)
@Override
public void initView(View view)
for (int i = 0; i < 2; i++)
AssistantManager manager = new AssistantManager();
manager.setMobile("15208279347");
manager.setName("客服" + i);
manager.setNickName("简途客户" + i);
manager.setAssistantId(i);
manager.setAvatar("drawable://" + R.drawable.customer_service_head);
manager.setPhoto("drawable://" + R.drawable.ad_page);
manager.setDescription("呵呵呵呵 的期望的开启电脑去");
manager.setType(i % 2 == 0 ? 0 : 1);
manager.setTourismSections("成都-都江堰-青城山");
assistantManagers.add(manager);
@Override
public void doBusiness(Context mContext)
baseHandler.sendEmptyMessageDelayed(1, 200);
@Override
public void resume()
@Override
public void destroy()
@Override
public void handleMessage(Message msg)
dataChange();
/*主要代码**/
private class TravelListAdapter extends WheelAdapter<AssistantManager>
private int mSelectPosition;
/**
* @param context 上下文
* @param data 数据源
* @param id item的布局资源文件
*/
public TravelListAdapter(Context context, List data, int id)
super(context, data, id);
@Override
public void onHandleScroll(int selectPosition)
mSelectPosition = selectPosition;
notifyDataSetChanged();
@Override
public void covertView(SimpleAdapterHolder holder, int position, List<AssistantManager> dataSource, AssistantManager manager)
float scale = 1f;
if (mSelectPosition == position)
scale = 1.2f;
TextView tvName = holder.getView(R.id.tv_name);
tvName.setText(manager.getName());
CircleImageView imageView = holder.getView(R.id.iv_avatar);
ImageLoader.getInstance().displayImage(manager.getAvatar(), imageView);
ViewHelper.setScaleX(holder.getmConvertView().findViewById(R.id.layout_content), scale);
ViewHelper.setScaleY(holder.getmConvertView().findViewById(R.id.layout_content), scale);
以上是关于ListView实现类似WheelView效果的探究的主要内容,如果未能解决你的问题,请参考以下文章