├── .gitignore ├── .idea ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── gradle.xml ├── misc.xml ├── modules.xml └── runConfigurations.xml ├── QQSportDemo.apk ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── example │ │ └── zhuyong │ │ └── qqsportdemo │ │ ├── MainActivity.java │ │ ├── QQSportView.java │ │ └── Util.java │ └── res │ ├── layout │ └── activity_main.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ ├── attrs.xml │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── art └── demo.gif ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 19 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /QQSportDemo.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuperKotlin/QQSportDemo/c2d0358c2a1a2a1705c306d183127ccc788de518/QQSportDemo.apk -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ##QQSportDemo效果演示: 2 | 3 | ![](/art/demo.gif) 4 | 5 | 简书地址:[http://www.jianshu.com/p/f3ab9fdf89bd](http://www.jianshu.com/p/f3ab9fdf89bd "仿APP中运动步数进度效果的自定义View") 6 | 7 | ### 关于我 8 | - 我的简书:[BraveJoy](http://www.jianshu.com/users/c96d2a9d160f/timeline) 9 | - 我的github:[SuperKotlin](https://github.com/SuperKotlin) 10 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 25 5 | buildToolsVersion "25.0.3" 6 | defaultConfig { 7 | applicationId "com.example.zhuyong.qqsportdemo" 8 | minSdkVersion 14 9 | targetSdkVersion 22 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 25 | exclude group: 'com.android.support', module: 'support-annotations' 26 | }) 27 | compile 'com.android.support:appcompat-v7:+' 28 | compile 'com.android.support.constraint:constraint-layout:1.0.2' 29 | testCompile 'junit:junit:4.12' 30 | compile 'cn.qqtheme.framework:ColorPicker:1.1.3' 31 | } 32 | -------------------------------------------------------------------------------- /app/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 C:\Users\zhuyong\AppData\Local\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 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/zhuyong/qqsportdemo/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.zhuyong.qqsportdemo; 2 | 3 | import android.animation.ObjectAnimator; 4 | import android.animation.ValueAnimator; 5 | import android.support.v7.app.AppCompatActivity; 6 | import android.os.Bundle; 7 | import android.util.Log; 8 | import android.view.View; 9 | import android.view.animation.DecelerateInterpolator; 10 | import android.widget.SeekBar; 11 | import android.widget.TextView; 12 | 13 | import cn.qqtheme.framework.picker.ColorPicker; 14 | 15 | public class MainActivity extends AppCompatActivity { 16 | 17 | private TextView mTvPaintWidth; 18 | private TextView mTvCurrentNum; 19 | private SeekBar mSbPaintWidth; 20 | private SeekBar mSbCurrentNum; 21 | private QQSportView qqsport_view; 22 | private int mCurrentNum = 8555;//当前步数,默认8555,最高10000 23 | 24 | @Override 25 | protected void onCreate(Bundle savedInstanceState) { 26 | super.onCreate(savedInstanceState); 27 | setContentView(R.layout.activity_main); 28 | qqsport_view = (QQSportView) findViewById(R.id.qqsport_view); 29 | mSbPaintWidth = (SeekBar) findViewById(R.id.sb); 30 | mSbCurrentNum = (SeekBar) findViewById(R.id.sb_num); 31 | mTvPaintWidth = (TextView) findViewById(R.id.tv_paint_width); 32 | mTvCurrentNum = (TextView) findViewById(R.id.tv_current_num); 33 | 34 | qqsport_view.setMaxNum(10000);//mCurrentNum不能超过此数值 35 | 36 | final ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, mCurrentNum); 37 | valueAnimator.setDuration(2000); 38 | valueAnimator.setInterpolator(new DecelerateInterpolator()); 39 | valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 40 | @Override 41 | public void onAnimationUpdate(ValueAnimator animation) { 42 | float current = (float) animation.getAnimatedValue(); 43 | qqsport_view.setCurrent((int) current); 44 | } 45 | }); 46 | valueAnimator.start(); 47 | 48 | /** 49 | * 修改画笔宽度 50 | */ 51 | mSbPaintWidth.setProgress(Util.dip2px(MainActivity.this, 10)); 52 | mSbPaintWidth.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { 53 | @Override 54 | public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 55 | mTvPaintWidth.setText("画笔宽度" + Util.px2dip(MainActivity.this, progress) + "dp"); 56 | qqsport_view.setWidth(progress); 57 | } 58 | 59 | @Override 60 | public void onStartTrackingTouch(SeekBar seekBar) { 61 | 62 | } 63 | 64 | @Override 65 | public void onStopTrackingTouch(SeekBar seekBar) { 66 | valueAnimator.start(); 67 | } 68 | }); 69 | /** 70 | * 修改步数 71 | */ 72 | mSbCurrentNum.setProgress(mCurrentNum); 73 | mTvCurrentNum.setText("当前步数" + mCurrentNum); 74 | mSbCurrentNum.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { 75 | @Override 76 | public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 77 | mTvCurrentNum.setText("当前步数" + progress); 78 | mCurrentNum = progress; 79 | qqsport_view.setCurrent(mCurrentNum); 80 | } 81 | 82 | @Override 83 | public void onStartTrackingTouch(SeekBar seekBar) { 84 | 85 | } 86 | 87 | @Override 88 | public void onStopTrackingTouch(SeekBar seekBar) { 89 | valueAnimator.setFloatValues(mCurrentNum); 90 | valueAnimator.start(); 91 | } 92 | }); 93 | /** 94 | * 修改颜色 95 | */ 96 | findViewById(R.id.btn_big_rad).setOnClickListener(new View.OnClickListener() { 97 | @Override 98 | public void onClick(View v) { 99 | ColorPicker picker = new ColorPicker(MainActivity.this); 100 | picker.setOnColorPickListener(new ColorPicker.OnColorPickListener() { 101 | @Override 102 | public void onColorPicked(int pickedColor) { 103 | qqsport_view.setOutPaintColor(pickedColor); 104 | valueAnimator.start(); 105 | } 106 | }); 107 | picker.show(); 108 | } 109 | }); 110 | 111 | /** 112 | * 修改颜色 113 | */ 114 | findViewById(R.id.btn_small_rad).setOnClickListener(new View.OnClickListener() { 115 | @Override 116 | public void onClick(View v) { 117 | ColorPicker picker = new ColorPicker(MainActivity.this); 118 | picker.setOnColorPickListener(new ColorPicker.OnColorPickListener() { 119 | @Override 120 | public void onColorPicked(int pickedColor) { 121 | qqsport_view.setInPaintColor(pickedColor); 122 | valueAnimator.start(); 123 | } 124 | }); 125 | picker.show(); 126 | } 127 | }); 128 | 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/zhuyong/qqsportdemo/QQSportView.java: -------------------------------------------------------------------------------- 1 | package com.example.zhuyong.qqsportdemo; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.graphics.Canvas; 6 | import android.graphics.Color; 7 | import android.graphics.Paint; 8 | import android.graphics.Rect; 9 | import android.graphics.RectF; 10 | import android.support.annotation.Nullable; 11 | import android.util.AttributeSet; 12 | import android.util.Log; 13 | import android.view.View; 14 | 15 | /** 16 | * Created by zhuyong on 2017/6/14. 17 | * 自定义View 18 | */ 19 | public class QQSportView extends View { 20 | private Context mContext; 21 | private int mOutBgColor = Color.BLUE; 22 | private int mInBgColor = Color.RED; 23 | private float mPaintOutWidth; 24 | private int mTextColor = Color.RED; 25 | private float mTextSize; 26 | 27 | private Paint mPaintOut;//大圆弧画笔 28 | private Paint mPaintIn;//当前步数的画笔 29 | private Paint mPaintTextLabel; 30 | private Paint mPaintText; 31 | private static final String mTextLabel = "今日步数"; 32 | 33 | private int mNumMax = 10000; 34 | private int mNumCurrent = 0; 35 | 36 | public QQSportView(Context context) { 37 | this(context, null); 38 | } 39 | 40 | public QQSportView(Context context, @Nullable AttributeSet attrs) { 41 | this(context, attrs, 0); 42 | } 43 | 44 | public QQSportView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 45 | super(context, attrs, defStyleAttr); 46 | mContext = context; 47 | TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.QQSportView); 48 | mOutBgColor = array.getColor(R.styleable.QQSportView_out_bg_color, mOutBgColor); 49 | mInBgColor = array.getColor(R.styleable.QQSportView_in_bg_color, mInBgColor); 50 | mPaintOutWidth = array.getDimension(R.styleable.QQSportView_circle_width, Util.dip2px(mContext, 10));//默认10dp 51 | mTextColor = array.getColor(R.styleable.QQSportView_text_color, mTextColor); 52 | mTextSize = array.getDimension(R.styleable.QQSportView_text_size, Util.dip2px(mContext, 40));//默认40sp 53 | array.recycle(); 54 | initPaint(); 55 | } 56 | 57 | 58 | private void initPaint() { 59 | //画背景圆弧 60 | mPaintOut = new Paint(); 61 | mPaintOut.setAntiAlias(true); 62 | mPaintOut.setStrokeWidth(mPaintOutWidth); 63 | mPaintOut.setColor(mOutBgColor); 64 | mPaintOut.setStyle(Paint.Style.STROKE); 65 | mPaintOut.setStrokeCap(Paint.Cap.ROUND); 66 | //画当前步数 67 | mPaintIn = new Paint(); 68 | mPaintIn.setAntiAlias(true); 69 | mPaintIn.setStrokeWidth(mPaintOutWidth); 70 | mPaintIn.setColor(mInBgColor); 71 | mPaintIn.setStyle(Paint.Style.STROKE); 72 | mPaintIn.setStrokeCap(Paint.Cap.ROUND); 73 | //画“今日步数” 74 | mPaintTextLabel = new Paint(); 75 | mPaintTextLabel.setAntiAlias(true); 76 | mPaintTextLabel.setColor(Color.GRAY); 77 | mPaintTextLabel.setStyle(Paint.Style.STROKE); 78 | mPaintTextLabel.setTextSize(Util.dip2px(mContext, 16)); 79 | //画数字 80 | mPaintText = new Paint(); 81 | mPaintText.setAntiAlias(true); 82 | mPaintText.setColor(mTextColor); 83 | mPaintText.setStyle(Paint.Style.STROKE); 84 | mPaintText.setTextSize(mTextSize); 85 | 86 | } 87 | 88 | @Override 89 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 90 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 91 | int length = MeasureSpec.getSize(widthMeasureSpec) > MeasureSpec.getSize(heightMeasureSpec) ? 92 | MeasureSpec.getSize(heightMeasureSpec) : MeasureSpec.getSize(widthMeasureSpec); 93 | setMeasuredDimension(length, length); 94 | } 95 | 96 | @Override 97 | protected void onDraw(Canvas canvas) { 98 | super.onDraw(canvas); 99 | //外圆弧,因为画笔有一定的宽度,所有画圆弧的范围要比View本身的大小稍微小一些,不然画笔画出来的东西会显示不完整 100 | RectF rectF = new RectF(mPaintOutWidth / 2, mPaintOutWidth / 2, getWidth() - mPaintOutWidth / 2, getHeight() - mPaintOutWidth / 2); 101 | canvas.drawArc(rectF, 135, 270, false, mPaintOut); 102 | 103 | //内圆弧 104 | if (mNumMax <= 0) return; 105 | float sweepAngle = (float) mNumCurrent / mNumMax; 106 | canvas.drawArc(rectF, 135, sweepAngle * 270, false, mPaintIn); 107 | 108 | //字(今日步数) 109 | Rect textBoundsLabel = new Rect(); 110 | mPaintTextLabel.getTextBounds(mTextLabel, 0, mTextLabel.length(), textBoundsLabel); 111 | 112 | int dexLabel = getWidth() / 2 - textBoundsLabel.width() / 2; 113 | Paint.FontMetricsInt fontMetrics1 = mPaintTextLabel.getFontMetricsInt(); 114 | int linrLabel = (fontMetrics1.bottom - fontMetrics1.top) / 2 - fontMetrics1.bottom; 115 | int dyLabel = getHeight() / 3 + linrLabel; 116 | canvas.drawText(mTextLabel, dexLabel, dyLabel, mPaintTextLabel); 117 | 118 | //字(多少步) 119 | String mText = mNumCurrent + ""; 120 | Rect textBounds = new Rect(); 121 | mPaintText.getTextBounds(mText, 0, mText.length(), textBounds); 122 | 123 | int dex = getWidth() / 2 - textBounds.width() / 2; 124 | Paint.FontMetricsInt fontMetrics = mPaintText.getFontMetricsInt(); 125 | int line = (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom; 126 | int dy = (int) (getHeight() / 1.5 + line); 127 | canvas.drawText(mText, dex, dy, mPaintText); 128 | } 129 | 130 | public void setMaxNum(int max) { 131 | this.mNumMax = max; 132 | } 133 | 134 | /** 135 | * 设置当前步数并进行重绘 136 | * 137 | * @param current 138 | */ 139 | public void setCurrent(int current) { 140 | this.mNumCurrent = current; 141 | invalidate(); 142 | } 143 | 144 | /** 145 | * 设置弧度的宽度 146 | * 147 | * @param width 148 | */ 149 | public void setWidth(int width) { 150 | this.mPaintOutWidth = width; 151 | initPaint(); 152 | invalidate(); 153 | } 154 | 155 | /** 156 | * 设置当大圆弧的颜色 157 | * 158 | * @param outPaintColor 159 | */ 160 | public void setOutPaintColor(int outPaintColor) { 161 | this.mOutBgColor = outPaintColor; 162 | initPaint(); 163 | invalidate(); 164 | } 165 | 166 | /** 167 | * 设置当小圆弧的颜色 168 | * 169 | * @param inPaintColor 170 | */ 171 | public void setInPaintColor(int inPaintColor) { 172 | this.mInBgColor = inPaintColor; 173 | initPaint(); 174 | invalidate(); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/zhuyong/qqsportdemo/Util.java: -------------------------------------------------------------------------------- 1 | package com.example.zhuyong.qqsportdemo; 2 | 3 | import android.content.Context; 4 | 5 | /** 6 | * Created by zhuyong on 2017/6/14. 7 | * 像素转换工具类 8 | */ 9 | 10 | public class Util { 11 | 12 | /** 13 | * 根据手机的分辨率从 dp 的单位 转成为 px(像素) 14 | */ 15 | public static int dip2px(Context context, float dpValue) { 16 | final float scale = context.getResources().getDisplayMetrics().density; 17 | return (int) (dpValue * scale + 0.5f); 18 | } 19 | 20 | /** 21 | * 根据手机的分辨率从 px(像素) 的单位 转成为 dp 22 | */ 23 | public static int px2dip(Context context, float pxValue) { 24 | final float scale = context.getResources().getDisplayMetrics().density; 25 | return (int) (pxValue / scale + 0.5f); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 21 | 22 | 28 | 29 | 34 | 35 | 41 | 42 | 47 | 48 | 52 | 53 |