ListView实现类似WheelView效果的探究

来源:互联网 发布:java值传递 编辑:程序博客网 时间:2024/04/28 22:09

不得不说,作为一名安卓码农,总是会有蛋蛋的忧伤,因为CP常说的就是:你看,人家ios的那个效果好炫酷,比如下面这样的

这里写图片描述

作为一名合格的码农,实在不能忍,最后还是实现了这个效果,虽然没有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);        }    }}
1 0