自定义view就像应用题:看着很长,其实很简单,因为越长的题目就给你越多的信息。
很多天气应用都有这个效果,首先看一下效果图
可以自定义显示数目的多少和总数目。效果很简单,主要是一起复习一下自定义View的一些东西
首先是自定义我们的属性,默认是一屛六个,一共十个格子
在res-values-attrs文件夹里,name就是你的自定义view的名字,part是一屛的格子的个数,total是总个数
<declare-styleable name="myView">
<attr name="part" format="integer" />
<attr name="total" format="integer" />
</declare-styleable>
在XML里使用:
在studio 3.0里会自动帮你引入: xmlns:app=”http://schemas.android.com/apk/res-auto”
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="false">
<com.test1.myView.myView
android:id="@+id/myview"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:part="6"
app:total="10"/>
</HorizontalScrollView>
代码也很简单:
首先先确定要用到的方法,onMeasure – onDraw
因为用到了自定义属性 所以构造方法要选择有AttributeSet这个参数的。
第一步:
在init方法里,获取自定义的两个属性:
integer类型的可以设置默认值,所以在layout文件不设置也没事。
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.myView);
part = typedArray.getInteger(R.styleable.myView_part, 6);
count = typedArray.getInteger(R.styleable.myView_total, 10);
然后初始化你要用到的画笔,我们细分一下画笔的个数:
- 画上下两条线的,画圆点的,可以共用一个
- 画文字的
- 画格子的分割线的
- 画点击效果的
然后在你的init函数里,初始化你的画笔。为什么不在onDraw呢,因为这个方法可能会被调用很多次。下面仅仅是你初始化的时候,然后当你调用postInvalidate方法的时候,还会调用onDraw方法。
12-15 15:28:53.030 14371-14371/com.test1 E/1234: onCreat
12-15 15:28:53.070 14371-14371/com.test1 E/1234: onMeasure
12-15 15:28:53.090 14371-14371/com.test1 E/1234: onDraw
12-15 15:28:53.152 14371-14371/com.test1 E/1234: onMeasure
12-15 15:28:53.152 14371-14371/com.test1 E/1234: onDraw
因为我的数据是自己编的,所以也放到了这里,实际应用中你可以写个set方法,但是注意的是,set的值在OnCreate里是获取不到的,因为你到时候设置的时候,代码应该是这样的:
myview = (myView) findViewById(R.id.myview);
myview.setList(你的参数);
你的set函数是执行在初始化以后的,但是这并不妨碍你的使用,因为在onDraw方法里,这个值就已经获取到了。
然后就是你的onMeasure方法了,一般来讲我们只关注是否MeasureSpec.AT_MOST就可以了,是的话我们给默认值,或者是你获取的父控件能给的最大值;否的话给XML里的值就行了。
然后就是在onDraw方法里把你的图形给画出来就行了,就是画点,划线,画文字什么的,这个很简单。
最后就是给设置点击事件了,在这里要注意,什么情况按下,什么情况是按下以后横向滑动不触发点击事件,什么时候是触发点击事件,还有就是获取点击的位置,绘制点击效果。
因为我是直接在mainActivity里调用的,下面的代码就是在mainActivity里写的,实际应用中,建议用自定义的HorizontalScrollView包裹一下,然后把mainActivity的代码,转移到HorizontalScrollView里面。
private float mDownY, mDownX;
boolean isPress=false;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
// 移动的起点
mDownX = ev.getX();
mDownY = ev.getY();
if (isTouchPointInView(myview, (int) mDownX, (int) mDownY)) {
isPress=true;
int[] location = new int[2];
myview.getLocationOnScreen(location);
int left = location[0];
myview.performTouched((int) mDownX, (int) mDownY,left);
}
break;
case MotionEvent.ACTION_MOVE:
if (Math.abs((int) (ev.getX()-mDownX))>10) {//滑动大于10就判断是滑动不是点击效果
isPress=false;
myview.cancleTouched(isPress);
}
break;
case MotionEvent.ACTION_UP:
if (isPress){
myview.cancleTouched(isPress);
isPress=false;
}
break;
}
return super.dispatchTouchEvent(ev);
}
//(x,y)是否在view的区域内
private boolean isTouchPointInView(View view, int x, int y) {
if (view == null) {
return false;
}
int[] location = new int[2];
view.getLocationOnScreen(location);
int left = location[0];
int top = location[1];
int right = left + view.getMeasuredWidth();
int bottom = top + view.getMeasuredHeight();
if (y >= top && y <= bottom && x >= left
&& x <= right) {
return true;
}
return false;
}
意思就是:判断是否点击区域在view区域内,是的话计算点击位置然后绘制点击效果。
下面是绘制点击效果:其中的left值是当你超出一屛的宽度的时候,left就是你超出的宽度。 触发的点击事件你可以根据实际项目需求,来定义一个接口,在你的实际应用中调用。
//设置点击效果,获取点击位置position
public void performTouched(int X, int Y, int left) {//传入点击区域的X和Y的位置,left是超出一屛的宽度
isPress = true;
for (int i = 0; i < count; i++) {
if (X > viewWidth * i - Math.abs(left) && X < viewWidth * (i + 1) - Math.abs(left)) {
posion = i;
}
}
postInvalidate();
}
//取消点击效果
public void cancleTouched(boolean press) {//取消
isPress = false;
if(press){//如果是点击而不是滑动,出发点击事件。
Toast.makeText(getContext(), "点击了" + posion, Toast.LENGTH_SHORT).show();
}
posion = -1;
postInvalidate();
}
在你的onDraw方法里:
if (isPress) {//绘制点击区域变色
RectF rect = new RectF(viewWidth * posion, 0, viewWidth * (posion + 1), myViewH);
canvas.drawRect(rect, paintOfReact);//使用RectF构造
}
总体来说:这是一个很简单的实现,都是一些简单应用,自定义属性等常用方法。
缺点:
- 绘制高低点的时候,没有考虑超出范围
- 没有考虑连线的美观,感兴趣的可以用二阶贝赛尔曲线来让两点之间的连线看上去更顺滑一点
- 没有考虑宽度不足一屛的时候,我感觉用不到。。。
下面提出完整代码:
package com.test1.myView;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
import com.test1.R;
import java.util.ArrayList;
import java.util.List;
public class myView extends View {
Paint paint, paintOfLine1, painOfText, paintOfReact;
int screenW = GetScreenSize.getScreenWidth(getContext());
int myViewW, myViewH;//控件的实际宽高
int count,part;//一屛分成六份
int viewWidth;//每一份的宽度
List<WenDu> list;
int margin = 50;//文字距离圆点的位置
boolean isPress = false;//判断点击,是否绘制点击效果
int posion = -1;//绘制点击效果的子view的位置
public myView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context,attrs);
}
public myView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context,attrs);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.BLUE);
for (int i = 0; i < count; i++) {
canvas.drawLine(viewWidth * (i + 1), 0, viewWidth * (i + 1), myViewH, paintOfLine1);//画竖线
if (i != count - 1) {
paint.setColor(Color.YELLOW);
canvas.drawLine(viewWidth * i + viewWidth / 2, myViewH / 2 - list.get(i).getMax(), viewWidth * (i + 1) + viewWidth / 2, myViewH / 2 - list.get(i + 1).getMax(), paint);//画高连接线,位置大小为基准线上下位置
paint.setColor(Color.WHITE);
canvas.drawLine(viewWidth * i + viewWidth / 2, myViewH / 2 - list.get(i).getMin(), viewWidth * (i + 1) + viewWidth / 2, myViewH / 2 - list.get(i + 1).getMin(), paint);//画低连接线
}
canvas.drawCircle(viewWidth * i + viewWidth / 2, myViewH / 2 - list.get(i).getMax(), 20, paint);//画高圆点
canvas.drawCircle(viewWidth * i + viewWidth / 2, myViewH / 2 - list.get(i).getMin(), 20, paint);//画低圆点
canvas.drawText(String.valueOf(list.get(i).getMax()), viewWidth * i + viewWidth / 2, myViewH / 2 - list.get(i).getMax() - margin, painOfText);//最高
canvas.drawText(String.valueOf(list.get(i).getMin()), viewWidth * i + viewWidth / 2, myViewH / 2 - list.get(i).getMin() + margin, painOfText);//最低
}
if (isPress) {//绘制点击区域变色
RectF rect = new RectF(viewWidth * posion, 0, viewWidth * (posion + 1), myViewH);
canvas.drawRect(rect, paintOfReact);//使用RectF构造
}
}
public void performTouched(int X, int Y, int left) {//传入点击区域的X和Y的位置,left是超出一屛的宽度
isPress = true;
for (int i = 0; i < count; i++) {
if (X > viewWidth * i - Math.abs(left) && X < viewWidth * (i + 1) - Math.abs(left)) {
posion = i;
}
}
postInvalidate();
}
public void cancleTouched(boolean press) {//取消
isPress = false;
if(press){
Toast.makeText(getContext(), "点击了" + posion, Toast.LENGTH_SHORT).show();
}
posion = -1;
postInvalidate();
}
private void init(Context context,AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.myView);
part = typedArray.getInteger(R.styleable.myView_part, 6);
count = typedArray.getInteger(R.styleable.myView_total, 10);
viewWidth= screenW / part;
paint = new Paint();//画线
paint.setColor(Color.WHITE);
paint.setStrokeWidth(10);
paint.setStyle(Paint.Style.FILL);
paintOfLine1 = new Paint();//画竖线
paintOfLine1.setColor(Color.parseColor("#33ffffff"));
paintOfLine1.setStrokeWidth(2);
list = new ArrayList<>();
for (int i = 0; i < count; i++) {
int max = (int) Math.round(Math.random() * 100);
int min = -max;
list.add(new WenDu(max, min));
}
painOfText = new Paint();
painOfText.setColor(Color.WHITE);
painOfText.setTextSize(30);
painOfText.setStrokeWidth(5);
painOfText.setStyle(Paint.Style.FILL);
painOfText.setTextAlign(Paint.Align.CENTER);
paintOfReact = new Paint();
paintOfReact.setColor(Color.YELLOW);
paintOfReact.setAlpha(50);
paintOfReact.setStyle(Paint.Style.FILL_AND_STROKE);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
//如果宽高都是warp_content时,设置控件的宽高的大小,warp的时候,宽度默认是计算的总宽度,高度默认600
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(viewWidth * count, 600);
myViewH=600;
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(viewWidth * count, heightSpecSize);
myViewH=heightSpecSize;
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(viewWidth * count, 600);
myViewH=600;
} else {//如果是指定高度的时候,或者是match的时候
setMeasuredDimension(viewWidth * count, heightSpecSize);
myViewH=heightSpecSize;
}
myViewW=viewWidth * count;
}
}