城市选择器

作者: 飞奔吧牛牛 | 来源:发表于2019-06-25 15:46 被阅读7次
    网上的城市选择器很多,但还是亲自动手实现一下,效果如下图所示
    Screenshot_2019-06-25-15-27-44.png

    思路:使用RecyclerView的吸附式ItemDecoration(覆写onDrawOver方法),将分好组的城市的拼音首字母绘制到上面。触摸右侧的字母指示器IndicatorView控制RecyclerView滚动到哪个位置。
    所以我们要解决的问题有:
    1.RecyclerView吸附式ItemDdecoration
    2.获取汉字拼音的首字母
    3.根据触摸到的字母,指定Recycler View滚动到对应的位置
    4.自定义View绘制“热门、A、B······Z”,重写绘制方法、测量方法,和触摸方法。

    一:吸附式ItemDecoration。

    https://blog.csdn.net/darlingxian/article/details/80325742

    二:获取汉字拼音的首字

    https://www.cnblogs.com/pxblog/p/10604003.html

    三:RecyclerView滚动到指定位置pos

    https://www.jianshu.com/p/6d5ecfdbb615

    四:自定义字母指示器IndicatorView

    1.绘制data中的字符串:热门、A、B、C······Z
    2.测量大小onMeasure
    3.重写onTouch方法,通过触摸的位置的坐标,计算出触摸的是哪个字母,并传入回调接口中

    package com.app.cityselector.view;
    
    import android.content.Context;
    import android.graphics.Canvas;
    import android.graphics.Paint;
    import android.util.AttributeSet;
    import android.util.DisplayMetrics;
    import android.view.MotionEvent;
    import android.view.View;
    
    import java.util.List;
    
    public class IndicatorView extends View {
    
        private Context context;
        private List<String> data;
        private Paint paint;
        private float ascent;
        private float descent;
        private float textHeight;
        private float textGap;
        private float charWidth;
        private int paddingLeft;
        private int paddingRight;
    
        public IndicatorView(Context context) {
            this(context, null);
        }
    
        public IndicatorView(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public IndicatorView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            this.context = context;
            initView();
        }
    
        private void initView() {
            paint = new Paint();
            DisplayMetrics metrics = context.getResources().getDisplayMetrics();
            paint.setTextSize(9 * metrics.density);
            ascent = paint.getFontMetrics().ascent;
            descent = paint.getFontMetrics().descent;
            textHeight = Math.abs(ascent - descent);
            charWidth = paint.measureText("A");
        }
    
        public void setData(List<String> data) {
            this.data = data;
            invalidate();
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            float y = 0;
            y = -ascent + textGap / 2;
            float x = (getWidth() - paddingLeft - paddingRight - charWidth) / 2;
            for (int i = 0; i < data.size(); i++) {
                canvas.drawText(data.get(i), i == 0 ? 0 : x, y, paint);
                y += textHeight + textGap;
            }
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            int widthSize = MeasureSpec.getSize(widthMeasureSpec);
            int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            int heightSize = MeasureSpec.getSize(heightMeasureSpec);
            int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    
            int width = widthSize;
            int height = heightSize;
            paddingLeft = getPaddingLeft();
            paddingRight = getPaddingRight();
            //wrap_content:计算得出最小的宽高,
            //match_content或具体值:撑满父容器,空出来的地方作为item间隔平均分布到item之间
            if (widthMode == MeasureSpec.EXACTLY) {
                width = widthSize;
            } else {
                width = (int) paint.measureText("热门") + paddingLeft + paddingRight;
            }
            if (heightMode == MeasureSpec.EXACTLY) {
                height = heightSize;
                textGap = (height - textHeight * data.size()) / data.size();
            } else {
                height = (int) (Math.abs(paint.getFontMetrics().ascent - paint.getFontMetrics().descent) * data.size());
            }
            setMeasuredDimension(width, height);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            float y = event.getY();
            int index = (int) (y / (textGap + textHeight));
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                case MotionEvent.ACTION_MOVE:
                    if (onItemTouched != null) {
                        onItemTouched.onTouched(data.get(index), index);
                    }
                    break;
                case MotionEvent.ACTION_UP:
                    if (onItemTouched != null) {
                        onItemTouched.onTouchedUp(data.get(index), index);
                    }
                    break;
            }
            return true;
        }
    
        OnItemTouched onItemTouched;
    
        public void setOnItemTouched(OnItemTouched onItemTouched) {
            this.onItemTouched = onItemTouched;
        }
    
        public interface OnItemTouched {
            void onTouched(String s, int pos);
    
            void onTouchedUp(String s, int pos);
        }
    
    }
    
    

    以上三步做好后,就完成了准备工作。
    CitySelectorView中使用RecyclerView展示数据,根据右侧的字母指示器指定RecyclerView滚动到指定pos。
    实体类:

    public class CityBean {
        private int id;
        private String code;
        private String name;
        //getter and setter
        ......
    }
    

    CityAdapter:展示的数据有热门城市和普通城市Item,所以要区分两类itemType

    package com.app.cityselector.view.adapter;
    
    import android.content.Context;
    import android.support.annotation.NonNull;
    import android.support.v7.widget.RecyclerView;
    import android.util.TypedValue;
    import android.view.Gravity;
    import android.view.LayoutInflater;
    import android.view.View;
    import android.view.ViewGroup;
    import android.widget.LinearLayout;
    import android.widget.TextView;
    
    import com.app.cityselector.R;
    import com.app.cityselector.bean.CityBean;
    
    import java.util.List;
    
    public class CityAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
        public static final int TYPE_HOT_CITY = 1;
        public static final int TYPE_CITY_ITEM = 2;
        private final LayoutInflater inflater;
    
        Context context;
        //热门城市
        List<CityBean> hotCitys;
        //全部城市
        List<CityBean> allCitys;
    
        public CityAdapter(Context context) {
            this.context = context;
            inflater = LayoutInflater.from(context);
        }
    
        public void setHotCitys(List<CityBean> hotCitys) {
            this.hotCitys = hotCitys;
        }
    
        public void setAllCitys(List<CityBean> allCitys) {
            this.allCitys = allCitys;
        }
    
        @Override
        public int getItemViewType(int position) {
            if (hotCitys != null && position == 0) {
                return TYPE_HOT_CITY;
            } else {
                return TYPE_CITY_ITEM;
            }
        }
    
        @NonNull
        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int type) {
            if (type == TYPE_HOT_CITY) {
                return new HotCityViewHolder(inflater.inflate(R.layout.item_hot_city, viewGroup, false));
            } else if (type == TYPE_CITY_ITEM) {
                return new CityItemViewHolder(inflater.inflate(R.layout.item_city, viewGroup, false));
            }
            return null;
        }
    
        @Override
        public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int i) {
            if (viewHolder instanceof HotCityViewHolder) {
                HotCityViewHolder hotCityViewHolder = (HotCityViewHolder) viewHolder;
                hotCityViewHolder.bindData(hotCitys);
            } else if (viewHolder instanceof CityItemViewHolder) {
                CityItemViewHolder cityItemViewHolder = (CityItemViewHolder) viewHolder;
                CityBean cityBean = allCitys.get(hotCitys != null ? i - 1 : i);
                cityItemViewHolder.bindData(cityBean);
            }
        }
    
        @Override
        public int getItemCount() {
            int count = 0;
            if (hotCitys != null) {
                count++;
            }
            if (allCitys != null) {
                count += allCitys.size();
            }
            return count;
        }
    
        static class CityItemViewHolder extends RecyclerView.ViewHolder {
    
            private TextView tvCityName;
    
            public CityItemViewHolder(@NonNull View itemView) {
                super(itemView);
                tvCityName = itemView.findViewById(R.id.tv_city_name);
            }
    
            public void bindData(CityBean cityBean) {
                tvCityName.setText(cityBean.getName());
            }
        }
    
        static class HotCityViewHolder extends RecyclerView.ViewHolder {
    
            private LinearLayout llHotCityContainer;
            //热门城市的列数
            int hotColumn = 3;
    
            public void setHotColumn(int hotColumn) {
                this.hotColumn = hotColumn;
            }
    
            public HotCityViewHolder(@NonNull View itemView) {
                super(itemView);
                llHotCityContainer = itemView.findViewById(R.id.gl_hot_city_container);
            }
            //展示热门城市
            public void bindData(List<CityBean> cityBeans) {
                llHotCityContainer.removeAllViews();
                Context context = itemView.getContext();
                int halfMargin = context.getResources().getDimensionPixelSize(R.dimen.base_margin_half);
                int paddingVertical = context.getResources().getDimensionPixelSize(R.dimen.padding_vertical);
                int row = cityBeans.size() / hotColumn;
                for (int i = 0; i < row + 1; i++) {
                    LinearLayout llRow = new LinearLayout(context);
                    llRow.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
                    llRow.setPadding(halfMargin, halfMargin, halfMargin, halfMargin);
                    for (int j = 0; j < hotColumn; j++) {
                        int index = row * i + j;
                        if (index < cityBeans.size()) {
                            CityBean cityBean = cityBeans.get(index);
                            TextView textView = new TextView(context);
                            textView.setText(cityBean.getName());
                            textView.setBackgroundResource(R.drawable.bg_hot_city);
                            textView.setClickable(true);
                            textView.setPadding(0, paddingVertical, 0, paddingVertical);
                            textView.setGravity(Gravity.CENTER);
                            textView.setTextColor(context.getResources().getColor(R.color.colorText));
                            textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
                            //setLayoutParams
                            LinearLayout.LayoutParams textLayoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
                            textLayoutParams.weight = 1;
                            textLayoutParams.leftMargin = halfMargin;
                            textLayoutParams.rightMargin = halfMargin;
                            textView.setLayoutParams(textLayoutParams);
                            llRow.addView(textView);
                        }
                    }
                    llHotCityContainer.addView(llRow);
                }
            }
        }
    }
    
    

    重点来了,当触摸右边的字母指示器时,根据字母获取RecyclerView中的城市名称的拼音的出现该字母的第一个位置,如果找不到,就定位到上一个字母出现的位置。

    
    
        /**
         * 根据ABC...Z获取第一次出现的pos
         *
         * @param s
         * @return
         */
        private int getFirstPos(String s) {
            if ("热门".equals(s)) {
                return 0;
            }
            int pos = 0;
            if (hotCity != null) {
                pos++;
            }
            int firstPos = 0;
            for (int i = 0; i < allCity.size(); i++) {
                //相等
                String firstLetter = ChinessToEn.getFirstLetter(allCity.get(i).getName()).toUpperCase();
                int compare = firstLetter.compareToIgnoreCase(s);
                if (compare == 0) {
                    firstPos = i;
                    break;
                } else if (compare < 0) {
                    if (i > 1 && !allCity.get(i).getName().equals(allCity.get(i - 1).getName())) {
                        firstPos = i;
                    }
                } else {
                    break;
                }
            }
            pos += firstPos;
            return pos;
        }
    

    获取到位置后,就可以混动RecyclerView了

     public static void moveToPosition(LinearLayoutManager manager, RecyclerView mRecyclerView, int n) {
            int firstItem = manager.findFirstVisibleItemPosition();
            int lastItem = manager.findLastVisibleItemPosition();
            if (n <= firstItem) {
                mRecyclerView.scrollToPosition(n);
            } else if (n <= lastItem) {
                int top = mRecyclerView.getChildAt(n - firstItem).getTop();
                mRecyclerView.scrollBy(0, top);
            } else {
                mRecyclerView.scrollToPosition(n);
            }
        }
    

    CitySelectorView完成代码:

    
    public class CitySelectorView extends FrameLayout implements View.OnClickListener {
    
        private Context context;
        private RecyclerView recyclerView;
        private IndicatorView indicatorView;
        private TextView tvCenter;
    
        private LinearLayoutManager layoutManager;
        private CityAdapter adapter;
        private List<CityBean> hotCity;
        private List<CityBean> allCity;
        private String[] indecatorData = new String[]{"热门", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"};
    
        public void setHotCity(List<CityBean> hotCity) {
            this.hotCity = hotCity;
        }
    
        public void setAllCity(List<CityBean> allCity) {
            this.allCity = allCity;
        }
    
        public void notifyDataSetChanged() {
            adapter.setHotCitys(hotCity);
            adapter.setAllCitys(allCity);
            adapter.notifyDataSetChanged();
        }
    
        public CitySelectorView(@NonNull Context context) {
            this(context, null);
        }
    
        public CitySelectorView(@NonNull Context context, @Nullable AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public CitySelectorView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
            this.context = context;
            init();
        }
    
        private void init() {
            LayoutInflater.from(context).inflate(R.layout.view_city_selector, this, true);
    
            recyclerView = findViewById(R.id.recycler_city);
            tvCenter = findViewById(R.id.tv_center);
            indicatorView = findViewById(R.id.indicator);
            //set RecyclerView
            layoutManager = new LinearLayoutManager(context);
            recyclerView.setLayoutManager(layoutManager);
            //吸附式
            recyclerView.addItemDecoration(new StickyItemDecoration(context, new ISticky() {
                @Override
                public boolean isGroupFirst(int pos) {
                    if (hotCity != null) {
                        if (pos == 0) return true;
                        if (allCity != null && allCity.size() != 0) {
                            if (pos == 1) {
                                return true;
                            } else {
                                return !ChinessToEn.getFirstLetter(allCity.get(pos - 1).getName()).equals(ChinessToEn.getFirstLetter(allCity.get(pos - 2).getName()));
                            }
                        }
                        return false;
                    } else {
                        if (pos == 0) return true;
                        if (allCity != null && allCity.size() != 0) {
                            return !ChinessToEn.getFirstLetter(allCity.get(pos).getName()).equals(ChinessToEn.getFirstLetter(allCity.get(pos - 1).getName()));
                        }
                        return false;
                    }
                }
    
                @Override
                public String getGroupTitle(int pos) {
                    if (hotCity != null) {
                        if (pos == 0) return "热门";
                        if (allCity != null && allCity.size() != 0) {
                            return ChinessToEn.getFirstLetter(allCity.get(pos - 1).getName()).toUpperCase();
                        }
                    } else {
                        if (allCity != null && allCity.size() != 0) {
                            return ChinessToEn.getFirstLetter(allCity.get(pos).getName()).toUpperCase();
                        }
                    }
                    return null;
                }
            }));
            adapter = new CityAdapter(context);
            recyclerView.setAdapter(adapter);
    
            indicatorView.setData(Arrays.asList(indecatorData));
            indicatorView.setOnItemTouched(new IndicatorView.OnItemTouched() {
                @Override
                public void onTouched(String s, int pos) {
                    tvCenter.setVisibility(VISIBLE);
                    tvCenter.setText(s);
                    int firstPos = getFirstPos(s);
                    L.e(firstPos);
                    moveToPosition(layoutManager, recyclerView, firstPos);
                }
    
                @Override
                public void onTouchedUp(String s, int pos) {
                    tvCenter.setVisibility(INVISIBLE);
                }
            });
        }
    
        @Override
        public void onClick(View v) {
    
        }
    
        /**
         * 根据ABC...Z获取第一次出现的pos
         *
         * @param s
         * @return
         */
        private int getFirstPos(String s) {
            if ("热门".equals(s)) {
                return 0;
            }
            int pos = 0;
            if (hotCity != null) {
                pos++;
            }
            int firstPos = 0;
            for (int i = 0; i < allCity.size(); i++) {
                //相等
                String firstLetter = ChinessToEn.getFirstLetter(allCity.get(i).getName()).toUpperCase();
                int compare = firstLetter.compareToIgnoreCase(s);
                if (compare == 0) {
                    firstPos = i;
                    break;
                } else if (compare < 0) {
                    if (i > 1 && !allCity.get(i).getName().equals(allCity.get(i - 1).getName())) {
                        firstPos = i;
                    }
                } else {
                    break;
                }
            }
            pos += firstPos;
            return pos;
        }
    
        /**
         * RecyclerView 移动到当前位置,
         *
         * @param manager       设置RecyclerView对应的manager
         * @param mRecyclerView 当前的RecyclerView
         * @param n             要跳转的位置
         */
        public static void moveToPosition(LinearLayoutManager manager, RecyclerView mRecyclerView, int n) {
            int firstItem = manager.findFirstVisibleItemPosition();
            int lastItem = manager.findLastVisibleItemPosition();
            if (n <= firstItem) {
                mRecyclerView.scrollToPosition(n);
            } else if (n <= lastItem) {
                int top = mRecyclerView.getChildAt(n - firstItem).getTop();
                mRecyclerView.scrollBy(0, top);
            } else {
                mRecyclerView.scrollToPosition(n);
            }
        }
    }
    
    

    每个知识点都不难,整合到一起就是个大功能了。

    相关文章

      网友评论

        本文标题:城市选择器

        本文链接:https://www.haomeiwen.com/subject/meugqctx.html