Android使用ListView实现滚轮的动画效果

    之前收到一个需求,需要把一个数据展示列表页面做成像滚轮那样的动画效果:中间最大然后向上下两端逐渐缩小。我想了想IOS那边自带滚轮组件,安卓得自己去实现,目前网上仿IOS的滚轮组件的也有一些,但是感觉不适合我,我的要求没那么复杂,于是决定自己动手去实现一下。

    动手前先分析一下应该怎么做,归根到底只是要实现缩放效果,由中间向两边变小,当一个item越接近中间就放大,越远离中间就缩小。那么可以通过先获取ListView的中点,然后获取当前可视的所有item跟ListView的中点的垂直距离计算出一个比例,然后将item的大小根据这个比例进行缩放,各个item跟ListView的中点的垂直距离不同,计算出来的比例也就不同,然后每次滚动的时候都计算比例然后进行缩放,这样应该就能实现我们想要的效果了。

    因为一开始我的列表展示就是用ListView做的,有其他效果在里面,也不方便换其他组件,所以依然还是用ListView实现就好了。由于我们是每次一滚动都要进行缩放,ListView有提供一个OnScrollListener,它的onScroll方法每次一开始滚动就会调用,所以我们选择重写它。当它被调用的时候,我们就开始获取ListView中点,然后开始计算每个item的距离进行缩放.

 /**
     * 中点的Y坐标
     */
    private float centerY = 0f;
    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        //计算中点
        centerY = getHeight()/2;
        //判断中点的有效性
        if(centerY <= 0){
            return;
        }
        //开始对当前显示的View进行缩放
        for(int i = 0; i < visibleItemCount; i++){
            //获取item
            View temp_view = getChildAt(i);
            //计算item的中点Y坐标
            float itemY = temp_view.getBottom()-(temp_view.getHeight()/2);
            //计算离中点的距离
            float distance = centerY;
            if(itemY > centerY){
                distance = itemY - centerY;
            }else{
                distance = centerY - itemY;
            }
            //根据距离进行缩放
            temp_view.setScaleY(1.1f - (distance / centerY) < 0 ? 0 : 1.1f - (distance / centerY));
            temp_view.setScaleX(1.1f - (distance / centerY) < 0 ? 0 : 1.1f - (distance / centerY));
            //根据距离改变透明度
            temp_view.setAlpha(1.1f - (distance / centerY) < 0 ? 0 : 1.1f - (distance / centerY));
        }
    }
    后面不想加demo了,所以直接上整个ListView的代码吧

/**
 * 模仿滚轮动画缩放的ListView
 * Created by xu on 2017/3/3.
 */
public class XuListView extends ListView implements AbsListView.OnScrollListener {
    private static final String TAG = "XuListView";

    /**
     * 中点的Y坐标
     */
    private float centerY = 0f;

    public XuListView(Context context, AttributeSet attrs) {
        super(context, attrs);
        //设置一个滚动监听
        setOnScrollListener(this);
    }

    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {

    }

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        //计算中点
        centerY = getHeight()/2;
        //判断中点的有效性
        if(centerY <= 0){
            return;
        }
        //开始对当前显示的View进行缩放
        for(int i = 0; i < visibleItemCount; i++){
            //获取item
            View temp_view = getChildAt(i);
            //计算item的中点Y坐标
            float itemY = temp_view.getBottom()-(temp_view.getHeight()/2);
            //计算离中点的距离
            float distance = centerY;
            if(itemY > centerY){
                distance = itemY - centerY;
            }else{
                distance = centerY - itemY;
            }
            //根据距离进行缩放
            temp_view.setScaleY(1.1f - (distance / centerY) < 0 ? 0 : 1.1f - (distance / centerY));
            temp_view.setScaleX(1.1f - (distance / centerY) < 0 ? 0 : 1.1f - (distance / centerY));
            //根据距离改变透明度
            temp_view.setAlpha(1.1f - (distance / centerY) < 0 ? 0 : 1.1f - (distance / centerY));
        }
    }
}

    这样基本就实现了我们想要的效果了


    但是现在有一个问题,就是第一个item和最后一个item无法滚动到中间从而放大突出显示。对此我暂时想了两个方法去解决:1、在头尾各种添加一些空白的item,使我们需要显示的数据都可以滚动到最中间。(我现在就是这么做的);2、使整个列表实现循环滚动。

    添加空白的item的话,我是通过修改adapter去实现的。数据源是一个数组,我在数组前面和后面各加一些特殊的数据,adapter实现getview的时候,如果发现当前item的数据是特殊数据,那么item就变透明,从而实现了我们原本要显示的数据都可以被滚动最中间;

    先从数据源下手,从头尾填充特殊的数据

public class MainActivity extends AppCompatActivity {
    XuListView mLisetview;
    MyAdapter adapter;
    ArrayList<String> nos = new ArrayList<String>();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        mLisetview = (XuListView) findViewById(R.id.list_test);

        ArrayList<String> temp = new ArrayList<String>();
        for(int i = 0;i<10;i++){
            temp.add(i+"");
        }
        adapter = new MyAdapter(this,temp);
        mLisetview.setAdapter(adapter);
        resetitem(mLisetview);
    }

    /**
     * 在头尾填充透明的item数据
     */
    private void resetitem(ListView listview) {

        if(listview == null){
            return;
        }
        //获取屏幕高度
        WindowManager wm =getWindowManager();
        int displayheight = wm.getDefaultDisplay().getHeight();
        //计算一个item的高度
        int itemhight = 0;
        if(adapter!=null){
            View v=(View)adapter.getView(0, null, listview);
            v.measure(
                    View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
                    View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
            itemhight=v.getMeasuredHeight();
        }
        //根据Item的高度和屏幕的高度计算需要多少个item可以填满一半的屏幕
        int newcount = ((displayheight/2)/itemhight);
        //填充前面的空白item
        for (int i = 1; i <= newcount; i++) {
            nos.add("full");
        }
        //添加我们需要显示的数据
        for(int i = 0;i<10;i++){
            nos.add(i+"");
        }
        //填充后面的空白item
        for (int i = 1; i <= newcount; i++) {
            nos.add("full");
        }
        //刷新数据
        adapter.refreshData(nos);
    }
}
    然后adapter里面对头尾的特殊数据进行识别,将item变为透明的。

public class MyAdapter extends BaseAdapter {

    ArrayList<String> nos = new ArrayList<String>();
    private Context context;

    public MyAdapter(Context context, ArrayList<String> nos){
        this.context = context;
        this.nos = nos;
    }
    public void refreshData(ArrayList<String> nos) {
        this.nos = nos;
        notifyDataSetChanged();
    }
    @Override
    public int getCount() {
        return nos.size();
    }

    @Override
    public Object getItem(int position) {
        return nos.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder = null;
        if (convertView == null) {
        // 如果是第一次显示该页面(要记得保存到viewholder中供下次直接从缓存中调用)
            holder = new ViewHolder();
            convertView = LayoutInflater.from(context).inflate(R.layout.item_test, null);
            holder.tv_no = (TextView) convertView.findViewById(R.id.tv_no);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder) convertView.getTag();
        }
        holder.tv_no.setText(nos.get(position));
        if(nos.get(position).equals("full")){
            convertView.setVisibility(View.INVISIBLE);
        }else{
            convertView.setVisibility(View.VISIBLE);
        }
        return convertView;
    }
    
    
    private class ViewHolder {
        TextView tv_no;
    }
}

    这样我们就实现可以第一种解决方法


    第二种方法,就是让整个ListView实现循环滚动,实现的方式有很多,大家可以自行百度,我这里就通过修改adapter的getCount方法去实现,就是在getCount方法return一个很大的值,getView获取数据的时候要模原本的数组大小,不然数组就越界了。然后ListView滚动到最中间,这样就实现循环滚动了

public class MainActivity extends AppCompatActivity {
    XuListView mLisetview;
    MyAdapter adapter;
    ArrayList<String> nos = new ArrayList<String>();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mLisetview = (XuListView) findViewById(R.id.list_test);

        ArrayList<String> temp = new ArrayList<String>();
        for(int i = 0;i<10;i++){
            temp.add(i+"");
        }
        adapter = new MyAdapter(this,temp);
        mLisetview.setAdapter(adapter);
        //滚动到中间
        mLisetview.setSelection(adapter.getCount()/2);
    }

}


    

/**
 * Created by xu on 2017/6/27.
 */
public class MyAdapter extends BaseAdapter {

    ArrayList<String> nos = new ArrayList<String>();
    private Context context;

    public MyAdapter(Context context, ArrayList<String> nos){
        this.context = context;
        this.nos = nos;
    }
    @Override
    public int getCount() {
        return Integer.MAX_VALUE;
    }

    @Override
    public Object getItem(int position) {
        return nos.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder = null;
        if (convertView == null) {
        // 如果是第一次显示该页面(要记得保存到viewholder中供下次直接从缓存中调用)
            holder = new ViewHolder();
            convertView = LayoutInflater.from(context).inflate(R.layout.item_test, null);
            holder.tv_no = (TextView) convertView.findViewById(R.id.tv_no);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder) convertView.getTag();
        }
        holder.tv_no.setText(nos.get(position%nos.size()));
        return convertView;
    }


    private class ViewHolder {
        TextView tv_no;
    }
}

    这样我们就实现了使列表进行循环滚动,从而达到每个item都可以滚动到中间突出显示的效果了


    

    既然我们把动画效果都做出来了,那么也可以直接做成一个滚轮选择器,只需要加多两步:1、把最靠近中间的item滚动到中间;2、把中间的item的序号通过一个接口返回出去。 我就直接贴代码吧,反正也不难。

     把最靠近中间的item滚动到中间的实现我是这么做的:首先决定好整个ListView可视的的item数量是多少,必须是奇数,这样才能只有一个item处于正中间,然后根据ListView的高度计算出每个item符合要求的高度,然后更改现有的item的高度,同时对内容进行缩放(不缩放内容单纯高度变小的话布局就乱了)。最后我们利用smoothScrollToPosition方法回正可视item中的第一个item,就实现了将最中间的item回滚到最中间的效果了。因为可视的item我们是根据ListView的高度去计算item的高度的,所有的可视item刚好铺满ListView,所以只要顶部那个回正了,其他的item也会回正。所以我们可以重写一下OnScrollListener的onScrollStateChanged方法,每次滚动结束就执行一次回滚

    

/**
 * 可视的item数
 */
private int mVisibleItemCount = -1;
/**
 * 没调整之前每个item的高度
 */
private float olditemheight = 0;
/**
 * 调整过后的每个item的高度
 */
private float newitemheight = -1;

/**
 * 调整每个可视的item的高度 以及对内容进行缩放
 */
public void reSetItemHeight() {
    for (int i = 0; i < getChildCount(); i++) {
        //获取item
        View temp_view = getChildAt(i);
        //设置item的高度
        ViewGroup.LayoutParams lp = temp_view.getLayoutParams();
        lp.height = (int) newitemheight;
        temp_view.setLayoutParams(lp);

        //缩放内容 我的item的内容用一个LinearLayout包了起来 所以直接缩放LinearLayout
        LinearLayout item_ll_value = (LinearLayout) temp_view.findViewById(R.id.item_ll_value);
        item_ll_value.setScaleY((newitemheight / olditemheight) < 0 ? 0 : (newitemheight / olditemheight));
        item_ll_value.setScaleX((newitemheight / olditemheight) < 0 ? 0 : (newitemheight / olditemheight));
    }
}
/**
 * 计算在给定的可视item数目下 每个item应该设置的高度
 * */
private void getNewItemHeight() {
    //先把旧的item存起来
    olditemheight = getChildAt(0).getHeight();
    //计算新的高度
    newitemheight = getHeight() / mVisibleItemCount;
    if ((getHeight() / mVisibleItemCount) % newitemheight > 0) {
        //除不尽的情况下把余数分给各个item,暂时发现分一次余数就够了,如果效果不理想就做个递归多分几次
        float remainder = (getHeight() / mVisibleItemCount) % newitemheight;
        newitemheight = remainder / mVisibleItemCount;

    }
}

@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {

    //滚动结束之后开始回滚item
    if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE && mVisibleItemCount != -1) {
        //使离中间最近的item回滚到中点位置
        smoothScrollToPosition(getFirstVisiblePosition());
    }

}


    实现了把最靠近中间的item滚动到中间,那么选择的item就是滚动结束后处于最中间的item。对此我们需要再次重写一下OnScrollListener的onScrollStateChanged方法。每次滚动结束后,取可视item中的第一个item的序号加上我们之前设置的可视item数的一半(舍去小数部分)就是最中间的item的序号了,也是当前选择项的序号。

   

/**
     * 当前选中项发生变化的监听者
     */
    private onSelectionChangeLisenter selectionChangeLisenter;

    
    /**
     * 设置选中项的监听者
     */
    public void setSelectionChangeLisenter(onSelectionChangeLisenter selectionChangeLisenter) {
        this.selectionChangeLisenter = selectionChangeLisenter;
    }
    
    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {

         //滚动结束之后开始正常回滚item并记录最中间的item为选中项  (必须设置可视项,ListView才会改为选择器模式)
        if( scrollState ==  AbsListView.OnScrollListener.SCROLL_STATE_IDLE && mVisibleItemCount != -1){
            //使离中间最近的item回滚到中点位置
            smoothScrollToPosition(getFirstVisiblePosition());
            //计算当前选中项的序号
            int nowPosition = getFirstVisiblePosition() + mVisibleItemCount/2;
            //把当前选中项的序号存起来并通过listener回调出去
            if(selectionChangeLisenter != null && nowPosition != curPosition){
                curPosition = nowPosition;
                selectionChangeLisenter.onSelectionChange(curPosition);
            }
        }

    }

     此处我是使用了一个接口去,用以实时把最新的数据返回出去

    /**
     * Created by xu on 2017/3/3.
     */
    public interface onSelectionChangeLisenter {
        void onSelectionChange(int position);
    }

    使用这个滚轮选择器的方法也非常简单,除了跟正常的ListView初始化和绑定adapter之外,只需要额外调用两个方法就行了

 //设置ListView的可视item数(必须是奇数)
        mLisetview.setVisibleItemCount(3);
//设置监听者监听选中项的变化
        mLisetview.setSelectionChangeLisenter(new onSelectionChangeLisenter() {
            @Override
            public void onSelectionChange(final int position) {
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(MainActivity.this,"选择项发生变化 当前选中序号:"+(temp.get(position)),Toast.LENGTH_SHORT).show();
                    }
                });
            }
        });

    这样我们就实现滚轮数字选择器的效果了

    

    现在贴下整个滚轮选择器的完整代码

/**
 * 模仿滚轮动画缩放的ListView
 * Created by xu on 2017/3/3.
 */
public class XuListView extends ListView implements AbsListView.OnScrollListener {
    private static final String TAG = "XuListView";

    /**
     * 中点的Y坐标
     */
    private float centerY = 0f;
    /**
     * 可视的item数
     */
    private int mVisibleItemCount = -1;
    /**
     * 没调整之前每个item的高度
     */
    private float olditemheight = 0;
    /**
     * 调整过后的每个item的高度
     */
    private float newitemheight = -1;
    /**
     * 当前选中项发生变化的监听者
     */
    private onSelectionChangeLisenter selectionChangeLisenter;
    /**
     * 当前选中项的序号
     */
    private int curPosition = -1;

    public XuListView(Context context, AttributeSet attrs) {
        super(context, attrs);
        //设置一个滚动监听
        setOnScrollListener(this);
    }

    /**
     * 设置选中项的监听者
     */
    public void setSelectionChangeLisenter(onSelectionChangeLisenter selectionChangeLisenter) {
        this.selectionChangeLisenter = selectionChangeLisenter;
    }

    /**
     * 设置ListView的显示item数
     * @param count :必须是奇数    如果为-1 则表示只是使用动画效果的普通ListView
     */
    public boolean setVisibleItemCount(int count){
        if(count % 2 == 0){
            return false;
        }else{
            mVisibleItemCount = count;
            return true;
        }

    }

    /**
     * 在这里第一次调整item高度
     */
    @Override
    public void onWindowFocusChanged(boolean hasWindowFocus) {
        super.onWindowFocusChanged(hasWindowFocus);
        if(mVisibleItemCount != -1){
            getNewItemHeight();
            reSetItemHeight();
        }
    }

    /**
     * 调整每个可视的item的高度 以及对内容进行缩放
     */
    public void reSetItemHeight(){
        for(int i = 0; i < getChildCount(); i++){
            //获取item
            View temp_view = getChildAt(i);
            //设置item的高度
            ViewGroup.LayoutParams lp =  temp_view.getLayoutParams();
            lp.height = (int)newitemheight;
            temp_view.setLayoutParams(lp);

            //缩放内容 我的item的内容用一个LinearLayout包了起来 所以直接缩放LinearLayout
            LinearLayout item_ll_value = (LinearLayout)temp_view.findViewById(R.id.item_ll_value);
            item_ll_value.setScaleY((newitemheight / olditemheight) < 0 ? 0 : (newitemheight / olditemheight));
            item_ll_value.setScaleX((newitemheight / olditemheight) < 0 ? 0 : (newitemheight / olditemheight));
        }
    }


    /**
     * 计算在给定的可视item数目下  每个item应该设置的高度
     */
    private void getNewItemHeight(){
        //先把旧的item存起来
        olditemheight = getChildAt(0).getHeight();
        //计算新的高度
        newitemheight = getHeight()/mVisibleItemCount;
        if((getHeight()/mVisibleItemCount) % newitemheight > 0){
            //除不尽的情况下把余数分给各个item,暂时发现分一次余数就够了,如果效果不理想就做个递归多分几次
            float remainder = (getHeight()/mVisibleItemCount) % newitemheight;
            newitemheight = remainder/mVisibleItemCount;
        }
    }
    
    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {

         //滚动结束之后开始正常回滚item并记录最中间的item为选中项  (必须设置可视项,ListView才会改为选择器模式)
        if( scrollState ==  AbsListView.OnScrollListener.SCROLL_STATE_IDLE && mVisibleItemCount != -1){
            //使离中间最近的item回滚到中点位置
            smoothScrollToPosition(getFirstVisiblePosition());
            //计算当前选中项的序号
            int nowPosition = getFirstVisiblePosition() + mVisibleItemCount/2;
            //把当前选中项的序号存起来并通过listener回调出去
            if(selectionChangeLisenter != null && nowPosition != curPosition){
                curPosition = nowPosition;
                selectionChangeLisenter.onSelectionChange(curPosition);
            }
        }
        
    }

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        //计算中点
        centerY = getHeight()/2;
        //判断中点的有效性
        if(centerY <= 0){
            return;
        }
        //开始对当前显示的View进行缩放
        for(int i = 0; i < visibleItemCount; i++){
            //获取item
            View temp_view = getChildAt(i);
            //计算item的中点Y坐标
            float itemY = temp_view.getBottom()-(temp_view.getHeight()/2);
            //计算离中点的距离
            float distance = centerY;
            if(itemY > centerY){
                distance = itemY - centerY;
            }else{
                distance = centerY - itemY;
            }

            //根据距离进行缩放
            temp_view.setScaleY(1.1f - (distance / centerY) < 0 ? 0 : 1.1f - (distance / centerY));
            temp_view.setScaleX(1.1f - (distance / centerY) < 0 ? 0 : 1.1f - (distance / centerY));
            //根据距离改变透明度
            temp_view.setAlpha(1.1f - (distance / centerY) < 0 ? 0 : 1.1f - (distance / centerY));
        }
    }

}
    注释很详细 相信小白看了也没什么难度。

    滚轮效果的实现方式有很多,解决头尾两个item无法滚动到中间的方法也很多,我说的方法仅供参考,没有最好的方法,只有最符合自己的需求的方法。

demo

    

















       











  







猜你喜欢

转载自blog.csdn.net/a287574014/article/details/73827120