chore: merge peizhen as regular directory (replace submodule)

This commit is contained in:
rjb
2026-05-11 07:29:06 +08:00
parent 1047599bde
commit d4c0016c73
1076 changed files with 80842 additions and 1 deletions

1
peizhen/citypicker/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

View File

@@ -0,0 +1,41 @@
apply plugin: 'com.android.library'
// apply plugin: 'com.github.dcendents.android-maven' // 与 Gradle 8.9 不兼容,暂时注释
group='com.github.zaaach'
android {
namespace 'com.zaaach.citypicker'
compileSdk 34
compileSdkVersion 34
buildToolsVersion "30.0.2"
defaultConfig {
minSdkVersion 14
targetSdkVersion 27
versionCode 7
versionName "2.0.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
androidTestImplementation('androidx.test.espresso:espresso-core:3.1.0', {
exclude group: 'com.android.support', module: 'support-annotations'
})
testImplementation 'junit:junit:4.12'
compileOnly 'androidx.appcompat:appcompat:1.0.0'
compileOnly 'androidx.recyclerview:recyclerview:1.0.0'
api 'com.google.android.material:material:1.0.0'
}

17
peizhen/citypicker/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,17 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in F:\Android\sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

View File

@@ -0,0 +1,26 @@
package com.zaaach.citypicker;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumentation test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getTargetContext();
assertEquals("com.zaaach.citypicker.test", appContext.getPackageName());
}
}

View File

@@ -0,0 +1,11 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.zaaach.citypicker">
<application
android:allowBackup="true"
android:label="@string/app_name"
android:supportsRtl="true">
</application>
</manifest>

Binary file not shown.

View File

@@ -0,0 +1,132 @@
package com.zaaach.citypicker;
import androidx.annotation.StyleRes;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import com.zaaach.citypicker.adapter.OnPickListener;
import com.zaaach.citypicker.model.HotCity;
import com.zaaach.citypicker.model.LocateState;
import com.zaaach.citypicker.model.LocatedCity;
import java.lang.ref.WeakReference;
import java.util.List;
/**
* @Author: Bro0cL
* @Date: 2018/2/6 17:52
*/
public class CityPicker {
private static final String TAG = "CityPicker";
private WeakReference<FragmentActivity> mContext;
private WeakReference<Fragment> mFragment;
private WeakReference<FragmentManager> mFragmentManager;
private boolean enableAnim;
private int mAnimStyle;
private LocatedCity mLocation;
private List<HotCity> mHotCities;
private OnPickListener mOnPickListener;
private CityPicker(){}
private CityPicker(Fragment fragment){
this(fragment.getActivity(), fragment);
mFragmentManager = new WeakReference<>(fragment.getChildFragmentManager());
}
private CityPicker(FragmentActivity activity){
this(activity, null);
mFragmentManager = new WeakReference<>(activity.getSupportFragmentManager());
}
private CityPicker(FragmentActivity activity, Fragment fragment){
mContext = new WeakReference<>(activity);
mFragment = new WeakReference<>(fragment);
}
public static CityPicker from(Fragment fragment){
return new CityPicker(fragment);
}
public static CityPicker from(FragmentActivity activity){
return new CityPicker(activity);
}
/**
* 设置动画效果
* @param animStyle
* @return
*/
public CityPicker setAnimationStyle(@StyleRes int animStyle) {
this.mAnimStyle = animStyle;
return this;
}
/**
* 设置当前已经定位的城市
* @param location
* @return
*/
public CityPicker setLocatedCity(LocatedCity location) {
this.mLocation = location;
return this;
}
public CityPicker setHotCities(List<HotCity> data){
this.mHotCities = data;
return this;
}
/**
* 启用动画效果,默认为false
* @param enable
* @return
*/
public CityPicker enableAnimation(boolean enable){
this.enableAnim = enable;
return this;
}
/**
* 设置选择结果的监听器
* @param listener
* @return
*/
public CityPicker setOnPickListener(OnPickListener listener){
this.mOnPickListener = listener;
return this;
}
public void show(){
FragmentTransaction ft = mFragmentManager.get().beginTransaction();
final Fragment prev = mFragmentManager.get().findFragmentByTag(TAG);
if (prev != null){
ft.remove(prev).commit();
ft = mFragmentManager.get().beginTransaction();
}
ft.addToBackStack(null);
final CityPickerDialogFragment cityPickerFragment =
CityPickerDialogFragment.newInstance(enableAnim);
cityPickerFragment.setLocatedCity(mLocation);
cityPickerFragment.setHotCities(mHotCities);
cityPickerFragment.setAnimationStyle(mAnimStyle);
cityPickerFragment.setOnPickListener(mOnPickListener);
cityPickerFragment.show(ft, TAG);
}
/**
* 定位完成
* @param location
* @param state
*/
public void locateComplete(LocatedCity location, @LocateState.State int state){
CityPickerDialogFragment fragment = (CityPickerDialogFragment) mFragmentManager.get().findFragmentByTag(TAG);
if (fragment != null){
fragment.locationChanged(location, state);
}
}
}

View File

@@ -0,0 +1,328 @@
package com.zaaach.citypicker;
import android.annotation.SuppressLint;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StyleRes;
import androidx.fragment.app.DialogFragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.DisplayMetrics;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import com.zaaach.citypicker.adapter.CityListAdapter;
import com.zaaach.citypicker.adapter.InnerListener;
import com.zaaach.citypicker.adapter.OnPickListener;
import com.zaaach.citypicker.adapter.decoration.DividerItemDecoration;
import com.zaaach.citypicker.adapter.decoration.SectionItemDecoration;
import com.zaaach.citypicker.db.DBManager;
import com.zaaach.citypicker.model.City;
import com.zaaach.citypicker.model.HotCity;
import com.zaaach.citypicker.model.LocateState;
import com.zaaach.citypicker.model.LocatedCity;
import com.zaaach.citypicker.util.ScreenUtil;
import com.zaaach.citypicker.view.SideIndexBar;
import java.util.ArrayList;
import java.util.List;
/**
* @Author: Bro0cL
* @Date: 2018/2/6 20:50
*/
public class CityPickerDialogFragment extends DialogFragment implements TextWatcher,
View.OnClickListener, SideIndexBar.OnIndexTouchedChangedListener, InnerListener {
private View mContentView;
private RecyclerView mRecyclerView;
private View mEmptyView;
private TextView mOverlayTextView;
private SideIndexBar mIndexBar;
private EditText mSearchBox;
private TextView mCancelBtn;
private ImageView mClearAllBtn;
private LinearLayoutManager mLayoutManager;
private CityListAdapter mAdapter;
private List<City> mAllCities;
private List<HotCity> mHotCities;
private List<City> mResults;
private DBManager dbManager;
private int height;
private int width;
private boolean enableAnim = false;
private int mAnimStyle = R.style.DefaultCityPickerAnimation;
private LocatedCity mLocatedCity;
private int locateState;
private OnPickListener mOnPickListener;
/**
* 获取实例
* @param enable 是否启用动画效果
* @return
*/
public static CityPickerDialogFragment newInstance(boolean enable){
final CityPickerDialogFragment fragment = new CityPickerDialogFragment();
Bundle args = new Bundle();
args.putBoolean("cp_enable_anim", enable);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setStyle(STYLE_NORMAL, R.style.CityPickerStyle);
}
public void setLocatedCity(LocatedCity location){
mLocatedCity = location;
}
public void setHotCities(List<HotCity> data){
if (data != null && !data.isEmpty()){
this.mHotCities = data;
}
}
@SuppressLint("ResourceType")
public void setAnimationStyle(@StyleRes int resId){
this.mAnimStyle = resId <= 0 ? mAnimStyle : resId;
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
mContentView = inflater.inflate(R.layout.cp_dialog_city_picker, container, false);
return mContentView;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
initData();
initViews();
}
private void initViews() {
RelativeLayout rl_back = mContentView.findViewById(R.id.rl_back);
rl_back.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
dismiss();
if (mOnPickListener != null){
mOnPickListener.onCancel();
}
}
});
TextView tv_title = mContentView.findViewById(R.id.tv_title);
mRecyclerView = mContentView.findViewById(R.id.cp_city_recyclerview);
mLayoutManager = new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false);
mRecyclerView.setLayoutManager(mLayoutManager);
mRecyclerView.setHasFixedSize(true);
mRecyclerView.addItemDecoration(new SectionItemDecoration(getActivity(), mAllCities), 0);
mRecyclerView.addItemDecoration(new DividerItemDecoration(getActivity()), 1);
mAdapter = new CityListAdapter(getActivity(), mAllCities, mHotCities, locateState);
mAdapter.autoLocate(true);
mAdapter.setInnerListener(this);
mAdapter.setLayoutManager(mLayoutManager);
mRecyclerView.setAdapter(mAdapter);
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
//确保定位城市能正常刷新
if (newState == RecyclerView.SCROLL_STATE_IDLE){
mAdapter.refreshLocationItem();
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
}
});
mEmptyView = mContentView.findViewById(R.id.cp_empty_view);
mOverlayTextView = mContentView.findViewById(R.id.cp_overlay);
mIndexBar = mContentView.findViewById(R.id.cp_side_index_bar);
mIndexBar.setNavigationBarHeight(ScreenUtil.getNavigationBarHeight(getActivity()));
mIndexBar.setOverlayTextView(mOverlayTextView)
.setOnIndexChangedListener(this);
mSearchBox = mContentView.findViewById(R.id.cp_search_box);
mSearchBox.addTextChangedListener(this);
// mCancelBtn = mContentView.findViewById(R.id.cp_cancel);
// mClearAllBtn = mContentView.findViewById(R.id.cp_clear_all);
// mCancelBtn.setOnClickListener(this);
// mClearAllBtn.setOnClickListener(this);
}
private void initData() {
Bundle args = getArguments();
if (args != null) {
enableAnim = args.getBoolean("cp_enable_anim");
}
//初始化热门城市
if (mHotCities == null || mHotCities.isEmpty()) {
mHotCities = new ArrayList<>();
mHotCities.add(new HotCity("北京", "北京", "101010100"));
mHotCities.add(new HotCity("上海", "上海", "101020100"));
mHotCities.add(new HotCity("广州", "广东", "101280101"));
mHotCities.add(new HotCity("深圳", "广东", "101280601"));
mHotCities.add(new HotCity("天津", "天津", "101030100"));
mHotCities.add(new HotCity("杭州", "浙江", "101210101"));
mHotCities.add(new HotCity("南京", "江苏", "101190101"));
mHotCities.add(new HotCity("成都", "四川", "101270101"));
mHotCities.add(new HotCity("武汉", "湖北", "101200101"));
}
//初始化定位城市,默认为空时会自动回调定位
if (mLocatedCity == null){
mLocatedCity = new LocatedCity(getString(R.string.cp_locating), "未知", "0");
locateState = LocateState.LOCATING;
}else{
locateState = LocateState.SUCCESS;
}
dbManager = new DBManager(getActivity());
mAllCities = dbManager.getAllCities();
mAllCities.add(0, mLocatedCity);
mAllCities.add(1, new HotCity("热门城市", "未知", "0"));
mResults = mAllCities;
}
@Override
public void onStart() {
super.onStart();
Dialog dialog = getDialog();
dialog.setOnKeyListener(new DialogInterface.OnKeyListener() {
@Override
public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK){
if (mOnPickListener != null){
mOnPickListener.onCancel();
}
}
return false;
}
});
measure();
Window window = dialog.getWindow();
if (window != null) {
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
window.setGravity(Gravity.BOTTOM);
window.setLayout(width, height - ScreenUtil.getStatusBarHeight(getActivity()));
if (enableAnim) {
window.setWindowAnimations(mAnimStyle);
}
}
}
//测量宽高
private void measure() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT){
DisplayMetrics dm = new DisplayMetrics();
getActivity().getWindowManager().getDefaultDisplay().getRealMetrics(dm);
height = dm.heightPixels;
width = dm.widthPixels;
}else{
DisplayMetrics dm = getResources().getDisplayMetrics();
height = dm.heightPixels;
width = dm.widthPixels;
}
}
/** 搜索框监听 */
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
public void afterTextChanged(Editable s) {
String keyword = s.toString();
if (TextUtils.isEmpty(keyword)){
// mClearAllBtn.setVisibility(View.GONE);
mEmptyView.setVisibility(View.GONE);
mResults = mAllCities;
((SectionItemDecoration)(mRecyclerView.getItemDecorationAt(0))).setData(mResults);
mAdapter.updateData(mResults);
}else {
// mClearAllBtn.setVisibility(View.VISIBLE);
//开始数据库查找
mResults = dbManager.searchCity(keyword);
((SectionItemDecoration)(mRecyclerView.getItemDecorationAt(0))).setData(mResults);
if (mResults == null || mResults.isEmpty()){
mEmptyView.setVisibility(View.VISIBLE);
}else {
mEmptyView.setVisibility(View.GONE);
mAdapter.updateData(mResults);
}
}
mRecyclerView.scrollToPosition(0);
}
@Override
public void onClick(View v) {
int id = v.getId();
// if (id == R.id.cp_cancel) {
// //dismiss();
// if (mOnPickListener != null){
// mOnPickListener.onCancel();
// }
// }else if(id == R.id.cp_clear_all){
// mSearchBox.setText("");
// }
}
@Override
public void onIndexChanged(String index, int position) {
//滚动RecyclerView到索引位置
mAdapter.scrollToSection(index);
}
public void locationChanged(LocatedCity location, int state){
mAdapter.updateLocateState(location, state);
}
@Override
public void dismiss(int position, City data) {
dismiss();
if (mOnPickListener != null){
mOnPickListener.onPick(position, data);
}
}
@Override
public void locate(){
if (mOnPickListener != null){
mOnPickListener.onLocate();
}
}
public void setOnPickListener(OnPickListener listener){
this.mOnPickListener = listener;
}
}

View File

@@ -0,0 +1,270 @@
package com.zaaach.citypicker.adapter;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.TextView;
import com.zaaach.citypicker.R;
import com.zaaach.citypicker.adapter.decoration.GridItemDecoration;
import com.zaaach.citypicker.model.City;
import com.zaaach.citypicker.model.HotCity;
import com.zaaach.citypicker.model.LocateState;
import com.zaaach.citypicker.model.LocatedCity;
import java.util.List;
/**
* @Author: Bro0cL
* @Date: 2018/2/5 12:06
*/
public class CityListAdapter extends RecyclerView.Adapter<CityListAdapter.BaseViewHolder> {
private static final int VIEW_TYPE_LOCATION = 10;
private static final int VIEW_TYPE_HOT = 11;
private Context mContext;
private List<City> mData;
private List<HotCity> mHotData;
private int locateState;
private InnerListener mInnerListener;
private LinearLayoutManager mLayoutManager;
private boolean stateChanged;
private boolean autoLocate;
public CityListAdapter(Context context, List<City> data, List<HotCity> hotData, int state) {
this.mData = data;
this.mContext = context;
this.mHotData = hotData;
this.locateState = state;
}
public void autoLocate(boolean auto){
autoLocate = auto;
}
public void setLayoutManager(LinearLayoutManager manager){
this.mLayoutManager = manager;
}
public void updateData(List<City> data){
this.mData = data;
notifyDataSetChanged();
}
public void updateLocateState(LocatedCity location, int state){
mData.remove(0);
mData.add(0, location);
stateChanged = !(locateState == state);
locateState = state;
refreshLocationItem();
}
public void refreshLocationItem(){
//如果定位城市的item可见则进行刷新
if (stateChanged && mLayoutManager.findFirstVisibleItemPosition() == 0) {
stateChanged = false;
notifyItemChanged(0);
}
}
/**
* 滚动RecyclerView到索引位置
* @param index
*/
public void scrollToSection(String index){
if (mData == null || mData.isEmpty()) return;
if (index == null || TextUtils.isEmpty(index) || index.length() < 1) return;
String indexFirstChar = index.substring(0, 1);
int size = mData.size();
for (int i = 0; i < size; i++) {
City city = mData.get(i);
if (city == null || city.getSection() == null) continue;
String section = city.getSection();
if (section == null || section.length() < 1) continue;
String sectionFirstChar = section.substring(0, 1);
if (TextUtils.equals(indexFirstChar, sectionFirstChar)){
if (mLayoutManager != null){
mLayoutManager.scrollToPositionWithOffset(i, 0);
// 延迟刷新逻辑已移除以避免 D8 编译问题
return;
}
}
}
}
@NonNull
@Override
public BaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view;
switch (viewType){
case VIEW_TYPE_LOCATION:
view = LayoutInflater.from(mContext).inflate(R.layout.cp_list_item_location_layout, parent, false);
return new LocationViewHolder(view);
case VIEW_TYPE_HOT:
view = LayoutInflater.from(mContext).inflate(R.layout.cp_list_item_hot_layout, parent, false);
return new HotViewHolder(view);
default:
view = LayoutInflater.from(mContext).inflate(R.layout.cp_list_item_default_layout, parent, false);
return new DefaultViewHolder(view);
}
}
@Override
public void onBindViewHolder(@NonNull BaseViewHolder holder, int position) {
if (holder instanceof DefaultViewHolder){
final int pos = holder.getAdapterPosition();
if (pos < 0 || pos >= mData.size()) return;
final City data = mData.get(pos);
if (data == null) return;
String name = data.getName();
if (name == null) name = "";
((DefaultViewHolder)holder).name.setText(name);
((DefaultViewHolder) holder).name.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mInnerListener != null && data != null){
mInnerListener.dismiss(pos, data);
}
}
});
}
//定位城市
if (holder instanceof LocationViewHolder){
final int pos = holder.getAdapterPosition();
if (pos < 0 || pos >= mData.size()) return;
final City data = mData.get(pos);
if (data == null) return;
//设置宽高
DisplayMetrics dm = mContext.getResources().getDisplayMetrics();
int screenWidth = dm.widthPixels;
TypedValue typedValue = new TypedValue();
mContext.getTheme().resolveAttribute(R.attr.cpGridItemSpace, typedValue, true);
int space = mContext.getResources().getDimensionPixelSize(R.dimen.cp_grid_item_space);
int padding = mContext.getResources().getDimensionPixelSize(R.dimen.cp_default_padding);
int indexBarWidth = mContext.getResources().getDimensionPixelSize(R.dimen.cp_index_bar_width);
int itemWidth = (screenWidth - padding - space * (GridListAdapter.SPAN_COUNT - 1) - indexBarWidth) / GridListAdapter.SPAN_COUNT;
ViewGroup.LayoutParams lp = ((LocationViewHolder) holder).container.getLayoutParams();
lp.width = itemWidth;
lp.height = ViewGroup.LayoutParams.WRAP_CONTENT;
((LocationViewHolder) holder).container.setLayoutParams(lp);
switch (locateState){
case LocateState.LOCATING:
((LocationViewHolder) holder).current.setText(R.string.cp_locating);
break;
case LocateState.SUCCESS:
String cityName = data.getName();
if (cityName == null) cityName = "";
((LocationViewHolder) holder).current.setText(cityName);
break;
case LocateState.FAILURE:
((LocationViewHolder) holder).current.setText(R.string.cp_locate_failed);
break;
}
((LocationViewHolder) holder).container.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (locateState == LocateState.SUCCESS) {
if (mInnerListener != null && data != null) {
mInnerListener.dismiss(pos, data);
}
} else if (locateState == LocateState.FAILURE){
locateState = LocateState.LOCATING;
notifyItemChanged(0);
if (mInnerListener != null){
mInnerListener.locate();
}
}
}
});
//第一次弹窗,如果未定位则自动定位
if (autoLocate && locateState == LocateState.LOCATING && mInnerListener != null){
mInnerListener.locate();
autoLocate = false;
}
}
//热门城市
if (holder instanceof HotViewHolder){
final int pos = holder.getAdapterPosition();
if (pos < 0 || pos >= mData.size()) return;
final City data = mData.get(pos);
if (data == null) return;
GridListAdapter mAdapter = new GridListAdapter(mContext, mHotData);
mAdapter.setInnerListener(mInnerListener);
((HotViewHolder) holder).mRecyclerView.setAdapter(mAdapter);
}
}
@Override
public int getItemCount() {
return mData == null ? 0 : mData.size();
}
@Override
public int getItemViewType(int position) {
if (mData == null || position >= mData.size()) return super.getItemViewType(position);
City city = mData.get(position);
if (city == null || city.getSection() == null) return super.getItemViewType(position);
String section = city.getSection();
if (section == null || section.length() < 1) return super.getItemViewType(position);
if (position == 0 && TextUtils.equals("定", section.substring(0, 1)))
return VIEW_TYPE_LOCATION;
if (position == 1 && TextUtils.equals("热", section.substring(0, 1)))
return VIEW_TYPE_HOT;
return super.getItemViewType(position);
}
public void setInnerListener(InnerListener listener){
this.mInnerListener = listener;
}
static class BaseViewHolder extends RecyclerView.ViewHolder{
BaseViewHolder(View itemView) {
super(itemView);
}
}
public static class DefaultViewHolder extends BaseViewHolder{
TextView name;
DefaultViewHolder(View itemView) {
super(itemView);
name = itemView.findViewById(R.id.cp_list_item_name);
}
}
public static class HotViewHolder extends BaseViewHolder {
RecyclerView mRecyclerView;
HotViewHolder(View itemView) {
super(itemView);
mRecyclerView = itemView.findViewById(R.id.cp_hot_list);
mRecyclerView.setHasFixedSize(true);
mRecyclerView.setLayoutManager(new GridLayoutManager(itemView.getContext(),
GridListAdapter.SPAN_COUNT, LinearLayoutManager.VERTICAL, false));
int space = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.cp_grid_item_space);
mRecyclerView.addItemDecoration(new GridItemDecoration(GridListAdapter.SPAN_COUNT,
space));
}
}
public static class LocationViewHolder extends BaseViewHolder {
FrameLayout container;
TextView current;
LocationViewHolder(View itemView) {
super(itemView);
container = itemView.findViewById(R.id.cp_list_item_location_layout);
current = itemView.findViewById(R.id.cp_list_item_location);
}
}
}

View File

@@ -0,0 +1,90 @@
package com.zaaach.citypicker.adapter;
import android.content.Context;
import androidx.recyclerview.widget.RecyclerView;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.TextView;
import com.zaaach.citypicker.R;
import com.zaaach.citypicker.model.City;
import com.zaaach.citypicker.model.HotCity;
import java.util.List;
/**
* @Author: Bro0cL
* @Date: 2018/2/8 21:22
*/
public class GridListAdapter extends RecyclerView.Adapter<GridListAdapter.GridViewHolder>{
public static final int SPAN_COUNT = 3;
private Context mContext;
private List<HotCity> mData;
private InnerListener mInnerListener;
public GridListAdapter(Context context, List<HotCity> data) {
this.mContext = context;
this.mData = data;
}
@Override
public GridViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(mContext).inflate(R.layout.cp_grid_item_layout, parent, false);
return new GridViewHolder(view);
}
@Override
public void onBindViewHolder(GridViewHolder holder, int position) {
final int pos = holder.getAdapterPosition();
final City data = mData.get(pos);
if (data == null) return;
//设置item宽高
DisplayMetrics dm = mContext.getResources().getDisplayMetrics();
int screenWidth = dm.widthPixels;
TypedValue typedValue = new TypedValue();
mContext.getTheme().resolveAttribute(R.attr.cpGridItemSpace, typedValue, true);
int space = mContext.getResources().getDimensionPixelSize(typedValue.resourceId);
int padding = mContext.getResources().getDimensionPixelSize(R.dimen.cp_default_padding);
int indexBarWidth = mContext.getResources().getDimensionPixelSize(R.dimen.cp_index_bar_width);
int itemWidth = (screenWidth - padding - space * (SPAN_COUNT - 1) - indexBarWidth) / SPAN_COUNT;
ViewGroup.LayoutParams lp = holder.container.getLayoutParams();
lp.width = itemWidth;
lp.height = ViewGroup.LayoutParams.WRAP_CONTENT;
holder.container.setLayoutParams(lp);
holder.name.setText(data.getName());
holder.container.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mInnerListener != null){
mInnerListener.dismiss(pos, data);
}
}
});
}
@Override
public int getItemCount() {
return mData == null ? 0 : mData.size();
}
public static class GridViewHolder extends RecyclerView.ViewHolder{
FrameLayout container;
TextView name;
public GridViewHolder(View itemView) {
super(itemView);
container = itemView.findViewById(R.id.cp_grid_item_layout);
name = itemView.findViewById(R.id.cp_gird_item_name);
}
}
public void setInnerListener(InnerListener listener){
this.mInnerListener = listener;
}
}

View File

@@ -0,0 +1,8 @@
package com.zaaach.citypicker.adapter;
import com.zaaach.citypicker.model.City;
public interface InnerListener {
void dismiss(int position, City data);
void locate();
}

View File

@@ -0,0 +1,9 @@
package com.zaaach.citypicker.adapter;
import com.zaaach.citypicker.model.City;
public interface OnPickListener {
void onPick(int position, City data);
void onLocate();
void onCancel();
}

View File

@@ -0,0 +1,45 @@
package com.zaaach.citypicker.adapter.decoration;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import androidx.recyclerview.widget.RecyclerView;
import android.util.TypedValue;
import android.view.View;
import com.zaaach.citypicker.R;
public class DividerItemDecoration extends RecyclerView.ItemDecoration {
private float dividerHeight;
private Paint mPaint;
public DividerItemDecoration(Context context) {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
TypedValue typedValue = new TypedValue();
context.getTheme().resolveAttribute(R.attr.cpSectionBackground, typedValue, true);
mPaint.setColor(context.getResources().getColor(typedValue.resourceId));
dividerHeight = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0.5f, context.getResources().getDisplayMetrics());
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
outRect.bottom = (int) dividerHeight;
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
int childCount = parent.getChildCount();
int left = parent.getPaddingLeft();
int right = parent.getWidth() - parent.getPaddingRight();
for (int i = 0; i < childCount - 1; i++) {
View view = parent.getChildAt(i);
float top = view.getBottom();
float bottom = view.getBottom() + dividerHeight;
c.drawRect(left, top, right, bottom, mPaint);
}
}
}

View File

@@ -0,0 +1,27 @@
package com.zaaach.citypicker.adapter.decoration;
import android.graphics.Rect;
import androidx.recyclerview.widget.RecyclerView;
import android.view.View;
public class GridItemDecoration extends RecyclerView.ItemDecoration{
private int mSpanCount;
private int mSpace;
public GridItemDecoration(int spanCount, int space) {
this.mSpanCount = spanCount;
this.mSpace = space;
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
int position = parent.getChildAdapterPosition(view);
int column = position % mSpanCount;
outRect.left = column * mSpace / mSpanCount;
outRect.right = mSpace - (column + 1) * mSpace / mSpanCount;
if (position >= mSpanCount) {
outRect.top = mSpace;
}
}
}

View File

@@ -0,0 +1,146 @@
package com.zaaach.citypicker.adapter.decoration;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.text.TextPaint;
import android.util.TypedValue;
import android.view.View;
import com.zaaach.citypicker.R;
import com.zaaach.citypicker.model.City;
import java.util.List;
public class SectionItemDecoration extends RecyclerView.ItemDecoration {
private List<City> mData;
private Paint mBgPaint;
private TextPaint mTextPaint;
private Rect mBounds;
private int mSectionHeight;
private int mBgColor;
private int mTextColor;
private int mTextSize;
public SectionItemDecoration(Context context, List<City> data) {
this.mData = data;
TypedValue typedValue = new TypedValue();
context.getTheme().resolveAttribute(R.attr.cpSectionBackground, typedValue, true);
mBgColor = context.getResources().getColor(typedValue.resourceId);
context.getTheme().resolveAttribute(R.attr.cpSectionHeight, typedValue, true);
mSectionHeight = context.getResources().getDimensionPixelSize(typedValue.resourceId);
context.getTheme().resolveAttribute(R.attr.cpSectionTextSize, typedValue, true);
mTextSize = context.getResources().getDimensionPixelSize(typedValue.resourceId);
context.getTheme().resolveAttribute(R.attr.cpSectionTextColor, typedValue, true);
mTextColor = context.getResources().getColor(typedValue.resourceId);
mBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBgPaint.setColor(mBgColor);
mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setTextSize(mTextSize);
mTextPaint.setColor(mTextColor);
mBounds = new Rect();
}
public void setData(List<City> data) {
this.mData = data;
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);
final int left = parent.getPaddingLeft();
final int right = parent.getWidth() - parent.getPaddingRight();
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
.getLayoutParams();
int position = params.getViewLayoutPosition();
if (mData != null && !mData.isEmpty() && position <= mData.size() - 1 && position > -1) {
if (position == 0) {
drawSection(c, left, right, child, params, position);
} else {
if (null != mData.get(position).getSection()
&& !mData.get(position).getSection().equals(mData.get(position - 1).getSection())) {
drawSection(c, left, right, child, params, position);
}
}
}
}
}
private void drawSection(Canvas c, int left, int right, View child,
RecyclerView.LayoutParams params, int position) {
c.drawRect(left,
child.getTop() - params.topMargin - mSectionHeight,
right,
child.getTop() - params.topMargin, mBgPaint);
mTextPaint.getTextBounds(mData.get(position).getSection(),
0,
mData.get(position).getSection().length(),
mBounds);
c.drawText(mData.get(position).getSection(),
child.getPaddingLeft(),
child.getTop() - params.topMargin - (mSectionHeight / 2 - mBounds.height() / 2),
mTextPaint);
}
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
int pos = ((LinearLayoutManager) (parent.getLayoutManager())).findFirstVisibleItemPosition();
if (pos < 0) return;
if (mData == null || mData.isEmpty()) return;
String section = mData.get(pos).getSection();
View child = parent.findViewHolderForLayoutPosition(pos).itemView;
boolean flag = false;
if ((pos + 1) < mData.size()) {
if (null != section && !section.equals(mData.get(pos + 1).getSection())) {
if (child.getHeight() + child.getTop() < mSectionHeight) {
c.save();
flag = true;
c.translate(0, child.getHeight() + child.getTop() - mSectionHeight);
}
}
}
c.drawRect(parent.getPaddingLeft(),
parent.getPaddingTop(),
parent.getRight() - parent.getPaddingRight(),
parent.getPaddingTop() + mSectionHeight, mBgPaint);
mTextPaint.getTextBounds(section, 0, section.length(), mBounds);
c.drawText(section,
child.getPaddingLeft(),
parent.getPaddingTop() + mSectionHeight - (mSectionHeight / 2 - mBounds.height() / 2),
mTextPaint);
if (flag)
c.restore();
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
int position = ((RecyclerView.LayoutParams) view.getLayoutParams()).getViewLayoutPosition();
if (mData != null && !mData.isEmpty() && position <= mData.size() - 1 && position > -1) {
if (position == 0) {
outRect.set(0, mSectionHeight, 0, 0);
} else {
if (null != mData.get(position).getSection()
&& !mData.get(position).getSection().equals(mData.get(position - 1).getSection())) {
outRect.set(0, mSectionHeight, 0, 0);
}
}
}
}
}

View File

@@ -0,0 +1,14 @@
package com.zaaach.citypicker.db;
public class DBConfig {
public static final String DB_NAME_V1 = "china_cities.db";
public static final String DB_NAME_V2 = "china_cities_v2.db";
public static final String LATEST_DB_NAME = DB_NAME_V2;
public static final String TABLE_NAME = "cities";
public static final String COLUMN_C_NAME = "c_name";
public static final String COLUMN_C_PROVINCE = "c_province";
public static final String COLUMN_C_PINYIN = "c_pinyin";
public static final String COLUMN_C_CODE = "c_code";
}

View File

@@ -0,0 +1,130 @@
package com.zaaach.citypicker.db;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Environment;
import com.zaaach.citypicker.model.City;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import static com.zaaach.citypicker.db.DBConfig.COLUMN_C_CODE;
import static com.zaaach.citypicker.db.DBConfig.COLUMN_C_NAME;
import static com.zaaach.citypicker.db.DBConfig.COLUMN_C_PINYIN;
import static com.zaaach.citypicker.db.DBConfig.COLUMN_C_PROVINCE;
import static com.zaaach.citypicker.db.DBConfig.LATEST_DB_NAME;
import static com.zaaach.citypicker.db.DBConfig.DB_NAME_V1;
import static com.zaaach.citypicker.db.DBConfig.TABLE_NAME;
/**
* Author Bro0cL on 2016/1/26.
*/
public class DBManager {
private static final int BUFFER_SIZE = 1024;
private String DB_PATH;
private Context mContext;
public DBManager(Context context) {
this.mContext = context;
DB_PATH = File.separator + "data"
+ Environment.getDataDirectory().getAbsolutePath() + File.separator
+ context.getPackageName() + File.separator + "databases" + File.separator;
copyDBFile();
}
private void copyDBFile(){
File dir = new File(DB_PATH);
if (!dir.exists()){
dir.mkdirs();
}
//如果旧版数据库存在,则删除
File dbV1 = new File(DB_PATH + DB_NAME_V1);
if (dbV1.exists()){
dbV1.delete();
}
//创建新版本数据库
File dbFile = new File(DB_PATH + LATEST_DB_NAME);
if (!dbFile.exists()){
InputStream is;
OutputStream os;
try {
is = mContext.getResources().getAssets().open(LATEST_DB_NAME);
os = new FileOutputStream(dbFile);
byte[] buffer = new byte[BUFFER_SIZE];
int length;
while ((length = is.read(buffer, 0, buffer.length)) > 0){
os.write(buffer, 0, length);
}
os.flush();
os.close();
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public List<City> getAllCities(){
SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(DB_PATH + LATEST_DB_NAME, null);
Cursor cursor = db.rawQuery("select * from " + TABLE_NAME, null);
List<City> result = new ArrayList<>();
City city;
while (cursor.moveToNext()){
String name = cursor.getString(cursor.getColumnIndex(COLUMN_C_NAME));
String province = cursor.getString(cursor.getColumnIndex(COLUMN_C_PROVINCE));
String pinyin = cursor.getString(cursor.getColumnIndex(COLUMN_C_PINYIN));
String code = cursor.getString(cursor.getColumnIndex(COLUMN_C_CODE));
city = new City(name, province, pinyin, code);
result.add(city);
}
cursor.close();
db.close();
Collections.sort(result, new CityComparator());
return result;
}
public List<City> searchCity(final String keyword){
String sql = "select * from " + TABLE_NAME + " where "
+ COLUMN_C_NAME + " like ? " + "or "
+ COLUMN_C_PINYIN + " like ? ";
SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(DB_PATH + LATEST_DB_NAME, null);
Cursor cursor = db.rawQuery(sql, new String[]{"%"+keyword+"%", keyword+"%"});
List<City> result = new ArrayList<>();
while (cursor.moveToNext()){
String name = cursor.getString(cursor.getColumnIndex(COLUMN_C_NAME));
String province = cursor.getString(cursor.getColumnIndex(COLUMN_C_PROVINCE));
String pinyin = cursor.getString(cursor.getColumnIndex(COLUMN_C_PINYIN));
String code = cursor.getString(cursor.getColumnIndex(COLUMN_C_CODE));
City city = new City(name, province, pinyin, code);
result.add(city);
}
cursor.close();
db.close();
CityComparator comparator = new CityComparator();
Collections.sort(result, comparator);
return result;
}
/**
* sort by a-z
*/
private class CityComparator implements Comparator<City>{
@Override
public int compare(City lhs, City rhs) {
String a = lhs.getPinyin().substring(0, 1);
String b = rhs.getPinyin().substring(0, 1);
return a.compareTo(b);
}
}
}

View File

@@ -0,0 +1,77 @@
package com.zaaach.citypicker.model;
import android.text.TextUtils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* author Bro0cL on 2016/1/26.
*/
public class City {
private String name;
private String province;
private String pinyin;
private String code;
public City(String name, String province, String pinyin, String code) {
this.name = name;
this.province = province;
this.pinyin = pinyin;
this.code = code;
}
/***
* 获取悬浮栏文本,(#、定位、热门 需要特殊处理)
* @return
*/
public String getSection(){
if (TextUtils.isEmpty(pinyin)) {
return "#";
} else {
String c = pinyin.substring(0, 1);
Pattern p = Pattern.compile("[a-zA-Z]");
Matcher m = p.matcher(c);
if (m.matches()) {
return c.toUpperCase();
}
//在添加定位和热门数据时设置的section就是‘定’、’热‘开头
else if (TextUtils.equals(c, "定") || TextUtils.equals(c, "热"))
return pinyin;
else
return "#";
}
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPinyin() {
return pinyin;
}
public void setPinyin(String pinyin) {
this.pinyin = pinyin;
}
public String getProvince() {
return province;
}
public void setProvince(String province) {
this.province = province;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
}

View File

@@ -0,0 +1,8 @@
package com.zaaach.citypicker.model;
public class HotCity extends City {
public HotCity(String name, String province, String code) {
super(name, province, "热门城市", code);
}
}

View File

@@ -0,0 +1,16 @@
package com.zaaach.citypicker.model;
import androidx.annotation.IntDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
public class LocateState {
public static final int LOCATING = 123;
public static final int SUCCESS = 132;
public static final int FAILURE = 321;
@IntDef({SUCCESS, FAILURE})
@Retention(RetentionPolicy.SOURCE)
public @interface State{}
}

View File

@@ -0,0 +1,8 @@
package com.zaaach.citypicker.model;
public class LocatedCity extends City {
public LocatedCity(String name, String province, String code) {
super(name, province, "定位城市", code);
}
}

View File

@@ -0,0 +1,83 @@
package com.zaaach.citypicker.util;
import android.app.Activity;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.Build;
import android.provider.Settings;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.WindowManager;
/**
* @Author: Bro0cL
* @Date: 2018/12/4 11:35
* @Discription:
*/
public class ScreenUtil {
private static int getInternalDimensionSize(Context context, String key) {
int result = 0;
try {
int resourceId = context.getResources().getIdentifier(key, "dimen", "android");
if (resourceId > 0) {
result = Math.round(context.getResources().getDimensionPixelSize(resourceId) *
Resources.getSystem().getDisplayMetrics().density /
context.getResources().getDisplayMetrics().density);
}
} catch (Resources.NotFoundException ignored) {
return 0;
}
return result;
}
public static int getStatusBarHeight(Context context){
return getInternalDimensionSize(context, "status_bar_height");
}
public static int getNavigationBarHeight(Context context) {
boolean mInPortrait = context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT;
int result = 0;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
if (hasNavBar((Activity) context)) {
String key;
if (mInPortrait) {
key = "navigation_bar_height";
} else {
key = "navigation_bar_height_landscape";
}
return getInternalDimensionSize(context, key);
}
}
return result;
}
private static boolean hasNavBar(Activity activity) {
//判断小米手机是否开启了全面屏,开启了,直接返回false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
if (Settings.Global.getInt(activity.getContentResolver(), "force_fsg_nav_bar", 0) != 0) {
return false;
}
}
//其他手机根据屏幕真实高度与显示高度是否相同来判断
WindowManager windowManager = activity.getWindowManager();
Display d = windowManager.getDefaultDisplay();
DisplayMetrics realDisplayMetrics = new DisplayMetrics();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
d.getRealMetrics(realDisplayMetrics);
}
int realHeight = realDisplayMetrics.heightPixels;
int realWidth = realDisplayMetrics.widthPixels;
DisplayMetrics displayMetrics = new DisplayMetrics();
d.getMetrics(displayMetrics);
int displayHeight = displayMetrics.heightPixels;
int displayWidth = displayMetrics.widthPixels;
return (realWidth - displayWidth) > 0 || (realHeight - displayHeight) > 0;
}
}

View File

@@ -0,0 +1,171 @@
package com.zaaach.citypicker.view;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import androidx.annotation.Nullable;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;
import android.widget.TextView;
import com.zaaach.citypicker.R;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @Author: Bro0cL
* @Date: 2018/2/8 10:56
*/
public class SideIndexBar extends View{
private static final String[] DEFAULT_INDEX_ITEMS = {"定位", "热门", "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 List<String> mIndexItems;
private float mItemHeight; //每个index的高度
private int mTextSize; //sp
private int mTextColor;
private int mTextTouchedColor;
private int mCurrentIndex = -1;
private Paint mPaint;
private Paint mTouchedPaint;
private int mWidth;
private int mHeight;
private float mTopMargin; //居中绘制,文字绘制起点和控件顶部的间隔
private TextView mOverlayTextView;
private OnIndexTouchedChangedListener mOnIndexChangedListener;
private int navigationBarHeight;
public void setNavigationBarHeight(int height){
this.navigationBarHeight = height;
}
public SideIndexBar(Context context) {
this(context, null);
}
public SideIndexBar(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public SideIndexBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
mIndexItems = new ArrayList<>();
mIndexItems.addAll(Arrays.asList(DEFAULT_INDEX_ITEMS));
TypedValue typedValue = new TypedValue();
context.getTheme().resolveAttribute(R.attr.cpIndexBarTextSize, typedValue, true);
mTextSize = context.getResources().getDimensionPixelSize(typedValue.resourceId);
context.getTheme().resolveAttribute(R.attr.cpIndexBarNormalTextColor, typedValue, true);
mTextColor = context.getResources().getColor(typedValue.resourceId);
context.getTheme().resolveAttribute(R.attr.cpIndexBarSelectedTextColor, typedValue, true);
mTextTouchedColor = context.getResources().getColor(typedValue.resourceId);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setTextSize(mTextSize);
mPaint.setColor(mTextColor);
mTouchedPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mTouchedPaint.setTextSize(mTextSize);
mTouchedPaint.setColor(mTextTouchedColor);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
String index;
for (int i = 0; i < mIndexItems.size(); i++) {
index = mIndexItems.get(i);
Paint.FontMetrics fm = mPaint.getFontMetrics();
canvas.drawText(index,
(mWidth - mPaint.measureText(index)) / 2,
mItemHeight / 2 + (fm.bottom-fm.top) / 2 - fm.bottom + mItemHeight * i + mTopMargin,
i == mCurrentIndex ? mTouchedPaint : mPaint);
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = getWidth();
if (Math.abs(h - oldh) == navigationBarHeight){
//底部导航栏隐藏或显示
mHeight = h;
}else {
//避免软键盘弹出时挤压
mHeight = Math.max(getHeight(), oldh);
}
mItemHeight = mHeight / mIndexItems.size();
mTopMargin = (mHeight - mItemHeight * mIndexItems.size()) / 2;
}
@Override
public boolean performClick() {
return super.performClick();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
performClick();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
float y = event.getY();
int indexSize = mIndexItems.size();
int touchedIndex = (int) (y / mItemHeight);
if (touchedIndex < 0) {
touchedIndex = 0;
}else if (touchedIndex >= indexSize) {
touchedIndex = indexSize - 1;
}
if (mOnIndexChangedListener != null && touchedIndex >= 0 && touchedIndex < indexSize){
if (touchedIndex != mCurrentIndex) {
mCurrentIndex = touchedIndex;
if (mOverlayTextView != null){
mOverlayTextView.setVisibility(VISIBLE);
mOverlayTextView.setText(mIndexItems.get(touchedIndex));
}
mOnIndexChangedListener.onIndexChanged(mIndexItems.get(touchedIndex), touchedIndex);
invalidate();
}
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mCurrentIndex = -1;
if (mOverlayTextView != null){
mOverlayTextView.setVisibility(GONE);
}
invalidate();
break;
}
return true;
}
public SideIndexBar setOverlayTextView(TextView overlay){
this.mOverlayTextView = overlay;
return this;
}
public SideIndexBar setOnIndexChangedListener(OnIndexTouchedChangedListener listener){
this.mOnIndexChangedListener = listener;
return this;
}
public interface OnIndexTouchedChangedListener{
void onIndexChanged(String index, int position);
}
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:fromYDelta="100%p"
android:toYDelta="0"
android:duration="300"/>
</set>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:fromYDelta="0"
android:toYDelta="100%p"
android:duration="300"/>
</set>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/cp_color_grid_item_bg" />
<corners
android:bottomLeftRadius="4dp"
android:bottomRightRadius="4dp"
android:radius="4dp"
android:topLeftRadius="4dp"
android:topRightRadius="4dp"/>
</shape>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/cp_color_gray_dark" />
<corners
android:bottomLeftRadius="6dp"
android:bottomRightRadius="6dp"
android:radius="6dp"
android:topLeftRadius="6dp"
android:topRightRadius="6dp" />
</shape>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape android:shape="rectangle"
xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#fff6f6f6" />
<corners android:topLeftRadius="18dp" android:topRightRadius="18dp" android:bottomLeftRadius="18dp" android:bottomRightRadius="18dp" />
</shape>

View File

@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="#ffffff"
xmlns:android="http://schemas.android.com/apk/res/android">
<RelativeLayout
android:id="@+id/rl_back"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_alignParentLeft="true"
android:layout_centerInParent="true"
android:layout_marginLeft="12dp"
android:gravity="center">
<ImageView
android:layout_width="21dp"
android:layout_height="21dp"
android:background="@drawable/icon_back"
android:textColor="#000000" />
</RelativeLayout>
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="城市选择"
android:textColor="#000000"
android:textSize="17sp" />
<TextView
android:id="@+id/right"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:textColor="#ff333333"
android:textSize="14sp"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:visibility="gone"
android:layout_marginRight="13dp"/>
</RelativeLayout>
<include layout="@layout/cp_search_view" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/cp_city_recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"/>
<TextView
android:id="@+id/cp_overlay"
android:layout_width="?attr/cpOverlayWidth"
android:layout_height="?attr/cpOverlayHeight"
android:visibility="gone"
android:gravity="center"
android:layout_centerInParent="true"
android:textSize="?attr/cpOverlayTextSize"
android:textColor="?attr/cpOverlayTextColor"
android:background="?attr/cpOverlayBackground"/>
<!--右侧字母导航-->
<com.zaaach.citypicker.view.SideIndexBar
android:id="@+id/cp_side_index_bar"
android:layout_width="@dimen/cp_index_bar_width"
android:layout_height="match_parent"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"/>
<include layout="@layout/cp_empty_view"/>
</RelativeLayout>
</LinearLayout>

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/cp_empty_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:visibility="gone"
android:background="@android:color/white"
android:gravity="center_horizontal">
<Space
android:layout_width="match_parent"
android:layout_height="108dp" />
<ImageView
android:id="@+id/cp_no_result_icon"
android:layout_width="?attr/cpEmptyIconWidth"
android:layout_height="?attr/cpEmptyIconHeight"
android:src="?attr/cpEmptyIcon"
android:scaleType="fitCenter"/>
<TextView
android:id="@+id/cp_no_result_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="?attr/cpEmptyText"
android:textSize="?attr/cpEmptyTextSize"
android:textColor="?attr/cpEmptyTextColor"/>
</LinearLayout>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/cp_grid_item_layout"
android:layout_width="wrap_content"
android:layout_height="?attr/cpListItemHeight"
android:foreground="?attr/selectableItemBackground">
<TextView
android:id="@+id/cp_gird_item_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/cp_grid_item_padding"
android:paddingBottom="@dimen/cp_grid_item_padding"
android:gravity="center"
android:textSize="?attr/cpListItemTextSize"
android:textColor="?attr/cpListItemTextColor"
android:background="?attr/cpGridItemBackground"/>
</FrameLayout>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/cp_list_item_name"
android:layout_width="match_parent"
android:layout_height="?attr/cpListItemHeight"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:paddingLeft="16dp"
android:paddingStart="16dp"
android:textColor="?attr/cpListItemTextColor"
android:textSize="?attr/cpListItemTextSize"
tools:ignore="RtlSymmetry" />

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/cp_hot_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:overScrollMode="never"
android:paddingLeft="16dp"
android:paddingBottom="16dp"
android:paddingTop="16dp"
android:paddingRight="@dimen/cp_index_bar_width"
android:paddingEnd="@dimen/cp_index_bar_width"
android:paddingStart="16dp" />

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:padding="16dp">
<FrameLayout
android:id="@+id/cp_list_item_location_layout"
android:layout_width="wrap_content"
android:layout_height="?attr/cpListItemHeight"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
tools:ignore="UselessParent">
<TextView
android:id="@+id/cp_list_item_location"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/cp_grid_item_padding"
android:paddingBottom="@dimen/cp_grid_item_padding"
android:gravity="center"
android:background="?attr/cpGridItemBackground"
android:textSize="?attr/cpListItemTextSize"
android:textColor="?attr/cpListItemTextColor"/>
</FrameLayout>
</LinearLayout>

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/cp_search_view"
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="@android:color/white"
tools:showIn="@layout/cp_dialog_city_picker">
<EditText
android:id="@+id/cp_search_box"
android:layout_width="match_parent"
android:layout_marginLeft="22.5dp"
android:layout_marginRight="22.5dp"
android:gravity="center"
android:layout_height="37dp"
android:background="@drawable/shape_sousuo_bg"
android:hint="搜索城市名"
android:textSize="14sp"
android:textColor="#ff999999"
android:imeActionLabel="搜索"
android:imeOptions="actionSend"
android:inputType="text"
android:singleLine="true"
android:text="" />
</RelativeLayout>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -0,0 +1,6 @@
<resources>
<!-- Example customization of dimensions originally defined in res/values/dimens.xml
(such as screen margins) for screens with more than 820dp of available width. This
would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). -->
<dimen name="activity_horizontal_margin">64dp</dimen>
</resources>

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="cpCancelTextSize" format="dimension|reference" />
<attr name="cpCancelTextColor" format="color|reference" />
<attr name="cpClearTextIcon" format="reference" />
<attr name="cpSearchTextSize" format="dimension|reference" />
<attr name="cpSearchTextColor" format="color|reference" />
<attr name="cpSearchHintText" format="string|reference" />
<attr name="cpSearchHintTextColor" format="color|reference" />
<attr name="cpSearchCursorDrawable" format="reference" />
<attr name="cpListItemTextSize" format="dimension|reference" />
<attr name="cpListItemTextColor" format="color|reference" />
<attr name="cpListItemHeight" format="dimension|reference"/>
<attr name="cpEmptyIcon" format="reference"/>
<attr name="cpEmptyIconWidth" format="dimension|reference"/>
<attr name="cpEmptyIconHeight" format="dimension|reference"/>
<attr name="cpEmptyText" format="string|reference"/>
<attr name="cpEmptyTextSize" format="dimension|reference"/>
<attr name="cpEmptyTextColor" format="color|reference"/>
<attr name="cpGridItemBackground" format="color|reference"/>
<attr name="cpGridItemSpace" format="reference"/>
<attr name="cpSectionHeight" format="reference"/>
<attr name="cpSectionTextSize" format="reference" />
<attr name="cpSectionTextColor" format="reference" />
<attr name="cpSectionBackground" format="reference" />
<attr name="cpIndexBarTextSize" format="reference" />
<attr name="cpIndexBarNormalTextColor" format="reference" />
<attr name="cpIndexBarSelectedTextColor" format="reference" />
<attr name="cpOverlayWidth" format="dimension|reference"/>
<attr name="cpOverlayHeight" format="dimension|reference"/>
<attr name="cpOverlayTextSize" format="dimension|reference"/>
<attr name="cpOverlayTextColor" format="color|reference"/>
<attr name="cpOverlayBackground" format="color|reference"/>
</resources>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="cp_colorPrimary">#3F51B5</color>
<color name="cp_colorPrimaryDark">#303F9F</color>
<color name="cp_colorAccent">#3F51B5</color>
<color name="cp_color_gray_light">#cdcdcd</color>
<color name="cp_color_gray">#999999</color>
<color name="cp_color_gray_deep">#666666</color>
<color name="cp_color_gray_dark">#333333</color>
<color name="cp_color_section_bg">#EDEDED</color>
<color name="cp_color_grid_item_bg">#f5f5f5</color>
</resources>

View File

@@ -0,0 +1,27 @@
<resources>
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="cp_default_padding">16dp</dimen>
<dimen name="cp_index_bar_width">36dp</dimen>
<dimen name="cp_index_bar_text_size">13sp</dimen>
<dimen name="cp_cancel_text_size">15sp</dimen>
<dimen name="cp_search_text_size">15sp</dimen>
<dimen name="cp_empty_icon_width">56dp</dimen>
<dimen name="cp_empty_icon_height">56dp</dimen>
<dimen name="cp_empty_text_size">14sp</dimen>
<dimen name="cp_section_height">32dp</dimen>
<dimen name="cp_section_text_size">15sp</dimen>
<dimen name="cp_list_item_height">50dp</dimen>
<dimen name="cp_list_item_text_size">15sp</dimen>
<dimen name="cp_grid_item_space">16dp</dimen>
<dimen name="cp_grid_item_padding">10dp</dimen>
<dimen name="cp_overlay_width">100dp</dimen>
<dimen name="cp_overlay_height">100dp</dimen>
<dimen name="cp_overlay_text_size">28sp</dimen>
</resources>

View File

@@ -0,0 +1,8 @@
<resources>
<string name="app_name">CityPicker</string>
<string name="cp_search_hint_text">城市中文名或拼音</string>
<string name="cp_cancel">取消</string>
<string name="cp_no_result">没有找到,换个词试试~</string>
<string name="cp_locate_failed">定位失败</string>
<string name="cp_locating">正在定位…</string>
</resources>

View File

@@ -0,0 +1,61 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/cp_colorPrimary</item>
<item name="colorPrimaryDark">@color/cp_colorPrimaryDark</item>
<item name="colorAccent">@color/cp_colorAccent</item>
</style>
<style name="CityPickerStyle" parent="AlertDialog.AppCompat">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:backgroundDimEnabled">false</item>
</style>
<style name="DefaultCityPickerAnimation" parent="@android:style/Animation">
<item name="android:windowEnterAnimation">@anim/cp_push_bottom_in</item>
<item name="android:windowExitAnimation">@anim/cp_push_bottom_out</item>
</style>
<style name="DefaultCityPickerTheme" parent="AppTheme">
<item name="cpCancelTextSize">@dimen/cp_cancel_text_size</item>
<item name="cpCancelTextColor">@color/cp_color_gray</item>
<item name="cpClearTextIcon">@mipmap/cp_icon_clear_all</item>
<item name="cpSearchTextSize">@dimen/cp_search_text_size</item>
<item name="cpSearchTextColor">@color/cp_color_gray_dark</item>
<item name="cpSearchHintText">@string/cp_search_hint_text</item>
<item name="cpSearchHintTextColor">@color/cp_color_gray_light</item>
<item name="cpSearchCursorDrawable">@color/cp_color_gray_dark</item>
<item name="cpListItemHeight">@dimen/cp_list_item_height</item>
<item name="cpListItemTextSize">@dimen/cp_list_item_text_size</item>
<item name="cpListItemTextColor">@color/cp_color_gray_deep</item>
<item name="cpEmptyIcon">@mipmap/cp_icon_empty</item>
<item name="cpEmptyIconWidth">@dimen/cp_empty_icon_width</item>
<item name="cpEmptyIconHeight">@dimen/cp_empty_icon_height</item>
<item name="cpEmptyText">@string/cp_no_result</item>
<item name="cpEmptyTextSize">@dimen/cp_empty_text_size</item>
<item name="cpEmptyTextColor">@color/cp_color_gray_deep</item>
<item name="cpGridItemBackground">@drawable/cp_grid_item_bg</item>
<item name="cpGridItemSpace">@dimen/cp_grid_item_space</item>
<item name="cpSectionHeight">@dimen/cp_section_height</item>
<item name="cpSectionTextSize">@dimen/cp_section_text_size</item>
<item name="cpSectionTextColor">@color/cp_color_gray</item>
<item name="cpSectionBackground">@color/cp_color_section_bg</item>
<item name="cpIndexBarTextSize">@dimen/cp_index_bar_text_size</item>
<item name="cpIndexBarNormalTextColor">@color/cp_color_gray</item>
<item name="cpIndexBarSelectedTextColor">@color/cp_color_gray_dark</item>
<item name="cpOverlayWidth">@dimen/cp_overlay_width</item>
<item name="cpOverlayHeight">@dimen/cp_overlay_height</item>
<item name="cpOverlayTextSize">@dimen/cp_overlay_text_size</item>
<item name="cpOverlayTextColor">@android:color/white</item>
<item name="cpOverlayBackground">@drawable/cp_overlay_bg</item>
</style>
</resources>

View File

@@ -0,0 +1,17 @@
package com.zaaach.citypicker;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}