1. 程式人生 > >ListView實現類似WheelView效果的探究

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);
        }
    }
}