android利用RecyclerView+自定義View實現城市選擇介面

龜龜龜龜發表於2017-10-11

1、設計的效果圖

android利用RecyclerView+自定義View實現城市選擇介面

2、分析

根據產品經理的要求:

1、城市選擇介面直接選擇二級城市;

2、不像一般的城市列表一行一個,按照設計圖所示按首字母分組;

3、右側有快速首字母索引。

拿到需求的第一反應,選擇介面採用RecyclerView巢狀GridView來實現,右側快速索引採用自定義view來實現。

資料來源服務端同學已經包裝好,json格式字串為{"datas":{"A":[{"id":"21012","name":"安陽市"},...],"B":[...]},...}

3、實現

首先來實現右側的快速索引,自定義view重寫onDraw方法,新增觸控監聽,並新增觸控監聽回撥,新增可配置化的選中TextView顯示

/**
 * 右側的字母索引View
 */
public class SideBar extends View {

    public String[] INDEX_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"};

    private OnTouchingLetterChangedListener onTouchingLetterChangedListener;
    private List<String> letterList;
    private int choose = -1;
    private Paint paint = new Paint();
    private TextView mTextDialog;

    public SideBar(Context context) {
        this(context, null);
    }

    public SideBar(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SideBar(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init() {
        //根據INDEX_STRING生成字母list
        letterList = Arrays.asList(INDEX_STRING);
    }

    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int height = getHeight();// 獲取對應高度
        int width = getWidth();// 獲取對應寬度
        int singleHeight = height / letterList.size();// 獲取每一個字母的高度
        for (int i = 0; i < letterList.size(); i++) {
            paint.setColor(Color.parseColor("#a9a9a9"));
            paint.setTypeface(Typeface.DEFAULT_BOLD);
            paint.setAntiAlias(true);
            paint.setTextSize(22);
            // 選中的狀態
            if (i == choose) {
                paint.setColor(Color.parseColor("#05c0ab"));
                paint.setFakeBoldText(true);
            }
            // x座標等於中間-字串寬度的一半.
            float xPos = width / 2 - paint.measureText(letterList.get(i)) / 2;
            float yPos = singleHeight * i + singleHeight / 2;
            canvas.drawText(letterList.get(i), xPos, yPos, paint);
            paint.reset();// 重置畫筆
        }
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        final int action = event.getAction();
        final float y = event.getY();// 點選y座標
        final int oldChoose = choose;
        final OnTouchingLetterChangedListener listener = onTouchingLetterChangedListener;
        final int c = (int) (y / getHeight() * letterList.size());// 點選y座標所佔總高度的比例*b陣列的長度就等於點選b中的個數.

        switch (action) {
            case MotionEvent.ACTION_UP:
                setBackgroundResource(R.color.alpha);
                choose = -1;
                invalidate();
                if (mTextDialog != null) {
                    mTextDialog.setVisibility(View.GONE);
                }
                if (c >= 0 && c < letterList.size()) {
                    if (listener != null) {
                        listener.onTouchingLetterChanged(letterList.get(c));
                    }
                }
                break;
            default:
                setBackgroundResource(R.color.background);                if (oldChoose != c) {
                    if (c >= 0 && c < letterList.size()) {
                        if (mTextDialog != null) {
                            mTextDialog.setText(letterList.get(c));
                            mTextDialog.setVisibility(View.VISIBLE);
                        }
                        choose = c;
                        invalidate();
                        if (listener != null) {
                            listener.onTouchingLetterChanging(letterList.get(c));
                        }
                    }
                }
                break;
        }
        return true;
    }

    public void setIndexText(ArrayList<String> indexStrings) {
        this.letterList = indexStrings;
        invalidate();
    }

    /**
     * 為SideBar設定顯示當前按下的字母的TextView
     *
     * @param mTextDialog
     */
    public void setTextView(TextView mTextDialog) {
        this.mTextDialog = mTextDialog;
    }

    /**
     * 向外公開的方法
     *
     * @param onTouchingLetterChangedListener
     */
    public void setOnTouchingLetterChangedListener(
            OnTouchingLetterChangedListener onTouchingLetterChangedListener) {
        this.onTouchingLetterChangedListener = onTouchingLetterChangedListener;
    }

    public void refresh() {
        init();
        invalidate();
    }

    /**
     * 介面
     */
    public interface OnTouchingLetterChangedListener {
        void onTouchingLetterChanged(String s);
        void onTouchingLetterChanging(String s);
    }

}複製程式碼

在佈局檔案中引入

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:background="@color/white">
    <RelativeLayout android:id="@+id/main_title"
            android:layout_width="match_parent"
            android:layout_height="44dp"
            android:background="@color/main_color"
            android:focusable="true"
            android:focusableInTouchMode="true"
            android:gravity="center_vertical"
            android:layout_alignParentTop="true">
        <ImageView android:id="@+id/imageView_back"
               android:layout_width="wrap_content"
               android:layout_height="match_parent"
               android:scaleType="fitCenter"
               android:src="@drawable/back_white"
               android:paddingTop="10dp"
               android:paddingBottom="10dp"
               android:layout_marginRight="10dp"/>
        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center"
            android:text="選擇城市"
            android:textColor="@color/white"
            android:textSize="18sp"/>
    </RelativeLayout>

    <RelativeLayout
        android:layout_width="match_parent"
        android:background="@color/background"
        android:layout_height="match_parent">
        <android.support.v7.widget.RecyclerView
            android:id="@+id/recycler_citys"
            android:layout_width="match_parent"
            android:paddingLeft="15dp"
            android:paddingRight="15dp"
            android:layout_height="match_parent">

        </android.support.v7.widget.RecyclerView>

        <com.gui.ggtest.view.SideBar
            android:id="@+id/sidebar_index"
            android:layout_width="15dp"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"
            android:layout_marginBottom="30dp"
            android:layout_marginTop="30dp" />

        <TextView
            android:id="@+id/tv_index"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:layout_centerInParent="true"
            android:background="@color/alpha"
            android:gravity="center"
            android:textColor="@color/main_color"
            android:textSize="24sp"
            android:textStyle="bold"
            android:visibility="visible" />
    </RelativeLayout>

</LinearLayout>複製程式碼

跑起來效果圖如下

android利用RecyclerView+自定義View實現城市選擇介面

下面該攻克左側城市列表了,在實際實現的過程中,發現如果使用RecyclerView+GridView的方式或者RecyclerView+RecyclerView的方式,都會出現城市列表滑動卡頓的問題,經分析,是RecyclerView在回收利用的時候動態建立新的子View,如果子View也是一個動態重新整理的控制元件,整個列表在滑動的時候沒有那種絲般的順滑(ε=ε=ε=┏(゜ロ゜;)┛所以,決定直接採用一個RecyclerView的方式來實現,採用GridLayoutManager的setSpanSizeLookup來控制多種佈局

關鍵程式碼如下

public class ChooseCityActivity extends Activity{
    //樣例資料
    public String data = "{\"datas\":{\"A\":[{\"id\":\"21012\",\"name\":\"安陽市\"},{\"id\":\"21308\",\"name\":\"安慶市\"},{\"id\":\"21623\",\"name\":\"阿壩\"},{\"id\":\"21803\",\"name\":\"鞍山市\"},{\"id\":\"21913\",\"name\":\"阿拉善盟\"},{\"id\":\"22013\",\"name\":\"安康市\"},{\"id\":\"22304\",\"name\":\"安順市\"},{\"id\":\"22907\",\"name\":\"阿勒泰\"},{\"id\":\"22911\",\"name\":\"阿克蘇\"},{\"id\":\"23002\",\"name\":\"阿里地區\"}],\"B\":[{\"id\":\"20311\",\"name\":\"寶山區\"},{\"id\":\"20833\",\"name\":\"濱州市\"},{\"id\":\"20909\",\"name\":\"保定市\"},{\"id\":\"21305\",\"name\":\"蚌埠市\"},{\"id\":\"21616\",\"name\":\"巴中市\"},{\"id\":\"21812\",\"name\":\"本溪市\"},{\"id\":\"21903\",\"name\":\"包頭市\"},{\"id\":\"21916\",\"name\":\"巴彥淖爾\"},{\"id\":\"22004\",\"name\":\"寶雞市\"},{\"id\":\"22105\",\"name\":\"保山市\"},{\"id\":\"22204\",\"name\":\"北海市\"},{\"id\":\"22212\",\"name\":\"百色市\"},{\"id\":\"22308\",\"name\":\"畢節地區\"},{\"id\":\"22507\",\"name\":\"白山市\"},{\"id\":\"22513\",\"name\":\"白城市\"},{\"id\":\"22607\",\"name\":\"白銀市\"},{\"id\":\"22710\",\"name\":\"白沙\"},{\"id\":\"22714\",\"name\":\"保亭\"},{\"id\":\"22909\",\"name\":\"博爾塔拉\"},{\"id\":\"22910\",\"name\":\"巴音郭楞\"}],\"C\":[{\"id\":\"10400\",\"name\":\"重慶市\"},{\"id\":\"20103\",\"name\":\"崇文區\"},{\"id\":\"20105\",\"name\":\"朝陽區\"},{\"id\":\"20113\",\"name\":\"昌平區\"},{\"id\":\"20304\",\"name\":\"長寧區\"},{\"id\":\"20319\",\"name\":\"崇明縣\"},{\"id\":\"20513\",\"name\":\"潮州市\"},{\"id\":\"20613\",\"name\":\"常州市\"},{\"id\":\"20905\",\"name\":\"承德市\"},{\"id\":\"20914\",\"name\":\"滄州市\"},{\"id\":\"21201\",\"name\":\"長沙市\"},{\"id\":\"21210\",\"name\":\"常德市\"},{\"id\":\"21213\",\"name\":\"郴州市\"},{\"id\":\"21309\",\"name\":\"池州市\"},{\"id\":\"21316\",\"name\":\"滁州市\"},{\"id\":\"21601\",\"name\":\"成都市\"},{\"id\":\"21706\",\"name\":\"長治市\"},{\"id\":\"21816\",\"name\":\"朝陽市\"},{\"id\":\"21904\",\"name\":\"赤峰市\"},{\"id\":\"22110\",\"name\":\"楚雄\"},{\"id\":\"22215\",\"name\":\"崇左市\"},{\"id\":\"22501\",\"name\":\"長春市\"},{\"id\":\"22708\",\"name\":\"澄邁縣\"},{\"id\":\"22711\",\"name\":\"昌江\"},{\"id\":\"22908\",\"name\":\"昌吉\"},{\"id\":\"23004\",\"name\":\"昌都地區\"}],\"D\":[{\"id\":\"20101\",\"name\":\"東城區\"},{\"id\":\"20114\",\"name\":\"大興區\"},{\"id\":\"20504\",\"name\":\"東莞市\"},{\"id\":\"20810\",\"name\":\"東營市\"},{\"id\":\"20812\",\"name\":\"德州市\"},{\"id\":\"21029\",\"name\":\"鄧州市\"},{\"id\":\"21609\",\"name\":\"德陽市\"},{\"id\":\"21621\",\"name\":\"達州市\"},{\"id\":\"21703\",\"name\":\"大同市\"},{\"id\":\"21802\",\"name\":\"大連市\"},{\"id\":\"21808\",\"name\":\"丹東市\"},{\"id\":\"22102\",\"name\":\"迪慶\"},{\"id\":\"22114\",\"name\":\"大理\"},{\"id\":\"22115\",\"name\":\"德巨集\"},{\"id\":\"22402\",\"name\":\"大慶市\"},{\"id\":\"22414\",\"name\":\"大興安嶺\"},{\"id\":\"22609\",\"name\":\"定西市\"},{\"id\":\"22705\",\"name\":\"東方市\"},{\"id\":\"22706\",\"name\":\"定安縣\"},{\"id\":\"22725\",\"name\":\"儋州市\"}],\"E\":[{\"id\":\"21105\",\"name\":\"鄂州市\"},{\"id\":\"21114\",\"name\":\"恩施\"},{\"id\":\"21902\",\"name\":\"鄂爾多斯\"}],\"F\":[{\"id\":\"20106\",\"name\":\"豐臺區\"},{\"id\":\"20110\",\"name\":\"房山區\"},{\"id\":\"20318\",\"name\":\"奉賢區\"},{\"id\":\"20503\",\"name\":\"佛山市\"},{\"id\":\"21306\",\"name\":\"阜陽市\"},{\"id\":\"21401\",\"name\":\"福州市\"},{\"id\":\"21508\",\"name\":\"撫州市\"},{\"id\":\"21813\",\"name\":\"撫順市\"},{\"id\":\"21815\",\"name\":\"阜新市\"},{\"id\":\"22206\",\"name\":\"防城港\"}],\"G\":[{\"id\":\"20501\",\"name\":\"廣州市\"},{\"id\":\"21502\",\"name\":\"贛州市\"},{\"id\":\"21619\",\"name\":\"廣元市\"}]}}";
    ImageView back;
    RecyclerView recycler_citys;
    CityChooseRecyclerAdapter cityChooseRecyclerAdapter;
    TextView tv_index;
    SideBar sidebar_index;
    Map<String, Object> datas;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_citychoose);
        initView();
        initData();
        addListener();
    }

    private void addListener() {
        back.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
                ChooseCityActivity.this.finish();
            }
        });
    }

    private void initData() {
        //實際專案中資料從網路獲取,這邊寫死資料
        Gson gson = new Gson();
        Type type =  new TypeToken<Map<String, Object>>()
        {
        }.getType();
        Map<String, Object> citysMap = gson.fromJson(data,type);
        datas = (Map<String, Object>) citysMap.get("datas");
        //初始化recyclerview
        initRecyclerView();
        //初始化sidebar
        initSideBar();
    }

    private void initRecyclerView() {
        cityChooseRecyclerAdapter= new CityChooseRecyclerAdapter(ChooseCityActivity.this,datas);
        //定義佈局管理
        final GridLayoutManager linearLayoutManager = new GridLayoutManager(ChooseCityActivity.this,3);
        //佈局分類的關鍵方法
        linearLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
            @Override
            public int getSpanSize(int position) {
                int result = cityChooseRecyclerAdapter.getItemViewType(position)== CityChooseRecyclerAdapter.ITEM_HEAD? 3 : 1;
                return result;
            }
        });
        recycler_citys.setLayoutManager(linearLayoutManager);
        recycler_citys.setAdapter(cityChooseRecyclerAdapter);
    }

    private void initSideBar() {
        //根據資料裡的列表控制右側導航欄
        final List<String> indexList = new ArrayList(datas.keySet());
        String[] strings = new String[indexList.size()];
        sidebar_index.INDEX_STRING = indexList.toArray(strings);
        sidebar_index.refresh();
        sidebar_index.setOnTouchingLetterChangedListener(new SideBar.OnTouchingLetterChangedListener() {
            @Override
            public void onTouchingLetterChanged(String s) {
                //滑動結束監聽
//                recycler_citys.smoothScrollToPosition(index);
            }
            @Override
            public void onTouchingLetterChanging(String s) {
                //滑動過程中監聽
                int index = cityChooseRecyclerAdapter.getIndexPosition(s);
                recycler_citys.scrollToPosition(index);
            }
        });
    }

    private void initView() {
        sidebar_index = (SideBar) findViewById(R.id.sidebar_index);
        tv_index = (TextView) findViewById(R.id.tv_index);
        recycler_citys = (RecyclerView) findViewById(R.id.recycler_citys);
        sidebar_index.setTextView(tv_index);
        back = (ImageView) findViewById(R.id.imageView_back);
    }

    public void selectCity(String cityId, String cityName) {
        if (!TextUtils.isEmpty(cityName)&&!TextUtils.isEmpty(cityId)){
            Toast.makeText(ChooseCityActivity.this,"cityId="+cityId+"--cityName="+cityName,Toast.LENGTH_SHORT).show();
        }
    }
}複製程式碼
public class CityChooseRecyclerAdapter extends RecyclerView.Adapter{

    public static final int ITEM_HEAD = 0;
    public static final int ITEM_CONTENT = 1;


    private LayoutInflater mInflater;
    public Context mContext;
    public List<Map<String,String>> dataList = new ArrayList<>();
    public List<Integer> numList = new ArrayList<>();
    public List<String> indexList = new ArrayList<>();

    public CityChooseRecyclerAdapter(Context context, Map<String, Object> datas){
        mInflater = LayoutInflater.from(context);
        mContext = context;
        List<String> indexList = new ArrayList(datas.keySet());
        for (String string : indexList) {
            numList.add(dataList.size());
            Map<String,String> headMap = new HashMap<>();
            headMap.put("type","head");
            headMap.put("name",string);
            dataList.add(headMap);
            List<Map<String, String>> templist = (List<Map<String, String>>) datas.get(string);
            dataList.addAll(templist);
        }
        this.indexList = indexList;
    }

    @Override
    public int getItemViewType(int position) {
        Map<String,String> item = dataList.get(position);
        if (TextUtils.isEmpty(item.get("type"))){
            return ITEM_CONTENT;
        }else if ("head".equals(item.get("type"))){
            return ITEM_HEAD;
        }else{
            return ITEM_CONTENT;
        }
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        RecyclerView.ViewHolder viewHolder = null;
        switch (viewType){
            case ITEM_HEAD:
                viewHolder = new CityChooseGridRViewHolder(mInflater.inflate(R.layout.item_grid_title, null,false));
                break;
            case ITEM_CONTENT:
                viewHolder = new CityChooseGridRViewHolder(mInflater.inflate(R.layout.item_grid_textvie, null,false));
                break;
        }
        return viewHolder;
    }
    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, final int position) {
        CityChooseGridRViewHolder cityChooseViewHolder = (CityChooseGridRViewHolder)holder;
        final Map<String,String> item = dataList.get(position);
        cityChooseViewHolder.tv_name.setText(item.get("name"));
        cityChooseViewHolder.tv_name.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (getItemViewType(position)==ITEM_CONTENT){
                    ((ChooseCityActivity)mContext).selectCity(item.get("id"),item.get("name"));
                }
            }
        });
    }

    @Override
    public int getItemCount() {
        return dataList.size();
    }
    private class CityChooseGridRViewHolder extends RecyclerView.ViewHolder
    {
        TextView tv_name;
        public CityChooseGridRViewHolder(View itemView) {
            super(itemView);
            tv_name = (TextView) itemView.findViewById(R.id.tv_name);
        }
    }

    public int getIndexPosition(String s){
        try{
            return numList.get(indexList.indexOf(s));
        }catch (Exception e){
            return 0;
        }
    }
}複製程式碼

最終實現效果

android利用RecyclerView+自定義View實現城市選擇介面

定位不在本篇討論範圍之內,用的是百度定位,接入起來也很簡單,暫且不表

ε=ε=ε=┏(゜ロ゜;)┛

4、總結

實現該頁面的難點主要就兩個,android不像ios那樣自帶索引view,需要自己實現,再一個就是這個分組的城市列表坑了一下,不像我們常見的

android利用RecyclerView+自定義View實現城市選擇介面這種形式

因此在實現的時候要特別注意一些細節,比如首字母分組,這邊是伺服器幫忙做了,比如RecyclerView巢狀的效能,我就在這個坑裡爬了一上午。。嘗試了RecyclerView+GridView和RecyclerView+RecyclerView的模式,都是卡頓,最後直接採用單個RecyclerView的模式,整個世界都清靜了。

好了,我要繼續寫程式碼了  ε=ε=ε=┏(゜ロ゜;)┛


程式碼git:github.com/gh8623/GGTe…


相關文章