最近在系统学习自定义View这一块的知识,前面几篇基本都是理论知识,这篇博客着重从实战来加强对自定义View的理解与运用。实现的两种效果,分别代表自定义View与自定义ViewGroup。
先上效果图:
上面的是一个可以滑动的刻度尺,支持快速滑动,选择的数字也会显示在下方;下面的是一个经典的流式布局,会根据文字长度自动进行布局。一起看看怎么实现的吧:
一.准备工作
1.布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:myscroll="http://schemas.android.com/apk/res-auto"
android:id="@+id/activity_five"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:orientation="vertical">
<org.tyk.android.artstudy.MySelectView
android:id="@+id/my_selectview"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginTop="50dp"
myscroll:lineColor="@color/font_text"
myscroll:textColor="@color/strong"
myscroll:textSize="20dp"></org.tyk.android.artstudy.MySelectView>
<TextView
android:id="@+id/number_txt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:layout_marginTop="20dp"
android:text="选择的数字为:"
android:textSize="20dp" />
<org.tyk.android.artstudy.MyFlowLayout
android:id="@+id/my_flowlayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="50dp"
android:background="@color/bg_page">
</org.tyk.android.artstudy.MyFlowLayout>
</LinearLayout>
从上到下的线性布局,依次是滑动刻度尺,数字TextView,流式布局,以及设置了一些自定义的属性。
2.自定义滑动刻度尺的初始准备
public MySelectView(Context context) {
this(context, null);
}
public MySelectView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MySelectView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//获取我们自定义的样式属性
TypedArray array = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MySelectView, defStyleAttr, 0);
int n = array.getIndexCount();
for (int i = 0; i < n; i++) {
int attr = array.getIndex(i);
switch (attr) {
case R.styleable.MySelectView_lineColor:
// 默认颜色设置为黑色
lineColor = array.getColor(attr, Color.BLACK);
break;
case R.styleable.MySelectView_textColor:
textColor = array.getColor(attr, Color.BLACK);
break;
case R.styleable.MySelectView_textSize:
// 默认设置为16sp,TypeValue也可以把sp转化为px
textSize = array.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
break;
}
}
array.recycle();
init();
}
public void init() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
bigBound = new Rect();
smallBound = new Rect();
}
public void setmStartWidth(int mStartWidth) {
this.mStartWidth = mStartWidth;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width;
int height;
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else {
width = widthSize * 1 / 2;
}
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else {
height = heightSize * 1 / 2;
}
setMeasuredDimension(width, height);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
mWidth = getWidth();
mHeight = getHeight();
mStartWidth = 0;
}
初始化自定义控件,获取自定义控件的样式属性,初始化相关工具,重写onMeasure()测量自定义控件大小,重写onLayout()获取自定义控件宽高。
二.自定义滑动刻度尺的实现
1.重写onDraw()方法绘制刻度尺
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setColor(lineColor);
//画背景
canvas.drawLine(0, 0, mWidth, 0, mPaint);
canvas.drawLine(0, mHeight, mWidth, mHeight, mPaint);
//画数字
for (int i = 0; i < 1000; i++) {
if (i % 5 == 0) {
mPaint.setColor(textColor);
canvas.drawLine(mStartWidth, 0, mStartWidth, getHeight() / 3, mPaint);
mPaint.setTextSize(textSize);
mPaint.getTextBounds(String.valueOf(i), 0, String.valueOf(i).length(), bigBound);
canvas.drawText(String.valueOf(i), mStartWidth - bigBound.width() / 2, getHeight() / 2 + bigBound.height() * 3 / 4, mPaint);
} else {
mPaint.setColor(lineColor);
mPaint.setTextSize(textSize - 15);
canvas.drawLine(mStartWidth, 0, mStartWidth, getHeight() / 5, mPaint);
mPaint.getTextBounds(String.valueOf(i), 0, String.valueOf(i).length(), smallBound);
canvas.drawText(String.valueOf(i), mStartWidth - smallBound.width() / 2, getHeight() / 2 + smallBound.height() * 3 / 4, mPaint);
}
mStartWidth += mWidth / 10;
}
//画中间刻度线
mPaint.setColor(textColor);
canvas.drawLine(mWidth / 2, 0, mWidth / 2, getHeight() / 3, mPaint);
}
绘制背景的两条实线,绘制中间的数字,绘制中间的刻度线。
2.重写onTouchEvent()方法处理滑动事件
@Override
public boolean onTouchEvent(MotionEvent event) {
if (velocityTracker == null) {
velocityTracker = VelocityTracker.obtain();
}
velocityTracker.addMovement(event);
int x = (int) event.getX();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
xDown = x;
break;
case MotionEvent.ACTION_MOVE:
xMove = x;
mStartWidth = xScroll + (xMove - xDown);
invalidate();
int numberScroll = (int) Math.round(Double.valueOf(mStartWidth) / Double.valueOf(mWidth / 10));
listener.getNumber(Math.abs(numberScroll - 5));
break;
case MotionEvent.ACTION_UP:
xUp = x;
xScroll = xScroll + (xUp - xDown);
//处理快速滑动
velocityTracker.computeCurrentVelocity(1000);
int scrollX = (int) velocityTracker.getXVelocity();
xScroll = xScroll + scrollX;
ValueAnimator walkAnimator = ValueAnimator.ofInt(mStartWidth, xScroll);
walkAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mStartWidth = (int) animation.getAnimatedValue();
invalidate();
}
});
walkAnimator.setDuration(500);
walkAnimator.start();
walkAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
//处理惯性滑动
int endX = xScroll % (mWidth / 10);
if (Math.abs(endX) < mWidth / 20) {
xScroll = xScroll - endX;
mStartWidth = xScroll;
invalidate();
} else {
xScroll = xScroll + (Math.abs(endX) - mWidth / 10);
mStartWidth = xScroll;
invalidate();
}
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
int number = (int) Math.round(Double.valueOf(xScroll) / Double.valueOf(mWidth / 10));
listener.getNumber(Math.abs(number - 5));
break;
}
return true;
}
这一块是整个自定义滑动刻度尺的重点,慢慢分析一下:
初始化VelocityTracker,并且把要追踪的MotionEvent注册到VelocityTracker的监听中,用来跟踪触摸屏事件,主要用来处理滑动刻度尺的快速滑动。
MotionEvent.ACTION_DOWN: 获取水平方向X的坐标
MotionEvent.ACTION_MOVE: 获取水平方向滑动的距离,然后不断改变绘制的开始位置,再调用invalidate()来进行重绘,达到滑动的效果。后面两句代码是为了让下面显示的数字能够实时更新,接口回调。
MotionEvent.ACTION_UP:
1.获取滑动到总距离。
2.处理快速滑动,首先获取1秒内X方向所滑动像素值,然后确定最终滑动的位置。通过一个属性动画,不断改变绘制的开始位置,再调用invalidate()来进行重绘,达到快速滑动的效果。
3.处理惯性滑动,仔细查看效果图你会发现,当最后滑动的终点位置不足一半时,会自动滑动到前一个位置;当最后滑动的终点位置超过一半时,会自动滑动到下一个位置。这里其实就是在动画结束的时候,进行判断,然后调用invalidate()来进行重绘,达到惯性滑动的效果。
4.最后两句代码是为了让下面显示的数字最终能够实时更新,接口回调。
三.自定义流式布局的实现
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
// 计算出所有的childView的宽和高
for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
}
setMeasuredDimension(sizeWidth, sizeHeight);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int left = 0;
int top = 0;
for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);
MarginLayoutParams params = (MarginLayoutParams) childView.getLayoutParams();
int lc = left + params.leftMargin;
int rc = childView.getMeasuredWidth() + lc;
int tc = top + params.topMargin;
int bc = childView.getMeasuredHeight() + tc;
childView.layout(lc, tc, rc, bc);
//超过宽度则换行
if (rc + childView.getMeasuredWidth() > getMeasuredWidth()) {
left = 0;
top = bc;
} else {
left = rc;
}
}
}
1.重写generateLayoutParams()方法返回MarginLayoutParams的实例,使自定义的流式布局能够支持margin属性
2.重写onDraw()方法计算出所有的childView的宽和高以及测量模式,并且设置自己的宽高
3.重写onLayout()方法对所有childView进行定位(设置childView的绘制区域),并且根据childView的宽度进行自动换行。
具体使用:
public void init() {
stringList.add("数据库");
stringList.add("移动开发");
stringList.add("前端开发");
stringList.add("微信小程序");
stringList.add("服务器开发");
stringList.add("PHP");
stringList.add("人工智能");
stringList.add("大数据");
mySelectView = (MySelectView) findViewById(R.id.my_selectview);
myFlowLayout = (MyFlowLayout) findViewById(R.id.my_flowlayout);
for (String textView : stringList) {
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
params.setMargins(40, 40, 40, 40);
TextView showText = new TextView(this);
showText.setLayoutParams(params);
showText.setTextColor(getResources().getColor(R.color.text_color));
showText.setTextSize(20);
showText.setText(textView);
showText.setBackground(getResources().getDrawable(R.drawable.flag_01));
myFlowLayout.addView(showText);
}
numberTxt = (TextView) findViewById(R.id.number_txt);
mySelectView.setListener(this);
}
将需要设置的文字动态添加到我们的流式布局中去即可,自定义的流式布局会自动根据添加文字的大小进行布局,达到最后的效果。
关于自定义View的一些细节可以参考之前的博客:
下一篇自定义View再见~~~
源码地址:
欢迎star,fork,提issues,一起进步!