├── .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 | ![check](http://o9o9d242i.bkt.clouddn.com/check.gif) 8 | 9 | >实现上图的效果参考[这篇文章](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B4%5DCanvas_PictureText.md) 10 | 11 | ## 再来这个效果图 12 | ![zombie](http://o9o9d242i.bkt.clouddn.com/zombie.gif) 13 | >上面效果图是根据第一个Demo的扩展,具体源码会在文章结尾提供链接 14 | 15 | ## 最后一个效果图 16 | ![sport](http://o9o9d242i.bkt.clouddn.com/sport.gif) 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 | 8 | 9 | 10 | 11 | 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 |