├── .gitignore
├── README.md
├── build.gradle
├── customview01.iml
├── proguard-rules.pro
└── src
├── androidTest
└── java
│ └── sflin
│ └── open
│ └── customview01
│ └── ExampleInstrumentationTest.java
├── main
├── AndroidManifest.xml
├── java
│ └── sflin
│ │ └── open
│ │ └── customview01
│ │ ├── CheckActivity.java
│ │ ├── CheckView.java
│ │ ├── MainActivity.java
│ │ ├── MyQQSport.java
│ │ ├── MyQQSportActivity.java
│ │ ├── ZombieActivity.java
│ │ └── ZombieView.java
└── res
│ ├── layout
│ ├── activity_check.xml
│ ├── activity_main.xml
│ ├── activity_myqqsport.xml
│ └── activity_zombie.xml
│ ├── mipmap-hdpi
│ ├── check.png
│ ├── ic_launcher.png
│ ├── icon_head.jpeg
│ └── zombie.png
│ ├── mipmap-mdpi
│ └── ic_launcher.png
│ ├── mipmap-xhdpi
│ └── ic_launcher.png
│ ├── mipmap-xxhdpi
│ └── ic_launcher.png
│ ├── mipmap-xxxhdpi
│ └── ic_launcher.png
│ ├── values-w820dp
│ └── dimens.xml
│ └── values
│ ├── atrs.xml
│ ├── colors.xml
│ ├── dimens.xml
│ ├── strings.xml
│ └── styles.xml
└── test
└── java
└── sflin
└── open
└── customview01
└── ExampleUnitTest.java
/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | **自定义View对于Android程序猿来说都应该了解的,或者说一定要掌握。虽然Google提供的控件可以满足日常开发需求,但是总有些奇葩需求肯定是满足不了的,所以对于自定义View的开发,就必不可少了。**
3 |
4 | >关于自定义View的基础学习推荐[GcsSloop自定义View系列](https://github.com/GcsSloop/AndroidNote/tree/master/CustomView)
5 |
6 | ## 首先看下面效果图
7 | 
8 |
9 | >实现上图的效果参考[这篇文章](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B4%5DCanvas_PictureText.md)
10 |
11 | ## 再来这个效果图
12 | 
13 | >上面效果图是根据第一个Demo的扩展,具体源码会在文章结尾提供链接
14 |
15 | ## 最后一个效果图
16 | 
17 | >这个自定义View是模仿QQ运动的那个界面
18 |
19 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 23
5 | buildToolsVersion "24.0.0"
6 |
7 | defaultConfig {
8 | applicationId "sflin.open.customview01"
9 | minSdkVersion 15
10 | targetSdkVersion 24
11 | versionCode 1
12 | versionName "1.0"
13 |
14 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
15 |
16 | }
17 | buildTypes {
18 | release {
19 | minifyEnabled false
20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
21 | }
22 | }
23 | }
24 |
25 | dependencies {
26 | compile fileTree(include: ['*.jar'], dir: 'libs')
27 | compile 'com.android.support:appcompat-v7:24.0.0'
28 | compile 'com.android.support.constraint:constraint-layout:1.0.0-alpha1'
29 | testCompile 'junit:junit:4.12'
30 | androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2'
31 | androidTestCompile 'com.android.support.test:runner:0.5'
32 | androidTestCompile 'com.android.support:support-annotations:24.0.0'
33 | }
34 |
--------------------------------------------------------------------------------
/customview01.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | generateDebugSources
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
--------------------------------------------------------------------------------
/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in E:\Android\sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
--------------------------------------------------------------------------------
/src/androidTest/java/sflin/open/customview01/ExampleInstrumentationTest.java:
--------------------------------------------------------------------------------
1 | package sflin.open.customview01;
2 |
3 | import android.content.Context;
4 | import android.support.test.InstrumentationRegistry;
5 | import android.support.test.filters.MediumTest;
6 | import android.support.test.runner.AndroidJUnit4;
7 |
8 | import org.junit.Test;
9 | import org.junit.runner.RunWith;
10 |
11 |
12 | import static org.junit.Assert.*;
13 |
14 | /**
15 | * Instrumentation test, which will execute on an Android device.
16 | *
17 | * @see Testing documentation
18 | */
19 | @MediumTest
20 | @RunWith(AndroidJUnit4.class)
21 | public class ExampleInstrumentationTest {
22 | @Test
23 | public void useAppContext() throws Exception {
24 | // Context of the app under test.
25 | Context appContext = InstrumentationRegistry.getTargetContext();
26 |
27 | assertEquals("sflin.open.customview01", appContext.getPackageName());
28 | }
29 | }
--------------------------------------------------------------------------------
/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/main/java/sflin/open/customview01/CheckActivity.java:
--------------------------------------------------------------------------------
1 | package sflin.open.customview01;
2 |
3 | import android.os.Bundle;
4 | import android.support.v7.app.AppCompatActivity;
5 | import android.view.View;
6 |
7 | /**
8 | * Created by a9951 on 2016/6/22.
9 | */
10 |
11 | public class CheckActivity extends AppCompatActivity {
12 |
13 | private CheckView mCheckView;
14 |
15 | @Override
16 | protected void onCreate(Bundle savedInstanceState) {
17 | super.onCreate(savedInstanceState);
18 | setContentView(R.layout.activity_check);
19 | mCheckView = (CheckView) findViewById(R.id.custom_check);
20 | }
21 |
22 | public void check(View view){
23 | mCheckView.check();
24 | }
25 |
26 | public void unCheck(View view){
27 | mCheckView.unCheck();
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/sflin/open/customview01/CheckView.java:
--------------------------------------------------------------------------------
1 | package sflin.open.customview01;
2 |
3 | import android.content.Context;
4 | import android.graphics.Bitmap;
5 | import android.graphics.BitmapFactory;
6 | import android.graphics.Canvas;
7 | import android.graphics.Paint;
8 | import android.graphics.Rect;
9 | import android.os.Handler;
10 | import android.os.Message;
11 | import android.util.AttributeSet;
12 | import android.util.Log;
13 | import android.view.View;
14 |
15 | /**
16 | * Created by a9951 on 2016/6/22.
17 | */
18 |
19 | public class CheckView extends View {
20 |
21 | private static final int ANIM_NULL = 0; //动画状态-没有
22 | private static final int ANIM_CHECK = 1; //动画状态-开启
23 | private static final int ANIM_UNCHECK = 2; //动画状态-结束
24 |
25 | private Context mContext; // 上下文
26 | private int mWidth, mHeight; // 宽高
27 | private Handler mHandler; // handler
28 |
29 | private Paint mPaint;
30 | private Bitmap okBitmap;
31 |
32 | private int animCurrentPage = -1; // 当前页码
33 | private int animMaxPage = 13; // 总页数
34 | private int animDuration = 500; // 动画时长
35 | private int animState = ANIM_NULL; // 动画状态
36 |
37 | private boolean isCheck = false; // 是否只选中状态
38 |
39 | public CheckView(Context context) {
40 | super(context, null);
41 |
42 | }
43 |
44 | public CheckView(Context context, AttributeSet attrs) {
45 | super(context, attrs);
46 | init(context);
47 | }
48 |
49 | /**
50 | * 初始化
51 | * @param context
52 | */
53 | private void init(Context context) {
54 | mContext = context;
55 |
56 | mPaint = new Paint();
57 | mPaint.setColor(0xffFF5317);
58 | mPaint.setStyle(Paint.Style.FILL);
59 | mPaint.setAntiAlias(true);
60 |
61 | okBitmap = BitmapFactory.decodeResource(mContext.getResources(), R.mipmap.check);
62 |
63 | mHandler = new Handler() {
64 | @Override
65 | public void handleMessage(Message msg) {
66 | super.handleMessage(msg);
67 | if (animCurrentPage < animMaxPage && animCurrentPage >= 0) {
68 | invalidate();
69 | if (animState == ANIM_NULL)
70 | return;
71 | if (animState == ANIM_CHECK) {
72 |
73 | animCurrentPage++;
74 | } else if (animState == ANIM_UNCHECK) {
75 | animCurrentPage--;
76 | }
77 |
78 | this.sendEmptyMessageDelayed(0, animDuration / animMaxPage);
79 | Log.e("AAA", "Count=" + animCurrentPage);
80 | } else {
81 | if (isCheck) {
82 | animCurrentPage = animMaxPage - 1;
83 | } else {
84 | animCurrentPage = -1;
85 | }
86 | invalidate();
87 | animState = ANIM_NULL;
88 | }
89 | }
90 | };
91 | }
92 |
93 |
94 | /**
95 | * View大小确定
96 | * @param w
97 | * @param h
98 | * @param oldw
99 | * @param oldh
100 | */
101 | @Override
102 | protected void onSizeChanged(int w, int h, int oldw, int oldh) {
103 | super.onSizeChanged(w, h, oldw, oldh);
104 | mWidth = w;
105 | mHeight = h;
106 | }
107 |
108 | /**
109 | * 绘制内容
110 | * @param canvas
111 | */
112 | @Override
113 | protected void onDraw(Canvas canvas) {
114 | super.onDraw(canvas);
115 |
116 | // 移动坐标系到画布中央
117 | canvas.translate(mWidth / 2, mHeight / 2);
118 |
119 | // 绘制背景圆形
120 | canvas.drawCircle(0, 0, 240, mPaint);
121 |
122 | // 得出图像边长
123 | int sideLength = okBitmap.getHeight();
124 |
125 | // 得到图像选区 和 实际绘制位置
126 | Rect src = new Rect(sideLength * animCurrentPage, 0, sideLength * (animCurrentPage + 1), sideLength);
127 | Rect dst = new Rect(-200, -200, 200, 200);
128 |
129 | // 绘制
130 | canvas.drawBitmap(okBitmap, src, dst, null);
131 | }
132 |
133 |
134 | /**
135 | * 选择
136 | */
137 | public void check() {
138 | if (animState != ANIM_NULL || isCheck)
139 | return;
140 | animState = ANIM_CHECK;
141 | animCurrentPage = 0;
142 | mHandler.sendEmptyMessageDelayed(0, animDuration / animMaxPage);
143 | isCheck = true;
144 | }
145 |
146 | /**
147 | * 取消选择
148 | */
149 | public void unCheck() {
150 | if (animState != ANIM_NULL || (!isCheck))
151 | return;
152 | animState = ANIM_UNCHECK;
153 | animCurrentPage = animMaxPage - 1;
154 | mHandler.sendEmptyMessageDelayed(0, animDuration / animMaxPage);
155 | isCheck = false;
156 | }
157 |
158 | /**
159 | * 设置动画时长
160 | * @param animDuration
161 | */
162 | public void setAnimDuration(int animDuration) {
163 | if (animDuration <= 0)
164 | return;
165 | this.animDuration = animDuration;
166 | }
167 |
168 | /**
169 | * 设置背景圆形颜色
170 | * @param color
171 | */
172 | public void setBackgroundColor(int color){
173 | mPaint.setColor(color);
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/src/main/java/sflin/open/customview01/MainActivity.java:
--------------------------------------------------------------------------------
1 | package sflin.open.customview01;
2 |
3 | import android.content.Intent;
4 | import android.os.Bundle;
5 | import android.support.annotation.Nullable;
6 | import android.support.v7.app.AppCompatActivity;
7 | import android.view.View;
8 |
9 | /**
10 | * Created by a9951 on 2016/6/22.
11 | */
12 |
13 | public class MainActivity extends AppCompatActivity {
14 |
15 | @Override
16 | protected void onCreate(@Nullable Bundle savedInstanceState) {
17 | super.onCreate(savedInstanceState);
18 | setContentView(R.layout.activity_main);
19 | }
20 |
21 | public void CheckView(View view){
22 | startActivity(new Intent(this,CheckActivity.class));
23 | }
24 |
25 | public void Zombie(View view){
26 | startActivity(new Intent(this,ZombieActivity.class));
27 | }
28 |
29 | public void MyQQSport(View view){
30 | startActivity(new Intent(this,MyQQSportActivity.class));
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/java/sflin/open/customview01/MyQQSport.java:
--------------------------------------------------------------------------------
1 | package sflin.open.customview01;
2 |
3 | import android.animation.AnimatorSet;
4 | import android.animation.ValueAnimator;
5 | import android.content.Context;
6 | import android.content.res.TypedArray;
7 | import android.graphics.Bitmap;
8 | import android.graphics.BitmapFactory;
9 | import android.graphics.BitmapShader;
10 | import android.graphics.Canvas;
11 | import android.graphics.Color;
12 | import android.graphics.DashPathEffect;
13 | import android.graphics.Paint;
14 | import android.graphics.Path;
15 | import android.graphics.Rect;
16 | import android.graphics.RectF;
17 | import android.graphics.Shader;
18 | import android.util.AttributeSet;
19 | import android.view.View;
20 |
21 | /**
22 | * Created by a9951 on 2016/6/20.
23 | */
24 |
25 | public class MyQQSport extends View {
26 |
27 | //总步数,好友平均步数,排名,当前时间,每天平均步数
28 | private int totalText,freAvgText,rankText,avgText;
29 | private String nowTime;
30 | private int[] dayStepNum;//步数
31 | private String[] dates;//最近7日
32 | private float percent;//外圆弧百分比
33 | //文字颜色
34 | private int textColor;
35 | //内外圈的颜色
36 | private int outCircleColor;
37 | private int inCircleColor;
38 | //头像
39 | private Bitmap headBitmap;
40 |
41 | //宽高,中心点,字体大小
42 | private int width,height;
43 | private float centerX,centerY,textGraySize,textSmallSize,textBigSize,arcStrokeWidth;
44 |
45 | //宽高比例
46 | private float heightScale = 1/6f;
47 | private float widthScale = 0.25f;
48 |
49 | private RectF mRect;//圆弧所需的矩形
50 | private Paint textSmallPaint;//排名的画笔
51 | private Paint textBigPaint;//总步数的画笔
52 | private Paint textGrayPaint;//其他字的画笔
53 | private Paint outSideLinePaint;//外圆弧的画笔
54 | private Paint inSideLinePaint;//内圆弧的画笔
55 | private Paint barPaint;//竖条画笔
56 | private Paint backgroundPaint;//背景画笔
57 | private Paint dashLinePaint;//虚线画笔
58 |
59 | //计步动画,圆弧动画
60 | private ValueAnimator mStepAnimator;
61 | private ValueAnimator mArcAnimator;
62 |
63 | public MyQQSport(Context context) {
64 | super(context);
65 | }
66 |
67 | public MyQQSport(Context context, AttributeSet attrs) {
68 | super(context, attrs);
69 | initAttr(context,attrs);
70 | //init(context);
71 | }
72 |
73 | //初始化配置参数
74 | public void initAttr(Context context, AttributeSet attrs){
75 | TypedArray typedArray = context.obtainStyledAttributes(attrs,R.styleable.MyQQSport);
76 | textColor = typedArray.getColor(R.styleable.MyQQSport_textColor, Color.parseColor("#FF8ED0FF"));
77 | outCircleColor = typedArray.getColor(R.styleable.MyQQSport_outCircleColor, Color.parseColor("#FF8ED0FF"));
78 | inCircleColor = typedArray.getColor(R.styleable.MyQQSport_inCircleColor, Color.parseColor("#FFD3EAFA"));
79 | typedArray.recycle();
80 | init(context);
81 | }
82 |
83 | //数据初始化
84 | private void init(Context context){
85 | //圆弧所需的矩形
86 | mRect = new RectF();
87 | //其他字的画笔
88 | textGrayPaint = new Paint();
89 | textGrayPaint.setAntiAlias(true);
90 | textGrayPaint.setStyle(Paint.Style.FILL);
91 | textGrayPaint.setColor(0xFFC6C1C3);
92 | textGrayPaint.setTextAlign(Paint.Align.CENTER);
93 | //排名的画笔
94 | textSmallPaint = new Paint();
95 | textSmallPaint.setAntiAlias(true);
96 | textSmallPaint.setStyle(Paint.Style.FILL);
97 | textSmallPaint.setColor(textColor);
98 | textSmallPaint.setTextAlign(Paint.Align.CENTER);
99 | //总步数的画笔
100 | textBigPaint = new Paint();
101 | textBigPaint.setAntiAlias(true);
102 | textBigPaint.setStyle(Paint.Style.FILL);
103 | textBigPaint.setColor(textColor);
104 | textBigPaint.setTextAlign(Paint.Align.CENTER);
105 | //外圆弧的画笔
106 | outSideLinePaint = new Paint();
107 | outSideLinePaint.setAntiAlias(true);
108 | outSideLinePaint.setStyle(Paint.Style.STROKE);
109 | outSideLinePaint.setColor(outCircleColor);
110 | outSideLinePaint.setStrokeCap(Paint.Cap.ROUND);
111 | //内圆弧的画笔
112 | inSideLinePaint = new Paint();
113 | inSideLinePaint.setAntiAlias(true);
114 | inSideLinePaint.setStyle(Paint.Style.STROKE);
115 | inSideLinePaint.setColor(inCircleColor);
116 | inSideLinePaint.setStrokeCap(Paint.Cap.ROUND);
117 | //竖条画笔
118 | barPaint = new Paint();
119 | barPaint.setAntiAlias(true);
120 | barPaint.setStyle(Paint.Style.FILL);
121 | barPaint.setColor(outCircleColor);
122 | barPaint.setStrokeCap(Paint.Cap.ROUND);
123 | //背景画笔
124 | backgroundPaint = new Paint();
125 | barPaint.setAntiAlias(true);
126 | barPaint.setStyle(Paint.Style.FILL);
127 | //虚线画笔
128 | dashLinePaint = new Paint();
129 | dashLinePaint.setAntiAlias(true);
130 | dashLinePaint.setColor(Color.parseColor("#C1C1C1"));
131 | dashLinePaint.setStyle(Paint.Style.STROKE);
132 | dashLinePaint.setPathEffect(new DashPathEffect(new float[]{8, 4}, 0));//画虚线
133 | //头像
134 | headBitmap = BitmapFactory.decodeResource(context.getResources(),R.mipmap.icon_head);
135 | }
136 |
137 |
138 | @Override
139 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
140 | // super.onMeasure(widthMeasureSpec,heightMeasureSpec);
141 | setMeasuredDimension(getWidthMeasure(widthMeasureSpec),getHeightMeasure(heightMeasureSpec));
142 | }
143 |
144 | //测量高度
145 | private int getHeightMeasure(int measureSpec){
146 | int result = getSuggestedMinimumHeight();
147 | int specMode = MeasureSpec.getMode(measureSpec);
148 | int specSize = MeasureSpec.getSize(measureSpec);
149 | switch (specMode){
150 | case MeasureSpec.UNSPECIFIED:
151 | break;
152 | case MeasureSpec.EXACTLY:
153 | case MeasureSpec.AT_MOST:
154 | result = (int) (specSize*0.7);
155 | break;
156 | }
157 | return result;
158 | }
159 |
160 | //测量宽度
161 | private int getWidthMeasure(int measureSpec){
162 | int result = getSuggestedMinimumWidth();
163 | int specMode = MeasureSpec.getMode(measureSpec);
164 | int specSize = MeasureSpec.getSize(measureSpec);
165 | switch (specMode){
166 | case MeasureSpec.UNSPECIFIED:
167 | break;
168 | case MeasureSpec.EXACTLY:
169 | case MeasureSpec.AT_MOST:
170 | result = (int) (specSize*0.9);
171 | break;
172 | }
173 | return result;
174 | }
175 |
176 | //计算所需各个数值大小
177 | @Override
178 | protected void onSizeChanged(int w, int h, int oldw, int oldh) {
179 | super.onSizeChanged(w, h, oldw, oldh);
180 | width = w;
181 | height = h;
182 | centerX = 0;
183 | centerY = -height*heightScale;
184 | mRect = new RectF(-width*widthScale,-width*widthScale+centerY,width*widthScale,width*widthScale+centerY);
185 | arcStrokeWidth = width/200f*6;
186 | textGraySize = width/200f*7;
187 | textSmallSize = width/200f*8;
188 | textBigSize = textSmallSize*3;
189 | }
190 |
191 | @Override
192 | protected void onDraw(Canvas canvas) {
193 | super.onDraw(canvas);
194 | //设置各个paint所需的大小
195 | textGrayPaint.setTextSize(textGraySize);
196 | textSmallPaint.setTextSize(textSmallSize);
197 | textBigPaint.setTextSize(textBigSize);
198 | outSideLinePaint.setStrokeWidth(arcStrokeWidth);
199 | inSideLinePaint.setStrokeWidth(arcStrokeWidth);
200 | barPaint.setStrokeWidth(arcStrokeWidth);
201 |
202 | paintBelowBackground(canvas);//画下层背景
203 | paintUpBackground(canvas);//画上层背景
204 | paintDashLine(canvas);//画虚线
205 | paintBar(canvas);//画近七天的竖条
206 | paintBottom(canvas);
207 |
208 | canvas.translate(width/2,height/2); //移动坐标原点到中间
209 | //画圆弧
210 | Path path = new Path();
211 | path.addArc(mRect,120,300);
212 | canvas.drawPath(path,inSideLinePaint);
213 | path.reset();
214 | path.addArc(mRect,120,300*percent);
215 | canvas.drawPath(path,outSideLinePaint);
216 | //圆弧间的文字
217 | textGrayPaint.setTextAlign(Paint.Align.CENTER);
218 | textGrayPaint.setTextSize(textGraySize);
219 | canvas.drawText("截至"+nowTime+"已走",0,-textBigSize+centerY,textGrayPaint);
220 | canvas.drawText(totalText+"",0,textSmallSize/2+centerY,textBigPaint);
221 | canvas.drawText("好友平均"+freAvgText+"步",0,textBigSize+centerY,textGrayPaint);
222 | canvas.drawText("第",-textSmallSize/2*3+centerX,width*widthScale+centerY,textGrayPaint);
223 | canvas.drawText(rankText+"",0+centerX,width*widthScale+centerY,textSmallPaint);
224 | canvas.drawText("名",textSmallSize/2*3+centerX,width*widthScale+centerY,textGrayPaint);
225 | }
226 |
227 | //添加动画
228 | private void addAnimator(){
229 | //步数动画
230 | mStepAnimator = ValueAnimator.ofInt(0,totalText);
231 | mStepAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
232 | @Override
233 | public void onAnimationUpdate(ValueAnimator animation) {
234 | totalText = (int) animation.getAnimatedValue();
235 | invalidate();
236 | }
237 | });
238 | //圆弧动画
239 | mArcAnimator = ValueAnimator.ofFloat(0,percent);
240 | mArcAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
241 | @Override
242 | public void onAnimationUpdate(ValueAnimator animation) {
243 | percent = (Float) animation.getAnimatedValue();
244 | invalidate();
245 | }
246 | });
247 | }
248 |
249 | //开启动画
250 | public void startAnimator(){
251 | AnimatorSet animatorSet = new AnimatorSet();
252 | animatorSet.setDuration(2000);
253 | animatorSet.playTogether(mStepAnimator,mArcAnimator);
254 | animatorSet.start();
255 | }
256 |
257 | //画下层背景
258 | private void paintBelowBackground(Canvas canvas){
259 | RectF rectF = new RectF(0,0,width,height);
260 | backgroundPaint.setColor(Color.parseColor("#FF46A6F5"));
261 | canvas.drawRoundRect(rectF,textGraySize,textGraySize,backgroundPaint);
262 | }
263 |
264 | //画上层背景
265 | private void paintUpBackground(Canvas canvas){
266 | Path path = new Path();
267 | path.moveTo(0,0);
268 | path.lineTo(width-20,0);
269 | path.quadTo(width,0,width,20);
270 | path.lineTo(width,height/6*5);
271 | path.quadTo(width/2,height/14*13,0,height/6*5);
272 | path.lineTo(0,textGraySize);
273 | path.quadTo(0,0,textGraySize,0);
274 | backgroundPaint.setColor(Color.parseColor("#FFFFFFFF"));
275 | canvas.drawPath(path,backgroundPaint);
276 | }
277 |
278 | //画虚线
279 | private void paintDashLine(Canvas canvas){
280 | canvas.drawLine(textGraySize,height/7*5,width-textGraySize,height/7*5,dashLinePaint);
281 | }
282 |
283 | //画竖条
284 | private void paintBar(Canvas canvas){
285 | float avgWidth = width/8;
286 | textGrayPaint.setTextSize(textGraySize-10);
287 | textGrayPaint.setTextAlign(Paint.Align.LEFT);
288 | canvas.drawText("最近7日",textGraySize,height/7*5-textSmallSize-textGraySize,textGrayPaint);
289 | textGrayPaint.setTextAlign(Paint.Align.RIGHT);
290 | canvas.drawText("平均"+avgText+"步/天",width-textGraySize,height/7*5-textSmallSize-textGraySize,textGrayPaint);
291 | textGrayPaint.setTextAlign(Paint.Align.CENTER);
292 | float avg = avgText;
293 | for(int i=0;i<7;i++){
294 | canvas.drawText(dates[i],avgWidth*(i+1),height/7*5+textBigSize,textGrayPaint);
295 | if(dayStepNum[i]=2){
302 | canvas.drawLine(avgWidth*(i+1),height/7*5+textSmallSize,avgWidth*(i+1),
303 | height/7*5-textSmallSize,barPaint);
304 | }else {
305 | canvas.drawLine(avgWidth*(i+1),height/7*5+textSmallSize,avgWidth*(i+1),
306 | height/7*5-(dayStepNum[i]/avg-1)*textSmallSize,barPaint);
307 | }
308 | }
309 | }
310 | }
311 |
312 | //画低栏的头像和文字
313 | private void paintBottom(Canvas canvas){
314 | int len = height/7;
315 | Rect dst = new Rect((int) textGraySize,len*6+len/4,
316 | (int) textGraySize+len/2,len*6+len/4*3);
317 | canvas.drawBitmap(toRoundBitmap(headBitmap),null,dst,null);
318 | Paint paint = new Paint();
319 | paint.setAntiAlias(true);
320 | paint.setTextSize(textGraySize/4*3);
321 | paint.setColor(Color.parseColor("#FFFFFF"));
322 | canvas.drawText("SFLin",textGraySize+len/4*3,len*6+len/4*2+10,paint);
323 | canvas.drawText("获得今日冠军",textGraySize+len/4*3+textBigSize,len*6+len/4*2+10,paint);
324 | canvas.drawText("查看 >",width-textBigSize,len*6+len/4*2+10,paint);
325 | }
326 |
327 | //将图片转换成圆形
328 | private Bitmap toRoundBitmap(Bitmap bitmap){
329 | int width = bitmap.getWidth();
330 | int height = bitmap.getHeight();
331 | int r;
332 | if (width > height) {
333 | r = height/2;
334 | } else {
335 | r = width/2;
336 | }
337 | Bitmap newBitmap = Bitmap.createBitmap(r*2,r*2, Bitmap.Config.ARGB_8888);
338 | Canvas canvas = new Canvas(newBitmap);
339 | Paint paint = new Paint();
340 | paint.setAntiAlias(true);
341 | RectF rectF = new RectF(0,0,r*2,r*2);
342 | BitmapShader bitmapShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP,
343 | Shader.TileMode.CLAMP);
344 | paint.setShader(bitmapShader);
345 | canvas.drawRoundRect(rectF,r,r,paint);
346 | return newBitmap;
347 | }
348 |
349 | //设置各个所需数据
350 | public void setData(int totalText,int freAvgText,int rankText,String nowTime,int avgText,int[] dayStepNum,String[] dates){
351 | this.totalText = totalText;
352 | this.freAvgText =freAvgText;
353 | this.rankText = rankText;
354 | this.nowTime = nowTime;
355 | this.avgText = avgText;
356 | this.dayStepNum = dayStepNum;
357 | this.dates = dates;
358 | if(totalText>freAvgText){
359 | percent = 1f;
360 | }else {
361 | percent = totalText/(float)freAvgText;
362 | }
363 | addAnimator();
364 | }
365 | }
366 |
--------------------------------------------------------------------------------
/src/main/java/sflin/open/customview01/MyQQSportActivity.java:
--------------------------------------------------------------------------------
1 | package sflin.open.customview01;
2 |
3 | import android.os.Bundle;
4 | import android.support.annotation.Nullable;
5 | import android.support.v7.app.AppCompatActivity;
6 |
7 | import java.text.SimpleDateFormat;
8 | import java.util.Calendar;
9 | import java.util.Date;
10 |
11 | /**
12 | * Created by a9951 on 2016/6/22.
13 | */
14 |
15 | public class MyQQSportActivity extends AppCompatActivity {
16 |
17 | private MyQQSport mMyQQSport;
18 |
19 | @Override
20 | protected void onCreate(@Nullable Bundle savedInstanceState) {
21 | super.onCreate(savedInstanceState);
22 | setContentView(R.layout.activity_myqqsport);
23 | mMyQQSport = (MyQQSport) findViewById(R.id.custom_myqqsport);
24 | mMyQQSport.setData(2532,3125,20,getNowTime(),avgStep(getDayStepNum()),getDayStepNum(),getDate());
25 | mMyQQSport.startAnimator();
26 | }
27 |
28 | public String getNowTime(){
29 | SimpleDateFormat df = new SimpleDateFormat("HH:mm");//设置日期格式
30 | return df.format(new Date());
31 | }
32 |
33 | public int[] getDayStepNum(){
34 | int[] stepNum = {1000,1500,400,3000,5000,3654,125};
35 | return stepNum;
36 | }
37 |
38 | public int avgStep(int[] steps){
39 | int total = 0;
40 | for(int step:steps){
41 | total +=step;
42 | }
43 | int avg = total/steps.length;
44 | return avg;
45 | }
46 |
47 | public String[] getDate(){
48 | String[] dates = new String[7];
49 | Calendar calendar = Calendar.getInstance();
50 | int day = calendar.get(Calendar.DATE);
51 | for (int i=0;i<7;i++){
52 | dates[i] = (day-(7-i))+"日";
53 | }
54 | return dates;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/main/java/sflin/open/customview01/ZombieActivity.java:
--------------------------------------------------------------------------------
1 | package sflin.open.customview01;
2 |
3 | import android.support.v7.app.AppCompatActivity;
4 | import android.os.Bundle;
5 | import android.view.View;
6 |
7 | public class ZombieActivity extends AppCompatActivity {
8 |
9 | private ZombieView mZombieView;
10 |
11 | @Override
12 | protected void onCreate(Bundle savedInstanceState) {
13 | super.onCreate(savedInstanceState);
14 | setContentView(R.layout.activity_zombie);
15 | mZombieView = (ZombieView) findViewById(R.id.custom_zombie);
16 | }
17 |
18 | public void start(View view){
19 | mZombieView.start();
20 | }
21 |
22 | public void sendBack(View view){
23 | mZombieView.sendBack();
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/java/sflin/open/customview01/ZombieView.java:
--------------------------------------------------------------------------------
1 | package sflin.open.customview01;
2 |
3 | import android.content.Context;
4 | import android.graphics.Bitmap;
5 | import android.graphics.BitmapFactory;
6 | import android.graphics.Canvas;
7 | import android.graphics.Rect;
8 | import android.os.Handler;
9 | import android.os.Message;
10 | import android.util.AttributeSet;
11 | import android.util.Log;
12 | import android.view.View;
13 |
14 | /**
15 | * Created by a9951 on 2016/6/22.
16 | */
17 |
18 | public class ZombieView extends View {
19 |
20 | private static final int ANIM_NULL = 0; //动画状态-没有
21 | private static final int ANIM_CHECK = 1; //动画状态-开启
22 | private static final int ANIM_UNCHECK = 2; //动画状态-结束
23 |
24 | private int mWidth,mHeight; //宽高
25 | private Handler mHandler;
26 |
27 | private Bitmap zombie;
28 |
29 | private int index = 0; //计算图片位置的下标
30 | private int animCurrentPage = 0; // 当前页码
31 | private int animMaxPage = 22; // 总页数
32 | private int animDuration = 5000; // 动画时长
33 | private int animState = ANIM_NULL; // 动画状态
34 |
35 | private boolean isStart = false; // 是否开始状态
36 |
37 | public ZombieView(Context context) {
38 | super(context);
39 | }
40 |
41 | public ZombieView(Context context, AttributeSet attrs) {
42 | super(context, attrs);
43 | init(context);
44 | }
45 |
46 | private void init(Context context){
47 | zombie = BitmapFactory.decodeResource(context.getResources(),R.mipmap.zombie);
48 |
49 | mHandler = new Handler(){
50 | @Override
51 | public void handleMessage(Message msg) {
52 | super.handleMessage(msg);
53 | if (animCurrentPage < animMaxPage && animCurrentPage >= 0) {
54 | invalidate();
55 | if (animState == ANIM_NULL)
56 | return;
57 | if (animState == ANIM_CHECK) {
58 | animCurrentPage++;
59 | } else if (animState == ANIM_UNCHECK) {
60 | animCurrentPage--;
61 | }
62 | Log.e("AAA", "Count=" + animCurrentPage);
63 | Log.e("AAA", "index=" + index);
64 | this.sendEmptyMessageDelayed(0, animDuration / animMaxPage);
65 | } else {
66 | animState = ANIM_NULL;
67 | }
68 | }
69 | };
70 | }
71 |
72 | @Override
73 | protected void onSizeChanged(int w, int h, int oldw, int oldh) {
74 | super.onSizeChanged(w, h, oldw, oldh);
75 | mWidth = w;
76 | mHeight = h;
77 | }
78 |
79 | @Override
80 | protected void onDraw(Canvas canvas) {
81 | super.onDraw(canvas);
82 |
83 | // 移动坐标系到画布中央
84 | canvas.translate(mWidth / 2, mHeight / 2);
85 |
86 | // 得出图像宽高
87 | int sideWidth = zombie.getWidth()/11;
88 | int sideHeight = zombie.getHeight()/2;
89 |
90 | // 得到图像选区
91 | Rect src = null;
92 | if (animCurrentPage%2==0 && animCurrentPage11){
95 | index = animCurrentPage/2;
96 | }
97 | src = new Rect(sideWidth*index,0,sideWidth*(index+1),sideHeight);
98 | }else {
99 | src = new Rect(sideWidth*index,sideHeight,sideWidth*(index+1),sideHeight*2);
100 | }
101 | //实际绘制位置
102 | Rect dst = new Rect(-66, -84, 66, 84);
103 | canvas.drawBitmap(zombie,src,dst,null);
104 | }
105 |
106 | /**
107 | * 开始
108 | */
109 | public void start() {
110 | if (animState != ANIM_NULL || isStart) return;
111 | animState = ANIM_CHECK;
112 | animCurrentPage = 0;
113 | mHandler.sendEmptyMessageDelayed(0, animDuration / animMaxPage);
114 | isStart = true;
115 | }
116 |
117 | /**
118 | * 回退
119 | */
120 | public void sendBack() {
121 | if (animState != ANIM_NULL || (!isStart)) {
122 | return;
123 | }
124 | animState = ANIM_UNCHECK;
125 | animCurrentPage = animMaxPage - 1;
126 | mHandler.sendEmptyMessageDelayed(0, animDuration / animMaxPage);
127 | isStart = false;
128 | }
129 |
130 | /**
131 | * 设置动画时长
132 | * @param animDuration
133 | */
134 | public void setAnimDuration(int animDuration) {
135 | if (animDuration <= 0)
136 | return;
137 | this.animDuration = animDuration;
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/src/main/res/layout/activity_check.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
12 |
13 |
17 |
18 |
24 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
12 |
13 |
18 |
19 |
24 |
--------------------------------------------------------------------------------
/src/main/res/layout/activity_myqqsport.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/src/main/res/layout/activity_zombie.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
12 |
13 |
17 |
18 |
24 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/main/res/mipmap-hdpi/check.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luren3/CustomView/fb64c986074b1e3f97c028be97de820e36866274/src/main/res/mipmap-hdpi/check.png
--------------------------------------------------------------------------------
/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luren3/CustomView/fb64c986074b1e3f97c028be97de820e36866274/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src/main/res/mipmap-hdpi/icon_head.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luren3/CustomView/fb64c986074b1e3f97c028be97de820e36866274/src/main/res/mipmap-hdpi/icon_head.jpeg
--------------------------------------------------------------------------------
/src/main/res/mipmap-hdpi/zombie.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luren3/CustomView/fb64c986074b1e3f97c028be97de820e36866274/src/main/res/mipmap-hdpi/zombie.png
--------------------------------------------------------------------------------
/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luren3/CustomView/fb64c986074b1e3f97c028be97de820e36866274/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luren3/CustomView/fb64c986074b1e3f97c028be97de820e36866274/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luren3/CustomView/fb64c986074b1e3f97c028be97de820e36866274/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luren3/CustomView/fb64c986074b1e3f97c028be97de820e36866274/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/src/main/res/values/atrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 |
--------------------------------------------------------------------------------
/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | CustomView01
3 |
4 |
--------------------------------------------------------------------------------
/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/test/java/sflin/open/customview01/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package sflin.open.customview01;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() throws Exception {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------