在开发的过程中,现有的控件满足不了功能的需求,这个时候就需要我们自定义控件了。最近在开发中需要实现滚动进行类别的选择,也就是我们所说的滚动选择器,这里我们自定义来实现这个功能。
实现步骤:
1、先看需要实现的效果图
2、实现自定义控件(PickerScrollView )
3、popupwindow实现类(CommonPopWindow )
4.主界面(MainActivity)
5、实体类(GetConfigReq )
6、底部弹出框布局文件
2、实现自定义控件(PickerScrollView )
/**
* 滚动选择器
*/
public class PickerScrollView extends View {
public static final String TAG = "PickerScrollView:";
/**
* text之间间距和minTextSize之比
*/
public static final float MARGIN_ALPHA = 2.8f;
/**
* 自动回滚到中间的速度
*/
public static final float SPEED = 2;
private List mDataList;
/**
* 选中的位置,这个位置是mDataList的中心位置,一直不变
*/
private int mCurrentSelected;
private Paint mPaint;
private float mMaxTextSize = 20;
private float mMinTextSize = 10;
private float mMaxTextAlpha = 255;
private float mMinTextAlpha = 120;
private int mColorText = 0x333333;
private int mViewHeight;
private int mViewWidth;
private float mLastDownY;
/**
* 滑动的距离
*/
private float mMoveLen = 0;
private boolean isInit = false;
private onSelectListener mSelectListener;
private Timer timer;
private MyTimerTask mTask;
Handler updateHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (Math.abs(mMoveLen) < SPEED) {
mMoveLen = 0;
if (mTask != null) {
mTask.cancel();
mTask = null;
performSelect();
}
} else
// 这里mMoveLen / Math.abs(mMoveLen)是为了保有mMoveLen的正负号,以实现上滚或下滚
mMoveLen = mMoveLen - mMoveLen / Math.abs(mMoveLen) * SPEED;
invalidate();
}
};
public PickerScrollView(Context context) {
super(context);
init();
}
public PickerScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public void setOnSelectListener(onSelectListener listener) {
mSelectListener = listener;
}
private void performSelect() {
if (mSelectListener != null)
mSelectListener.onSelect(mDataList.get(mCurrentSelected));
}
public void setData(List datas) {
mDataList = datas;
mCurrentSelected = datas.size() / 2;
invalidate();
}
/**
* 选择选中的item的index
*
* @param selected
*/
public void setSelected(int selected) {
mCurrentSelected = selected;
int distance = mDataList.size() / 2 - mCurrentSelected;
if (distance < 0)
for (int i = 0; i < -distance; i++) {
moveHeadToTail();
mCurrentSelected--;
}
else if (distance > 0)
for (int i = 0; i < distance; i++) {
moveTailToHead();
mCurrentSelected++;
}
invalidate();
}
/**
* 选择选中的内容
*
* @param mSelectItem
*/
public void setSelected(String mSelectItem) {
for (int i = 0; i < mDataList.size(); i++)
if (mDataList.get(i).equals(mSelectItem)) {
setSelected(i);
break;
}
}
private void moveHeadToTail() {
GetConfigReq.DatasBean datasBean = mDataList.get(0);
mDataList.remove(0);
mDataList.add(datasBean);
}
private void moveTailToHead() {
GetConfigReq.DatasBean datasBean = mDataList.get(mDataList.size() - 1);
mDataList.remove(mDataList.size() - 1);
mDataList.add(0, datasBean);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mViewHeight = getMeasuredHeight();
mViewWidth = getMeasuredWidth();
// 按照View的高度计算字体大小
mMaxTextSize = mViewHeight / 8.0f;
mMinTextSize = mMaxTextSize / 2f;
isInit = true;
invalidate();
}
private void init() {
timer = new Timer();
mDataList = new ArrayList<>();
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setTextAlign(Paint.Align.CENTER);
mPaint.setColor(mColorText);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 根据index绘制view
if (isInit)
drawData(canvas);
}
private void drawData(Canvas canvas) {
// 先绘制选中的text再往上往下绘制其余的text
float scale = parabola(mViewHeight / 4.0f, mMoveLen);
float size = (mMaxTextSize - mMinTextSize) * scale + mMinTextSize;
mPaint.setTextSize(size);
mPaint.setAlpha((int) ((mMaxTextAlpha - mMinTextAlpha) * scale + mMinTextAlpha));
// text居中绘制,注意baseline的计算才能达到居中,y值是text中心坐标
float x = (float) (mViewWidth / 2.0);
float y = (float) (mViewHeight / 2.0 + mMoveLen);
Paint.FontMetricsInt fmi = mPaint.getFontMetricsInt();
float baseline = (float) (y - (fmi.bottom / 2.0 + fmi.top / 2.0));
int indexs = mCurrentSelected;
String textData = mDataList.get(indexs).getCategoryName();
canvas.drawText(textData, x, baseline, mPaint);
// 绘制上方data
for (int i = 1; (mCurrentSelected - i) >= 0; i++) {
drawOtherText(canvas, i, -1);
}
// 绘制下方data
for (int i = 1; (mCurrentSelected + i) < mDataList.size(); i++) {
drawOtherText(canvas, i, 1);
}
}
/**
* @param canvas
* @param position 距离mCurrentSelected的差值
* @param type 1表示向下绘制,-1表示向上绘制
*/
private void drawOtherText(Canvas canvas, int position, int type) {
float d = (float) (MARGIN_ALPHA * mMinTextSize * position + type
* mMoveLen);
float scale = parabola(mViewHeight / 4.0f, d);
float size = (mMaxTextSize - mMinTextSize) * scale + mMinTextSize;
mPaint.setTextSize(size);
mPaint.setAlpha((int) ((mMaxTextAlpha - mMinTextAlpha) * scale + mMinTextAlpha));
float y = (float) (mViewHeight / 2.0 + type * d);
Paint.FontMetricsInt fmi = mPaint.getFontMetricsInt();
float baseline = (float) (y - (fmi.bottom / 2.0 + fmi.top / 2.0));
int indexs = mCurrentSelected + type * position;
String textData = mDataList.get(indexs).getCategoryName();
canvas.drawText(textData, (float) (mViewWidth / 2.0), baseline, mPaint);
}
/**
* 抛物线
*
* @param zero 零点坐标
* @param x 偏移量
* @return scale
*/
private float parabola(float zero, float x) {
float f = (float) (1 - Math.pow(x / zero, 2));
return f < 0 ? 0 : f;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
doDown(event);
break;
case MotionEvent.ACTION_MOVE:
doMove(event);
break;
case MotionEvent.ACTION_UP:
doUp(event);
break;
}
return true;
}
private void doDown(MotionEvent event) {
if (mTask != null) {
mTask.cancel();
mTask = null;
}
mLastDownY = event.getY();
}
private void doMove(MotionEvent event) {
mMoveLen += (event.getY() - mLastDownY);
if (mMoveLen > MARGIN_ALPHA * mMinTextSize / 2) {
// 往下滑超过离开距离
moveTailToHead();
mMoveLen = mMoveLen - MARGIN_ALPHA * mMinTextSize;
} else if (mMoveLen < -MARGIN_ALPHA * mMinTextSize / 2) {
// 往上滑超过离开距离
moveHeadToTail();
mMoveLen = mMoveLen + MARGIN_ALPHA * mMinTextSize;
}
mLastDownY = event.getY();
invalidate();
}
private void doUp(MotionEvent event) {
// 抬起手后mCurrentSelected的位置由当前位置move到中间选中位置
if (Math.abs(mMoveLen) < 0.0001) {
mMoveLen = 0;
return;
}
if (mTask != null) {
mTask.cancel();
mTask = null;
}
mTask = new MyTimerTask(updateHandler);
timer.schedule(mTask, 0, 10);
}
class MyTimerTask extends TimerTask {
Handler handler;
public MyTimerTask(Handler handler) {
this.handler = handler;
}
@Override
public void run() {
handler.sendMessage(handler.obtainMessage());
}
}
public interface onSelectListener {
void onSelect(GetConfigReq.DatasBean pickers);
}
}
3、popupwindow实现类(CommonPopWindow )
/**
* popupwindow
*/
public class CommonPopWindow {
private static PopupWindow mPopupWindow;
private static Builder mBuilder;
private static View mContentView;
private static Window mWindow;
public interface ViewClickListener {
void getChildView(PopupWindow mPopupWindow, View view, int mLayoutResId);
}
private CommonPopWindow() {
mBuilder = new Builder();
}
public static Builder newBuilder() {
if (mBuilder == null) {
mBuilder = new Builder();
}
return mBuilder;
}
/**
* 获取PopupWindow宽度
*
* @return
*/
public int getWidth() {
if (mPopupWindow != null) {
return mContentView.getMeasuredWidth();
}
return 0;
}
/**
* 获取PopupWindow高度
*
* @return
*/
public int getHeight() {
if (mPopupWindow != null) {
return mContentView.getMeasuredHeight();
}
return 0;
}
/**
* 显示在控件的下方
*/
public CommonPopWindow showDownPop(View parent) {
if (parent.getVisibility() == View.GONE) {
mPopupWindow.showAtLocation(parent, Gravity.NO_GRAVITY, 0, 0);
} else {
int[] location = new int[2];
parent.getLocationOnScreen(location);
if (mPopupWindow != null) {
mPopupWindow.showAtLocation(parent, Gravity.NO_GRAVITY, location[0], location[1] + parent.getHeight());
}
}
return this;
}
/**
* 显示在控件的上方
*/
public CommonPopWindow showAsUp(View view) {
if (view.getVisibility() == View.GONE) {
mPopupWindow.showAtLocation(view, Gravity.NO_GRAVITY, 0, 0);
} else {
int[] location = new int[2];
view.getLocationOnScreen(location);
if (mPopupWindow != null) {
mPopupWindow.showAtLocation(view, Gravity.NO_GRAVITY, location[0], location[1] - view.getHeight());
//方式二
// mPopupWindow.showAsDropDown(view, 0, -(getHeight() + view.getMeasuredHeight()));
}
}
return this;
}
/**
* 显示在控件的左边
*/
public CommonPopWindow showAsLeft(View view) {
if (view.getVisibility() == View.GONE) {
mPopupWindow.showAtLocation(view, Gravity.NO_GRAVITY, 0, 0);
} else {
int[] location = new int[2];
view.getLocationOnScreen(location);
if (mPopupWindow != null) {
mPopupWindow.showAtLocation(view, Gravity.NO_GRAVITY, location[0] - getWidth(), location[1]);
}
}
return this;
}
/**
* 显示在控件右边
*/
public CommonPopWindow showAsRight(View view) {
if (view.getVisibility() == View.GONE) {
mPopupWindow.showAtLocation(view, Gravity.NO_GRAVITY, 0, 0);
} else {
int[] location = new int[2];
view.getLocationOnScreen(location);
if (mPopupWindow != null) {
mPopupWindow.showAtLocation(view, Gravity.NO_GRAVITY, location[0] + view.getWidth(), location[1]);
}
}
return this;
}
/**
* 显示控件下方
*
* @param view
* @return
*/
public CommonPopWindow showAsDown(View view) {
if (mPopupWindow != null) {
mPopupWindow.showAsDropDown(view);
}
return this;
}
/**
* 全屏弹出
*/
public CommonPopWindow showAsBottom(View view) {
if (view.getVisibility() == View.GONE) {
mPopupWindow.showAtLocation(view, Gravity.NO_GRAVITY, 0, 0);
} else {
int[] location = new int[2];
view.getLocationOnScreen(location);
if (mPopupWindow != null) {
mPopupWindow.showAtLocation(view, Gravity.BOTTOM, 0, 0);
}
}
return this;
}
public CommonPopWindow showAtLocation(View anchor, int gravity, int x, int y) {
if (mPopupWindow != null) {
mPopupWindow.showAtLocation(anchor, gravity, x, y);
}
return this;
}
/**
* 取消
*/
public static void dismiss() {
if (mWindow != null) {
WindowManager.LayoutParams params = mWindow.getAttributes();
params.alpha = 1.0f;
mWindow.setAttributes(params);
}
if (mPopupWindow != null && mPopupWindow.isShowing())
mPopupWindow.dismiss();
}
/*
* ---------------------Builder-------------------------
*/
public static class Builder implements PopupWindow.OnDismissListener {
private Context mContext;
private int mLayoutResId;//布局ID
private int mWidth, mHeight;//弹窗宽高
private int mAnimationStyle;//动画样式
private ViewClickListener mListener;//子View监听回调
private Drawable mDrawable;//背景Drawable
private boolean mTouchable = true;//是否相应touch事件
private boolean mFocusable = true;//是否获取焦点
private boolean mOutsideTouchable = true;//设置外部是否点击
private boolean mBackgroundDarkEnable = false;//是否背景窗体变暗
private float mDarkAlpha = 1.0f;//透明值
public CommonPopWindow build(Context context) {
this.mContext = context;
CommonPopWindow popWindow = new CommonPopWindow();
apply();
if (mListener != null && mLayoutResId != 0) {
mListener.getChildView(mPopupWindow, mContentView, mLayoutResId);
}
return popWindow;
}
private void apply() {
if (mLayoutResId != 0) {
mContentView = LayoutInflater.from(mContext).inflate(mLayoutResId, null);
}
if (mWidth != 0 && mHeight != 0) {
mPopupWindow = new PopupWindow(mContentView, mWidth, mHeight);
} else {
mPopupWindow = new PopupWindow(mContentView, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
}
mPopupWindow.setTouchable(mTouchable);
mPopupWindow.setFocusable(mFocusable);
mPopupWindow.setOutsideTouchable(mOutsideTouchable);
if (mDrawable != null) {
mPopupWindow.setBackgroundDrawable(mDrawable);
} else {
mPopupWindow.setBackgroundDrawable(new ColorDrawable());
}
if (mAnimationStyle != -1) {
mPopupWindow.setAnimationStyle(mAnimationStyle);
}
if (mWidth == 0 || mHeight == 0) {
measureWidthAndHeight(mContentView);
//如果没有设置高度的情况下,设置宽高并赋值
mWidth = mPopupWindow.getContentView().getMeasuredWidth();
mHeight = mPopupWindow.getContentView().getMeasuredHeight();
}
Activity activity = (Activity) mContext;
if (activity != null && mBackgroundDarkEnable) {
float alpha = (mDarkAlpha >= 0f || mDarkAlpha <= 1f) ? mDarkAlpha : 0.7f;
mWindow = activity.getWindow();
WindowManager.LayoutParams params = mWindow.getAttributes();
params.alpha = alpha;
mWindow.setAttributes(params);
}
mPopupWindow.setOnDismissListener(this);
mPopupWindow.update();
}
@Override
public void onDismiss() {
dismiss();
}
/**
* 测量View的宽高
*
* @param mContentView
*/
private void measureWidthAndHeight(View mContentView) {
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.AT_MOST);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.AT_MOST);
mContentView.measure(widthMeasureSpec, heightMeasureSpec);
}
/**
* 设置布局ID
*
* @param layoutResId
* @return
*/
public Builder setView(@LayoutRes int layoutResId) {
mContentView = null;
this.mLayoutResId = layoutResId;
return this;
}
/**
* 设置宽高
*
* @param width
* @param height
* @return
*/
public Builder setSize(int width, int height) {
mWidth = width;
mHeight = height;
return this;
}
/**
* 设置背景
*
* @param drawable
* @return
*/
public Builder setBackgroundDrawable(Drawable drawable) {
mDrawable = drawable;
return this;
}
/**
* 设置背景是否变暗
*
* @param darkEnable
* @return
*/
public Builder setBackgroundDarkEnable(boolean darkEnable) {
mBackgroundDarkEnable = darkEnable;
return this;
}
/**
* 设置背景透明度
*
* @param dackAlpha
* @return
*/
public Builder setBackgroundAlpha(@FloatRange(from = 0.0, to = 1.0) float dackAlpha) {
mDarkAlpha = dackAlpha;
return this;
}
/**
* 是否点击Outside消失
*
* @param touchable
* @return
*/
public Builder setOutsideTouchable(boolean touchable) {
mOutsideTouchable = touchable;
return this;
}
/**
* 是否设置Touch事件
*
* @param touchable
* @return
*/
public Builder setTouchable(boolean touchable) {
mTouchable = touchable;
return this;
}
/**
* 设置动画
*
* @param animationStyle
* @return
*/
public Builder setAnimationStyle(@StyleRes int animationStyle) {
mAnimationStyle = animationStyle;
return this;
}
/**
* 是否设置获取焦点
*
* @param focusable
* @return
*/
public Builder setFocusable(boolean focusable) {
mFocusable = focusable;
return this;
}
/**
* 设置子View点击事件回调
*
* @param listener
* @return
*/
public Builder setViewOnClickListener(ViewClickListener listener) {
this.mListener = listener;
return this;
}
}
}
4.主界面(MainActivity)
public class MainActivity extends AppCompatActivity implements View.OnClickListener,CommonPopWindow.ViewClickListener{
private TextView click;
private TextView text;
private List datasBeanList;
private String categoryName;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
initData();
initListener();
}
private void initListener() {
click.setOnClickListener(this);
}
private void initData() {
//模拟请求后台返回数据
String response = "{\"ret\":0,\"msg\":\"succes,\",\"datas\":[{\"ID\":\" 0\",\"categoryName\":\"\\u793e\\u56e2\",\"state\":\"1\"},{\"ID\":\"1\",\"categoryName\":\"\\u539f\\u521b\",\"state\":\"1\"},{\"ID\":\"2\",\"categoryName\":\"\\u73b0\\u8d27\",\"state\":\"1\"}]}";
GetConfigReq getConfigReq = new Gson().fromJson(response, GetConfigReq.class);
//0请求表示成功
if (getConfigReq.getRet() == 0) {
//滚动选择数据集合
datasBeanList = getConfigReq.getDatas();
}
}
private void initView() {
click = findViewById(R.id.textClick);
text = findViewById(R.id.text);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.textClick:
setAddressSelectorPopup(v);
break;
}
}
/**
* 将选择器放在底部弹出框
* @param v
*/
private void setAddressSelectorPopup(View v) {
int screenHeigh = getResources().getDisplayMetrics().heightPixels;
CommonPopWindow.newBuilder()
.setView(R.layout.pop_picker_selector_bottom)
.setAnimationStyle(R.style.AnimUp)
.setBackgroundDrawable(new BitmapDrawable())
.setSize(ViewGroup.LayoutParams.MATCH_PARENT, Math.round(screenHeigh * 0.3f))
.setViewOnClickListener(this)
.setBackgroundDarkEnable(true)
.setBackgroundAlpha(0.7f)
.setBackgroundDrawable(new ColorDrawable(999999))
.build(this)
.showAsBottom(v);
}
@Override
public void getChildView(final PopupWindow mPopupWindow, View view, int mLayoutResId) {
switch (mLayoutResId) {
case R.layout.pop_picker_selector_bottom:
TextView imageBtn = view.findViewById(R.id.img_guanbi);
PickerScrollView addressSelector = view.findViewById(R.id.address);
// 设置数据,默认选择第一条
addressSelector.setData(datasBeanList);
addressSelector.setSelected(0);
//滚动监听
addressSelector.setOnSelectListener(new PickerScrollView.onSelectListener() {
@Override
public void onSelect(GetConfigReq.DatasBean pickers) {
categoryName = pickers.getCategoryName();
}
});
//完成按钮
imageBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mPopupWindow.dismiss();
text.setText(categoryName);
}
});
break;
}
}
}
5、实体类(GetConfigReq )
public class GetConfigReq {
/**
* ret : 0
* msg : succes,
* datas : [{"ID":" 0","categoryName":"社团","state":"1"},{"ID":"1","categoryName":"原创","state":"1"},{"ID":"2","categoryName":"现货","state":"1"}]
*/
private int ret;
private String msg;
private List datas;
public int getRet() {
return ret;
}
public void setRet(int ret) {
this.ret = ret;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public List getDatas() {
return datas;
}
public void setDatas(List datas) {
this.datas = datas;
}
public static class DatasBean {
/**
* ID : 0
* categoryName : 社团
* state : 1
*/
private String ID;
private String categoryName;
private String state;
public String getID() {
return ID;
}
public void setID(String ID) {
this.ID = ID;
}
public String getCategoryName() {
return categoryName;
}
public void setCategoryName(String categoryName) {
this.categoryName = categoryName;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
}
}
6、底部弹出框布局文件
7、到这里就实现了滚动选择器的功能,主要还是自定义控件的实现。
需要Demo的童鞋底部公众号回复:"滚动选择器"即可获取。
以下是公众号(longxuanzhigu),之后发布的文章会同步到该公众号,方便交流学习Android知识及分享个人爱好文章: