├── .gitignore ├── .idea ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── encodings.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── chx │ │ └── piechartdemo │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── chx │ │ │ └── piechartdemo │ │ │ ├── DensityUtils.java │ │ │ ├── Main2Activity.java │ │ │ ├── MainActivity.java │ │ │ ├── MyPieChart.java │ │ │ ├── MyPieChart2.java │ │ │ └── MyView.java │ └── res │ │ ├── drawable │ │ └── edit_bg_stroke.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ └── activity_main2.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 │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── chx │ └── piechartdemo │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | out/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | 26 | # Log Files 27 | *.log 28 | 29 | # Android Studio Navigation editor temp files 30 | .navigation/ 31 | 32 | # Android Studio captures folder 33 | captures/ 34 | 35 | # Intellij 36 | *.iml 37 | .idea/workspace.xml 38 | .idea/tasks.xml 39 | .idea/gradle.xml 40 | .idea/dictionaries 41 | .idea/libraries 42 | 43 | # Keystore files 44 | *.jks 45 | 46 | # External native build folder generated in Android Studio 2.2 and later 47 | .externalNativeBuild 48 | 49 | # Google Services (e.g. APIs or Firebase) 50 | google-services.json 51 | 52 | # Freeline 53 | freeline.py 54 | freeline/ 55 | freeline_project_description.json 56 | -------------------------------------------------------------------------------- /.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/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.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 | 47 | 48 | 49 | 50 | 1.8 51 | 52 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MyPieChartDemo 2 | 自定义饼状图 3 |   4 | 具体实现详解 http://www.jianshu.com/p/7233f529657d 5 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 25 5 | buildToolsVersion "26.0.0" 6 | defaultConfig { 7 | applicationId "com.chx.piechartdemo" 8 | minSdkVersion 15 9 | targetSdkVersion 25 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:25.3.1' 28 | compile 'com.android.support.constraint:constraint-layout:1.0.2' 29 | testCompile 'junit:junit:4.12' 30 | } 31 | -------------------------------------------------------------------------------- /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 E:\Studio\23.0.0sdk/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/androidTest/java/com/chx/piechartdemo/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.chx.piechartdemo; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.chx.piechartdemo", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/chx/piechartdemo/DensityUtils.java: -------------------------------------------------------------------------------- 1 | package com.chx.piechartdemo; 2 | 3 | import android.content.Context; 4 | import android.util.TypedValue; 5 | 6 | /** 7 | * 常用单位转换的工具类 8 | */ 9 | public class DensityUtils { 10 | private DensityUtils() { 11 | 12 | } 13 | 14 | /** 15 | * dp转px 16 | * 17 | * @param context 18 | * @return 19 | */ 20 | public static int dp2px(Context context, float dpVal) { 21 | return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpVal, context.getResources() 22 | .getDisplayMetrics()); 23 | } 24 | 25 | /** 26 | * sp转px 27 | * 28 | * @param context 29 | * @return 30 | */ 31 | public static int sp2px(Context context, float spVal) { 32 | // return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, spVal, context.getResources() 33 | // .getDisplayMetrics()); 34 | final float fontScale = context.getResources().getDisplayMetrics().scaledDensity; 35 | return (int) (spVal * fontScale + 0.5f); 36 | } 37 | 38 | /** 39 | * px转dp 40 | * 41 | * @param context 42 | * @param pxVal 43 | * @return 44 | */ 45 | public static float px2dp(Context context, float pxVal) { 46 | final float scale = context.getResources().getDisplayMetrics().density; 47 | return (pxVal / scale); 48 | } 49 | 50 | /** 51 | * px转sp 52 | * 53 | * @param pxVal 54 | * @return 55 | */ 56 | public static float px2sp(Context context, float pxVal) { 57 | return (pxVal / context.getResources().getDisplayMetrics().scaledDensity); 58 | } 59 | /** 60 | * 得到屏幕宽度 61 | * 62 | * @param context 63 | * @return 64 | */ 65 | public static int getDisplayWidth(Context context) { 66 | return context.getResources().getDisplayMetrics().widthPixels; 67 | } 68 | 69 | /** 70 | * 得到屏幕高度 71 | * 72 | * @param context 73 | * @return 74 | */ 75 | public static int getDisplayHeight(Context context) { 76 | return context.getResources().getDisplayMetrics().heightPixels; 77 | } 78 | } -------------------------------------------------------------------------------- /app/src/main/java/com/chx/piechartdemo/Main2Activity.java: -------------------------------------------------------------------------------- 1 | package com.chx.piechartdemo; 2 | 3 | import android.support.v7.app.AppCompatActivity; 4 | import android.os.Bundle; 5 | import android.text.TextUtils; 6 | import android.view.View; 7 | import android.widget.Button; 8 | import android.widget.EditText; 9 | 10 | public class Main2Activity extends AppCompatActivity { 11 | 12 | 13 | private EditText editText; 14 | private Button btn; 15 | private MyView myView; 16 | 17 | 18 | @Override 19 | protected void onCreate(Bundle savedInstanceState) { 20 | super.onCreate(savedInstanceState); 21 | setContentView(R.layout.activity_main2); 22 | 23 | myView = (MyView) findViewById(R.id.myview); 24 | 25 | editText = (EditText) findViewById(R.id.edit); 26 | 27 | btn = (Button) findViewById(R.id.btn); 28 | 29 | btn.setOnClickListener(new View.OnClickListener() { 30 | @Override 31 | public void onClick(View v) { 32 | if (!TextUtils.isEmpty(editText.getText().toString().trim())) { 33 | myView.aDDEntry(editText.getText().toString().trim()); 34 | } 35 | } 36 | }); 37 | 38 | 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/chx/piechartdemo/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.chx.piechartdemo; 2 | 3 | import android.support.v7.app.AppCompatActivity; 4 | import android.os.Bundle; 5 | import android.widget.Toast; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | public class MainActivity extends AppCompatActivity implements MyPieChart.OnItemClickListener { 11 | 12 | private MyPieChart myPieChart; 13 | 14 | @Override 15 | protected void onCreate(Bundle savedInstanceState) { 16 | super.onCreate(savedInstanceState); 17 | setContentView(R.layout.activity_main); 18 | myPieChart = (MyPieChart) findViewById(R.id.mypiechart); 19 | 20 | myPieChart.setRadius(DensityUtils.dp2px(this, 80)); 21 | 22 | myPieChart.setOnItemClickListener(this); 23 | 24 | List pieEntries = new ArrayList<>(); 25 | pieEntries.add(new MyPieChart.PieEntry(1, R.color.chart_orange, true)); 26 | pieEntries.add(new MyPieChart.PieEntry(1, R.color.chart_green, false)); 27 | pieEntries.add(new MyPieChart.PieEntry(1, R.color.chart_blue, false)); 28 | pieEntries.add(new MyPieChart.PieEntry(1, R.color.chart_purple, false)); 29 | pieEntries.add(new MyPieChart.PieEntry(1, R.color.chart_mblue, false)); 30 | pieEntries.add(new MyPieChart.PieEntry(1, R.color.chart_turquoise, false)); 31 | 32 | myPieChart.setPieEntries(pieEntries); 33 | 34 | } 35 | 36 | @Override 37 | public void onItemClick(int position) { 38 | Toast.makeText(this, "点击了" + position, Toast.LENGTH_SHORT).show(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/chx/piechartdemo/MyPieChart.java: -------------------------------------------------------------------------------- 1 | package com.chx.piechartdemo; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.Color; 6 | import android.graphics.Paint; 7 | import android.graphics.RectF; 8 | import android.support.annotation.Nullable; 9 | import android.util.AttributeSet; 10 | import android.view.MotionEvent; 11 | import android.view.View; 12 | 13 | 14 | import java.text.DecimalFormat; 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | 18 | /** 19 | * Created by chaohx on 2017/8/9. 20 | */ 21 | 22 | public class MyPieChart extends View { 23 | 24 | private List pieEntries; 25 | 26 | private Paint paint; //画笔 27 | 28 | private float centerX; //中心点坐标 x 29 | private float centerY; //中心点坐标 y 30 | private float radius; //未选中状态的半径 31 | private float sRadius; //选中状态的半径 32 | 33 | private OnItemClickListener listener; //点击事件的回调 34 | 35 | public void setOnItemClickListener(OnItemClickListener listener) { 36 | this.listener = listener; 37 | } 38 | 39 | public MyPieChart(Context context) { 40 | super(context); 41 | init(); 42 | } 43 | 44 | public MyPieChart(Context context, @Nullable AttributeSet attrs) { 45 | super(context, attrs); 46 | init(); 47 | } 48 | 49 | public MyPieChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 50 | super(context, attrs, defStyleAttr); 51 | init(); 52 | } 53 | 54 | private void init() { 55 | pieEntries = new ArrayList<>(); 56 | paint = new Paint(); 57 | paint.setTextSize(DensityUtils.sp2px(getContext(), 15)); 58 | paint.setAntiAlias(true); 59 | } 60 | 61 | 62 | public void setRadius(float radius) { 63 | this.sRadius = radius; 64 | } 65 | 66 | /** 67 | * 设置数据并刷新 68 | * 69 | * @param pieEntries 70 | */ 71 | public void setPieEntries(List pieEntries) { 72 | this.pieEntries = pieEntries; 73 | invalidate(); 74 | } 75 | 76 | @Override 77 | protected void onDraw(Canvas canvas) { 78 | super.onDraw(canvas); 79 | //计算总值 80 | float total = 0; 81 | for (int i = 0; i < pieEntries.size(); i++) { 82 | total += pieEntries.get(i).getNumber(); 83 | } 84 | //刷新中心点 和半径 85 | centerX = getPivotX(); 86 | centerY = getPivotY(); 87 | if (sRadius == 0) { //这里做个判断,如果没有通过setRadius方法设置半径,则半径为真个view最小边的一半 88 | sRadius = (getWidth() > getHeight() ? getHeight() / 2 : getWidth() / 2); 89 | } 90 | //计算出两个状态的半径,这里二者相差5dp. 91 | radius = sRadius - DensityUtils.dp2px(getContext(), 5); 92 | 93 | //其实角度设置为0,即x轴正方形 94 | float startC = 0; 95 | //遍历List 开始画扇形 96 | for (int i = 0; i < pieEntries.size(); i++) { 97 | //计算当前扇形扫过的角度 98 | float sweep; 99 | if (total <= 0) { 100 | sweep = 360 / pieEntries.size(); 101 | } else { 102 | sweep = 360 * (pieEntries.get(i).getNumber() / total); 103 | } 104 | //设置当前扇形的颜色 105 | paint.setColor(getResources().getColor(pieEntries.get(i).colorRes)); 106 | //判断当前扇形是否被选中,确定用哪个半径 107 | float radiusT; 108 | if (pieEntries.get(i).isSelected()) { 109 | radiusT = sRadius; 110 | } else { 111 | radiusT = radius; 112 | } 113 | //画扇形的方法 114 | RectF rectF = new RectF(centerX - radiusT, centerY - radiusT, centerX + radiusT, centerY + radiusT); 115 | canvas.drawArc(rectF, startC, sweep, true, paint); 116 | 117 | 118 | if ((pieEntries.get(i).getNumber() > 0 && total > 0) || (total <= 0 && pieEntries.get(i).getNumber() <= 0)) { 119 | //下面是画扇形外围的 短线和百分数值。 120 | float arcCenterC = startC + sweep / 2; //当前扇形弧线的中间点和圆心的连线 与 起始角度的夹角 121 | float arcCenterX = 0; //当前扇形弧线的中间点 的坐标 x 以此点作为短线的起始点 122 | float arcCenterY = 0; //当前扇形弧线的中间点 的坐标 y 123 | 124 | float arcCenterX2 = 0; //这两个点作为短线的结束点 125 | float arcCenterY2 = 0; 126 | //百分百数字的格式 127 | DecimalFormat numberFormat = new DecimalFormat("00.00"); 128 | paint.setColor(Color.BLACK); 129 | 130 | //分象限 利用三角函数 来求出每个短线的起始点和结束点,并画出短线和百分比。 131 | //具体的计算方法看下面图示介绍 132 | if (arcCenterC >= 0 && arcCenterC < 90) { 133 | arcCenterX = (float) (centerX + radiusT * Math.cos(arcCenterC * Math.PI / 180)); 134 | arcCenterY = (float) (centerY + radiusT * Math.sin(arcCenterC * Math.PI / 180)); 135 | arcCenterX2 = (float) (arcCenterX + DensityUtils.dp2px(getContext(), 10) * Math.cos(arcCenterC * Math.PI / 180)); 136 | arcCenterY2 = (float) (arcCenterY + DensityUtils.dp2px(getContext(), 10) * Math.sin(arcCenterC * Math.PI / 180)); 137 | canvas.drawLine(arcCenterX, arcCenterY, arcCenterX2, arcCenterY2, paint); 138 | if (total <= 0) { 139 | canvas.drawText(numberFormat.format(0) + "%", arcCenterX2, arcCenterY2 + paint.getTextSize() / 2, paint); 140 | } else { 141 | canvas.drawText(numberFormat.format(pieEntries.get(i).getNumber() / total * 100) + "%", arcCenterX2, arcCenterY2 + paint.getTextSize() / 2, paint); 142 | } 143 | } else if (arcCenterC >= 90 && arcCenterC < 180) { 144 | arcCenterC = 180 - arcCenterC; 145 | arcCenterX = (float) (centerX - radiusT * Math.cos(arcCenterC * Math.PI / 180)); 146 | arcCenterY = (float) (centerY + radiusT * Math.sin(arcCenterC * Math.PI / 180)); 147 | arcCenterX2 = (float) (arcCenterX - DensityUtils.dp2px(getContext(), 10) * Math.cos(arcCenterC * Math.PI / 180)); 148 | arcCenterY2 = (float) (arcCenterY + DensityUtils.dp2px(getContext(), 10) * Math.sin(arcCenterC * Math.PI / 180)); 149 | canvas.drawLine(arcCenterX, arcCenterY, arcCenterX2, arcCenterY2, paint); 150 | if (total <= 0) { 151 | canvas.drawText(numberFormat.format(0) + "%", (float) (arcCenterX2 - paint.getTextSize() * 3.5), arcCenterY2 + paint.getTextSize() / 2, paint); 152 | } else { 153 | canvas.drawText(numberFormat.format(pieEntries.get(i).getNumber() / total * 100) + "%", (float) (arcCenterX2 - paint.getTextSize() * 3.5), arcCenterY2 + paint.getTextSize() / 2, paint); 154 | } 155 | } else if (arcCenterC >= 180 && arcCenterC < 270) { 156 | arcCenterC = 270 - arcCenterC; 157 | arcCenterX = (float) (centerX - radiusT * Math.sin(arcCenterC * Math.PI / 180)); 158 | arcCenterY = (float) (centerY - radiusT * Math.cos(arcCenterC * Math.PI / 180)); 159 | arcCenterX2 = (float) (arcCenterX - DensityUtils.dp2px(getContext(), 10) * Math.sin(arcCenterC * Math.PI / 180)); 160 | arcCenterY2 = (float) (arcCenterY - DensityUtils.dp2px(getContext(), 10) * Math.cos(arcCenterC * Math.PI / 180)); 161 | canvas.drawLine(arcCenterX, arcCenterY, arcCenterX2, arcCenterY2, paint); 162 | if (total <= 0) { 163 | canvas.drawText(numberFormat.format(0) + "%", (float) (arcCenterX2 - paint.getTextSize() * 3.5), arcCenterY2, paint); 164 | } else { 165 | canvas.drawText(numberFormat.format(pieEntries.get(i).getNumber() / total * 100) + "%", (float) (arcCenterX2 - paint.getTextSize() * 3.5), arcCenterY2, paint); 166 | } 167 | } else if (arcCenterC >= 270 && arcCenterC < 360) { 168 | arcCenterC = 360 - arcCenterC; 169 | arcCenterX = (float) (centerX + radiusT * Math.cos(arcCenterC * Math.PI / 180)); 170 | arcCenterY = (float) (centerY - radiusT * Math.sin(arcCenterC * Math.PI / 180)); 171 | arcCenterX2 = (float) (arcCenterX + DensityUtils.dp2px(getContext(), 10) * Math.cos(arcCenterC * Math.PI / 180)); 172 | arcCenterY2 = (float) (arcCenterY - DensityUtils.dp2px(getContext(), 10) * Math.sin(arcCenterC * Math.PI / 180)); 173 | canvas.drawLine(arcCenterX, arcCenterY, arcCenterX2, arcCenterY2, paint); 174 | if (total <= 0) { 175 | canvas.drawText(numberFormat.format(0) + "%", arcCenterX2, arcCenterY2, paint); 176 | } else { 177 | canvas.drawText(numberFormat.format(pieEntries.get(i).getNumber() / total * 100) + "%", arcCenterX2, arcCenterY2, paint); 178 | } 179 | } 180 | } 181 | //将每个扇形的起始角度 和 结束角度 放入对应的对象 182 | pieEntries.get(i).setStartC(startC); 183 | pieEntries.get(i).setEndC(startC + sweep); 184 | //将当前扇形的结束角度作为下一个扇形的起始角度 185 | startC += sweep; 186 | } 187 | } 188 | 189 | @Override 190 | public boolean onTouchEvent(MotionEvent event) { 191 | float touchX; 192 | float touchY; 193 | switch (event.getAction()) { 194 | case MotionEvent.ACTION_DOWN: 195 | touchX = event.getX(); //touch点的坐标 196 | touchY = event.getY(); 197 | //判断touch点到圆心的距离 是否小于半径 198 | if (Math.pow(touchX - centerX, 2) + Math.pow(touchY - centerY, 2) <= Math.pow(radius, 2)) { 199 | //计算 touch点和圆心的连线 与 x轴正方向的夹角 200 | float touchC = getSweep(touchX, touchY); 201 | //遍历 List 判断touch点在哪个扇形中 202 | for (int i = 0; i < pieEntries.size(); i++) { 203 | if (touchC >= pieEntries.get(i).getStartC() && touchC < pieEntries.get(i).getEndC()) { 204 | pieEntries.get(i).setSelected(true); 205 | if (listener != null) 206 | listener.onItemClick(i); //将被点击的扇形id回调出去 207 | } else { 208 | pieEntries.get(i).setSelected(false); 209 | } 210 | } 211 | invalidate();//刷新画布 212 | } 213 | break; 214 | } 215 | 216 | return super.onTouchEvent(event); 217 | } 218 | 219 | /** 220 | * 获取 touch点/圆心连线 与 x轴正方向 的夹角 221 | * 222 | * @param touchX 223 | * @param touchY 224 | */ 225 | private float getSweep(float touchX, float touchY) { 226 | float xZ = touchX - centerX; 227 | float yZ = touchY - centerY; 228 | float a = Math.abs(xZ); 229 | float b = Math.abs(yZ); 230 | double c = Math.toDegrees(Math.atan(b / a)); 231 | if (xZ >= 0 && yZ >= 0) {//第一象限 232 | return (float) c; 233 | } else if (xZ <= 0 && yZ >= 0) {//第二象限 234 | return 180 - (float) c; 235 | } else if (xZ <= 0 && yZ <= 0) {//第三象限 236 | return (float) c + 180; 237 | } else {//第四象限 238 | return 360 - (float) c; 239 | } 240 | } 241 | 242 | public interface OnItemClickListener { 243 | public void onItemClick(int position); 244 | } 245 | 246 | 247 | /** 248 | * 每个扇形的对象 249 | */ 250 | public static class PieEntry { 251 | private float number; //数值 252 | private int colorRes; //颜色资源 253 | private boolean selected; //是否选中 254 | private float startC; //对应扇形起始角度 255 | private float endC; //对应扇形结束角度 256 | 257 | public PieEntry(float number, int colorRes, boolean selected) { 258 | this.number = number; //防止分母为零 259 | this.colorRes = colorRes; 260 | this.selected = selected; 261 | } 262 | 263 | public float getStartC() { 264 | return startC; 265 | } 266 | 267 | public void setStartC(float startC) { 268 | this.startC = startC; 269 | } 270 | 271 | public float getEndC() { 272 | return endC; 273 | } 274 | 275 | public void setEndC(float endC) { 276 | this.endC = endC; 277 | } 278 | 279 | public boolean isSelected() { 280 | return selected; 281 | } 282 | 283 | public void setSelected(boolean selected) { 284 | this.selected = selected; 285 | } 286 | 287 | public float getNumber() { 288 | return number; 289 | } 290 | 291 | public void setNumber(float number) { 292 | this.number = number; 293 | } 294 | 295 | public int getColorRes() { 296 | return colorRes; 297 | } 298 | 299 | public void setColorRes(int colorRes) { 300 | this.colorRes = colorRes; 301 | } 302 | } 303 | 304 | 305 | } 306 | -------------------------------------------------------------------------------- /app/src/main/java/com/chx/piechartdemo/MyPieChart2.java: -------------------------------------------------------------------------------- 1 | package com.chx.piechartdemo; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.Color; 6 | import android.graphics.Paint; 7 | import android.graphics.RectF; 8 | import android.support.annotation.Nullable; 9 | import android.util.AttributeSet; 10 | import android.view.MotionEvent; 11 | import android.view.View; 12 | import android.view.ViewGroup; 13 | import android.widget.EditText; 14 | import android.widget.RelativeLayout; 15 | 16 | import java.text.DecimalFormat; 17 | import java.util.ArrayList; 18 | import java.util.List; 19 | 20 | /** 21 | * Created by chaohx on 2017/8/9. 22 | */ 23 | 24 | public class MyPieChart2 extends RelativeLayout { 25 | 26 | private List pieEntries; 27 | 28 | private Paint paint; //画笔 29 | 30 | private float centerX; //中心点坐标 x 31 | private float centerY; //中心点坐标 y 32 | private float radius; //未选中状态的半径 33 | private float sRadius; //选中状态的半径 34 | 35 | private OnItemClickListener listener; //点击事件的回调 36 | 37 | public void setOnItemClickListener(OnItemClickListener listener) { 38 | this.listener = listener; 39 | } 40 | 41 | public MyPieChart2(Context context) { 42 | super(context); 43 | init(); 44 | } 45 | 46 | public MyPieChart2(Context context, @Nullable AttributeSet attrs) { 47 | super(context, attrs); 48 | init(); 49 | } 50 | 51 | public MyPieChart2(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 52 | super(context, attrs, defStyleAttr); 53 | init(); 54 | } 55 | 56 | private void init() { 57 | pieEntries = new ArrayList<>(); 58 | paint = new Paint(); 59 | paint.setTextSize(DensityUtils.sp2px(getContext(), 15)); 60 | paint.setAntiAlias(true); 61 | } 62 | 63 | 64 | public void setRadius(float radius) { 65 | this.sRadius = radius; 66 | } 67 | 68 | /** 69 | * 设置数据并刷新 70 | * 71 | * @param pieEntries 72 | */ 73 | public void setPieEntries(List pieEntries) { 74 | this.pieEntries = pieEntries; 75 | invalidate(); 76 | } 77 | 78 | @Override 79 | protected void onDraw(Canvas canvas) { 80 | super.onDraw(canvas); 81 | //计算总值 82 | float total = 0; 83 | for (int i = 0; i < pieEntries.size(); i++) { 84 | total += pieEntries.get(i).getNumber(); 85 | } 86 | //刷新中心点 和半径 87 | centerX = getPivotX(); 88 | centerY = getPivotY(); 89 | if (sRadius == 0) { //这里做个判断,如果没有通过setRadius方法设置半径,则半径为真个view最小边的一半 90 | sRadius = (getWidth() > getHeight() ? getHeight() / 2 : getWidth() / 2); 91 | } 92 | //计算出两个状态的半径,这里二者相差5dp. 93 | radius = sRadius - DensityUtils.dp2px(getContext(), 5); 94 | 95 | //其实角度设置为0,即x轴正方形 96 | float startC = 0; 97 | //遍历List 开始画扇形 98 | for (int i = 0; i < pieEntries.size(); i++) { 99 | //计算当前扇形扫过的角度 100 | float sweep; 101 | if (total <= 0) { 102 | sweep = 360 / pieEntries.size(); 103 | } else { 104 | sweep = 360 * (pieEntries.get(i).getNumber() / total); 105 | } 106 | //设置当前扇形的颜色 107 | paint.setColor(getResources().getColor(pieEntries.get(i).colorRes)); 108 | //判断当前扇形是否被选中,确定用哪个半径 109 | float radiusT; 110 | if (pieEntries.get(i).isSelected()) { 111 | radiusT = sRadius; 112 | } else { 113 | radiusT = radius; 114 | } 115 | //画扇形的方法 116 | RectF rectF = new RectF(centerX - radiusT, centerY - radiusT, centerX + radiusT, centerY + radiusT); 117 | canvas.drawArc(rectF, startC, sweep, true, paint); 118 | 119 | 120 | if ((pieEntries.get(i).getNumber() > 0 && total > 0) || (total <= 0 && pieEntries.get(i).getNumber() <= 0)) { 121 | //下面是画扇形外围的 短线和百分数值。 122 | float arcCenterC = startC + sweep / 2; //当前扇形弧线的中间点和圆心的连线 与 起始角度的夹角 123 | float arcCenterX = 0; //当前扇形弧线的中间点 的坐标 x 以此点作为短线的起始点 124 | float arcCenterY = 0; //当前扇形弧线的中间点 的坐标 y 125 | 126 | float arcCenterX2 = 0; //这两个点作为短线的结束点 127 | float arcCenterY2 = 0; 128 | //百分百数字的格式 129 | DecimalFormat numberFormat = new DecimalFormat("00.00"); 130 | paint.setColor(Color.BLACK); 131 | 132 | //分象限 利用三角函数 来求出每个短线的起始点和结束点,并画出短线和百分比。 133 | //具体的计算方法看下面图示介绍 134 | if (arcCenterC >= 0 && arcCenterC < 90) { 135 | arcCenterX = (float) (centerX + radiusT * Math.cos(arcCenterC * Math.PI / 180)); 136 | arcCenterY = (float) (centerY + radiusT * Math.sin(arcCenterC * Math.PI / 180)); 137 | arcCenterX2 = (float) (arcCenterX + DensityUtils.dp2px(getContext(), 10) * Math.cos(arcCenterC * Math.PI / 180)); 138 | arcCenterY2 = (float) (arcCenterY + DensityUtils.dp2px(getContext(), 10) * Math.sin(arcCenterC * Math.PI / 180)); 139 | canvas.drawLine(arcCenterX, arcCenterY, arcCenterX2, arcCenterY2, paint); 140 | if (total <= 0) { 141 | canvas.drawText(numberFormat.format(0) + "%", arcCenterX2, arcCenterY2 + paint.getTextSize() / 2, paint); 142 | } else { 143 | canvas.drawText(numberFormat.format(pieEntries.get(i).getNumber() / total * 100) + "%", arcCenterX2, arcCenterY2 + paint.getTextSize() / 2, paint); 144 | } 145 | } else if (arcCenterC >= 90 && arcCenterC < 180) { 146 | arcCenterC = 180 - arcCenterC; 147 | arcCenterX = (float) (centerX - radiusT * Math.cos(arcCenterC * Math.PI / 180)); 148 | arcCenterY = (float) (centerY + radiusT * Math.sin(arcCenterC * Math.PI / 180)); 149 | arcCenterX2 = (float) (arcCenterX - DensityUtils.dp2px(getContext(), 10) * Math.cos(arcCenterC * Math.PI / 180)); 150 | arcCenterY2 = (float) (arcCenterY + DensityUtils.dp2px(getContext(), 10) * Math.sin(arcCenterC * Math.PI / 180)); 151 | canvas.drawLine(arcCenterX, arcCenterY, arcCenterX2, arcCenterY2, paint); 152 | if (total <= 0) { 153 | canvas.drawText(numberFormat.format(0) + "%", (float) (arcCenterX2 - paint.getTextSize() * 3.5), arcCenterY2 + paint.getTextSize() / 2, paint); 154 | } else { 155 | canvas.drawText(numberFormat.format(pieEntries.get(i).getNumber() / total * 100) + "%", (float) (arcCenterX2 - paint.getTextSize() * 3.5), arcCenterY2 + paint.getTextSize() / 2, paint); 156 | } 157 | } else if (arcCenterC >= 180 && arcCenterC < 270) { 158 | arcCenterC = 270 - arcCenterC; 159 | arcCenterX = (float) (centerX - radiusT * Math.sin(arcCenterC * Math.PI / 180)); 160 | arcCenterY = (float) (centerY - radiusT * Math.cos(arcCenterC * Math.PI / 180)); 161 | arcCenterX2 = (float) (arcCenterX - DensityUtils.dp2px(getContext(), 10) * Math.sin(arcCenterC * Math.PI / 180)); 162 | arcCenterY2 = (float) (arcCenterY - DensityUtils.dp2px(getContext(), 10) * Math.cos(arcCenterC * Math.PI / 180)); 163 | canvas.drawLine(arcCenterX, arcCenterY, arcCenterX2, arcCenterY2, paint); 164 | if (total <= 0) { 165 | canvas.drawText(numberFormat.format(0) + "%", (float) (arcCenterX2 - paint.getTextSize() * 3.5), arcCenterY2, paint); 166 | } else { 167 | canvas.drawText(numberFormat.format(pieEntries.get(i).getNumber() / total * 100) + "%", (float) (arcCenterX2 - paint.getTextSize() * 3.5), arcCenterY2, paint); 168 | } 169 | } else if (arcCenterC >= 270 && arcCenterC < 360) { 170 | arcCenterC = 360 - arcCenterC; 171 | arcCenterX = (float) (centerX + radiusT * Math.cos(arcCenterC * Math.PI / 180)); 172 | arcCenterY = (float) (centerY - radiusT * Math.sin(arcCenterC * Math.PI / 180)); 173 | arcCenterX2 = (float) (arcCenterX + DensityUtils.dp2px(getContext(), 10) * Math.cos(arcCenterC * Math.PI / 180)); 174 | arcCenterY2 = (float) (arcCenterY - DensityUtils.dp2px(getContext(), 10) * Math.sin(arcCenterC * Math.PI / 180)); 175 | canvas.drawLine(arcCenterX, arcCenterY, arcCenterX2, arcCenterY2, paint); 176 | if (total <= 0) { 177 | canvas.drawText(numberFormat.format(0) + "%", arcCenterX2, arcCenterY2, paint); 178 | } else { 179 | canvas.drawText(numberFormat.format(pieEntries.get(i).getNumber() / total * 100) + "%", arcCenterX2, arcCenterY2, paint); 180 | } 181 | } 182 | } 183 | //将每个扇形的起始角度 和 结束角度 放入对应的对象 184 | pieEntries.get(i).setStartC(startC); 185 | pieEntries.get(i).setEndC(startC + sweep); 186 | //将当前扇形的结束角度作为下一个扇形的起始角度 187 | startC += sweep; 188 | } 189 | } 190 | 191 | @Override 192 | public boolean onTouchEvent(MotionEvent event) { 193 | float touchX; 194 | float touchY; 195 | switch (event.getAction()) { 196 | case MotionEvent.ACTION_DOWN: 197 | touchX = event.getX(); //touch点的坐标 198 | touchY = event.getY(); 199 | //判断touch点到圆心的距离 是否小于半径 200 | if (Math.pow(touchX - centerX, 2) + Math.pow(touchY - centerY, 2) <= Math.pow(radius, 2)) { 201 | //计算 touch点和圆心的连线 与 x轴正方向的夹角 202 | float touchC = getSweep(touchX, touchY); 203 | //遍历 List 判断touch点在哪个扇形中 204 | for (int i = 0; i < pieEntries.size(); i++) { 205 | if (touchC >= pieEntries.get(i).getStartC() && touchC < pieEntries.get(i).getEndC()) { 206 | pieEntries.get(i).setSelected(true); 207 | if (listener != null) 208 | listener.onItemClick(i); //将被点击的扇形id回调出去 209 | } else { 210 | pieEntries.get(i).setSelected(false); 211 | } 212 | } 213 | invalidate();//刷新画布 214 | } else { 215 | EditText editText = new EditText(getContext()); 216 | editText.setHint("请在此输入"); 217 | editText.setTextSize(DensityUtils.sp2px(getContext(), 3)); 218 | editText.setBackgroundResource(R.drawable.edit_bg_stroke); 219 | LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 220 | layoutParams.leftMargin = (int) touchX; 221 | layoutParams.topMargin = (int) touchY; 222 | addView(editText, layoutParams); 223 | } 224 | break; 225 | } 226 | return super.onTouchEvent(event); 227 | } 228 | 229 | /** 230 | * 获取 touch点/圆心连线 与 x轴正方向 的夹角 231 | * 232 | * @param touchX 233 | * @param touchY 234 | */ 235 | private float getSweep(float touchX, float touchY) { 236 | float xZ = touchX - centerX; 237 | float yZ = touchY - centerY; 238 | float a = Math.abs(xZ); 239 | float b = Math.abs(yZ); 240 | double c = Math.toDegrees(Math.atan(b / a)); 241 | if (xZ >= 0 && yZ >= 0) {//第一象限 242 | return (float) c; 243 | } else if (xZ <= 0 && yZ >= 0) {//第二象限 244 | return 180 - (float) c; 245 | } else if (xZ <= 0 && yZ <= 0) {//第三象限 246 | return (float) c + 180; 247 | } else {//第四象限 248 | return 360 - (float) c; 249 | } 250 | } 251 | 252 | public interface OnItemClickListener { 253 | public void onItemClick(int position); 254 | } 255 | 256 | 257 | /** 258 | * 每个扇形的对象 259 | */ 260 | public static class PieEntry { 261 | private float number; //数值 262 | private int colorRes; //颜色资源 263 | private boolean selected; //是否选中 264 | private float startC; //对应扇形起始角度 265 | private float endC; //对应扇形结束角度 266 | 267 | public PieEntry(float number, int colorRes, boolean selected) { 268 | this.number = number; //防止分母为零 269 | this.colorRes = colorRes; 270 | this.selected = selected; 271 | } 272 | 273 | public float getStartC() { 274 | return startC; 275 | } 276 | 277 | public void setStartC(float startC) { 278 | this.startC = startC; 279 | } 280 | 281 | public float getEndC() { 282 | return endC; 283 | } 284 | 285 | public void setEndC(float endC) { 286 | this.endC = endC; 287 | } 288 | 289 | public boolean isSelected() { 290 | return selected; 291 | } 292 | 293 | public void setSelected(boolean selected) { 294 | this.selected = selected; 295 | } 296 | 297 | public float getNumber() { 298 | return number; 299 | } 300 | 301 | public void setNumber(float number) { 302 | this.number = number; 303 | } 304 | 305 | public int getColorRes() { 306 | return colorRes; 307 | } 308 | 309 | public void setColorRes(int colorRes) { 310 | this.colorRes = colorRes; 311 | } 312 | } 313 | 314 | 315 | } 316 | -------------------------------------------------------------------------------- /app/src/main/java/com/chx/piechartdemo/MyView.java: -------------------------------------------------------------------------------- 1 | package com.chx.piechartdemo; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.Color; 6 | import android.graphics.Paint; 7 | import android.util.AttributeSet; 8 | import android.util.Log; 9 | import android.view.MotionEvent; 10 | import android.view.View; 11 | import android.widget.RelativeLayout; 12 | 13 | import java.util.ArrayList; 14 | import java.util.LinkedList; 15 | import java.util.List; 16 | 17 | /** 18 | * Created by chaohx on 2017/8/25. 19 | */ 20 | 21 | public class MyView extends View { 22 | 23 | 24 | private LinkedList entries; 25 | 26 | private Paint paint; //画笔 27 | 28 | 29 | public MyView(Context context) { 30 | super(context); 31 | init(); 32 | } 33 | 34 | public MyView(Context context, AttributeSet attrs) { 35 | super(context, attrs); 36 | init(); 37 | } 38 | 39 | public MyView(Context context, AttributeSet attrs, int defStyleAttr) { 40 | super(context, attrs, defStyleAttr); 41 | init(); 42 | } 43 | 44 | 45 | public void init() { 46 | entries = new LinkedList<>(); 47 | paint = new Paint(); 48 | paint.setTextSize(DensityUtils.sp2px(getContext(), 15)); 49 | paint.setColor(Color.BLACK); 50 | paint.setAntiAlias(true); 51 | } 52 | 53 | 54 | @Override 55 | protected void onDraw(Canvas canvas) { 56 | super.onDraw(canvas); 57 | for (int i = 0; i < entries.size(); i++) { 58 | canvas.drawText(entries.get(i).getStr(), entries.get(i).x, entries.get(i).y, paint); 59 | } 60 | } 61 | 62 | @Override 63 | public boolean onTouchEvent(MotionEvent event) { 64 | 65 | switch (event.getAction()) { 66 | case MotionEvent.ACTION_DOWN://按下 67 | Log.e("onTouchEvent", "onTouchEvent: ACTION_DOWN"); 68 | break; 69 | 70 | case MotionEvent.ACTION_MOVE://移动 71 | Log.e("onTouchEvent", "onTouchEvent: ACTION_MOVE"); 72 | if (entries.size() > 0) { 73 | entries.getLast().setX(event.getX()); 74 | entries.getLast().setY(event.getY()); 75 | invalidate(); 76 | } 77 | break; 78 | case MotionEvent.ACTION_UP://松开 79 | Log.e("onTouchEvent", "onTouchEvent: ACTION_UP"); 80 | break; 81 | } 82 | return true; 83 | } 84 | 85 | public void aDDEntry(String s) { 86 | entries.add(new Entry(s, 100, 100)); 87 | invalidate(); 88 | } 89 | 90 | 91 | /** 92 | * 每一条文字的对象 93 | */ 94 | public static class Entry { 95 | private String str = ""; 96 | private float x; 97 | private float y; 98 | 99 | 100 | public Entry(String str, float x, float y) { 101 | this.str = str; 102 | this.x = x; 103 | this.y = y; 104 | } 105 | 106 | public String getStr() { 107 | return str; 108 | } 109 | 110 | public void setStr(String str) { 111 | this.str = str; 112 | } 113 | 114 | public float getX() { 115 | return x; 116 | } 117 | 118 | public void setX(float x) { 119 | this.x = x; 120 | } 121 | 122 | public float getY() { 123 | return y; 124 | } 125 | 126 | public void setY(float y) { 127 | this.y = y; 128 | } 129 | } 130 | 131 | 132 | } 133 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/edit_bg_stroke.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main2.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 15 | 16 |