├── .gitignore ├── LICENSE.md ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── jeanboy │ │ └── linechart │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── jeanboy │ │ │ └── linechart │ │ │ ├── LineChartView.java │ │ │ └── MainActivity.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 │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── jeanboy │ └── linechart │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── resources ├── Screenshot_20170613-183802.jpg ├── Screenshot_20170613-183803.jpg ├── anim.gif ├── change.gif └── operate.gif └── 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 11 | /.gradle 12 | /gradlew.bat 13 | /gradlew 14 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android-LineChart 2 | 3 | ![](https://img.shields.io/badge/platform-Android-brightgreen.svg) ![](https://img.shields.io/badge/language-java-yellow.svg) ![](https://img.shields.io/badge/license-Apache--2.0-blue.svg) 4 | 5 | ## 介绍 6 | 7 | 一个简单的折线,贝塞尔曲线图表控件,高度可扩展,支持动态显示。 8 | 9 | ## 效果图 10 | 11 | ![演示][1] ![演示][2] ![演示][3] 12 | 13 | ![演示][4] ![演示][5] 14 | 15 | ## 使用 16 | 17 | 1. 设置布局 18 | ```XML 19 | 24 | 25 | 29 | 30 | 34 | 35 | 36 | 37 | ``` 38 | 39 | 2. 添加数据 40 | ```Java 41 | lineChartView.setData(datas); 42 | ``` 43 | 44 | 3. 修改Y轴标尺间隔 45 | ```Java 46 | lineChartView.setRulerYSpace(value); 47 | ``` 48 | 49 | 4. 修改X轴标尺间隔(锚点间距) 50 | ```Java 51 | lineChartView.setStepSpace(value); 52 | ``` 53 | 54 | 5. 设置是否显示表格 55 | ```Java 56 | lineChartView.setShowTable(isShowTable); 57 | ``` 58 | 59 | 6. 设置是否为贝塞尔曲线 60 | ```Java 61 | lineChartView.setBezierLine(isBezier); 62 | ``` 63 | 64 | 7. 设置锚点是否为方形 65 | ```Java 66 | lineChartView.setCubePoint(isCube); 67 | ``` 68 | 69 | 8. 播放动画 70 | ```Java 71 | lineChartView.playAnim(); 72 | ``` 73 | 74 | ## 关于我 75 | 76 | 如果对你有帮助,请 star 一下,然后 follow 我,给我增加一下分享动力,谢谢! 77 | 78 | 如果你有什么疑问或者问题,可以提交 issue 和 request,发邮件给我 jeanboy@foxmail.com 。 79 | 80 | 或者加入下面的 QQ 群来一起学习交流。 81 | 82 | Android技术进阶:386463747 83 | 84 | ## License 85 | 86 | Copyright 2017 jeanboy 87 | 88 | Licensed under the Apache License, Version 2.0 (the "License"); 89 | you may not use this file except in compliance with the License. 90 | You may obtain a copy of the License at 91 | 92 | http://www.apache.org/licenses/LICENSE-2.0 93 | 94 | Unless required by applicable law or agreed to in writing, software 95 | distributed under the License is distributed on an "AS IS" BASIS, 96 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 97 | See the License for the specific language governing permissions and 98 | limitations under the License. 99 | 100 | [1]:https://github.com/jeanboydev/Android-LineChart/blob/master/resources/anim.gif 101 | [2]:https://github.com/jeanboydev/Android-LineChart/blob/master/resources/change.gif 102 | [3]:https://github.com/jeanboydev/Android-LineChart/blob/master/resources/operate.gif 103 | [4]:https://github.com/jeanboydev/Android-LineChart/blob/master/resources/Screenshot_20170613-183802.jpg 104 | [5]:https://github.com/jeanboydev/Android-LineChart/blob/master/resources/Screenshot_20170613-183803.jpg 105 | -------------------------------------------------------------------------------- /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.jeanboy.linechart" 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 D:\Develop\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/androidTest/java/com/jeanboy/linechart/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.jeanboy.linechart; 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.jeanboy.linechart", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/jeanboy/linechart/LineChartView.java: -------------------------------------------------------------------------------- 1 | package com.jeanboy.linechart; 2 | 3 | import android.animation.Animator; 4 | import android.animation.AnimatorListenerAdapter; 5 | import android.animation.ValueAnimator; 6 | import android.content.Context; 7 | import android.graphics.Canvas; 8 | import android.graphics.Color; 9 | import android.graphics.Paint; 10 | import android.graphics.Path; 11 | import android.graphics.PathMeasure; 12 | import android.graphics.Point; 13 | import android.util.AttributeSet; 14 | import android.view.View; 15 | import android.view.animation.AccelerateDecelerateInterpolator; 16 | 17 | import java.util.ArrayList; 18 | import java.util.Collections; 19 | import java.util.Comparator; 20 | import java.util.List; 21 | 22 | /** 23 | * Created by jeanboy on 2017/6/12. 24 | */ 25 | 26 | public class LineChartView extends View { 27 | 28 | private Paint linePaint;//曲线画笔 29 | private Paint pointPaint;//曲线上锚点画笔 30 | private Paint tablePaint;//表格画笔 31 | private Paint textRulerPaint;//标尺文本画笔 32 | private Paint textPointPaint;//曲线上锚点文本画笔 33 | 34 | private Path linePath;//曲线路径 35 | private Path tablePath;//表格路径 36 | 37 | private int mWidth, mHeight; 38 | 39 | private List dataList = new ArrayList<>(); 40 | 41 | private Point[] linePoints; 42 | private int stepStart; 43 | private int stepEnd; 44 | private int stepSpace; 45 | private int stepSpaceDefault = 10; 46 | private int stepSpaceDP = stepSpaceDefault;//item宽度默认dp 47 | private int topSpace, bottomSpace; 48 | private int tablePadding; 49 | private int tablePaddingDP = 20;//view四周padding默认dp 50 | 51 | private int maxValue, minValue; 52 | private int rulerValueDefault = 10; 53 | private int rulerValue = rulerValueDefault;//刻度单位跨度 54 | private int rulerValuePadding;//刻度单位与轴的间距 55 | private int rulerValuePaddingDP = 8;//刻度单位与轴的间距默认dp 56 | private float heightPercent = 0.618f; 57 | 58 | private int lineColor = Color.parseColor("#286DD4");//曲线颜色 59 | private float lineWidthDP = 2f;//曲线宽度dp 60 | 61 | private int pointColor = Color.parseColor("#FF4081");//锚点颜色 62 | private float pointWidthDefault = 8f; 63 | private float pointWidthDP = pointWidthDefault;//锚点宽度dp 64 | 65 | private int tableColor = Color.parseColor("#BBBBBB");//表格线颜色 66 | private float tableWidthDP = 0.5f;//表格线宽度dp 67 | 68 | private int rulerTextColor = tableColor;//表格标尺文本颜色 69 | private float rulerTextSizeSP = 10f;//表格标尺文本大小 70 | 71 | private int pointTextColor = Color.parseColor("#009688");//锚点文本颜色 72 | private float pointTextSizeSP = 10f;//锚点文本大小 73 | 74 | private boolean isShowTable = false; 75 | private boolean isBezierLine = false; 76 | private boolean isCubePoint = false; 77 | private boolean isInitialized = false; 78 | private boolean isPlayAnim = false; 79 | 80 | private ValueAnimator valueAnimator; 81 | private float currentValue = 0f; 82 | private boolean isAnimating = false; 83 | 84 | public LineChartView(Context context) { 85 | this(context, null); 86 | } 87 | 88 | public LineChartView(Context context, AttributeSet attrs) { 89 | this(context, attrs, 0); 90 | } 91 | 92 | public LineChartView(Context context, AttributeSet attrs, int defStyleAttr) { 93 | super(context, attrs, defStyleAttr); 94 | 95 | setupView(); 96 | } 97 | 98 | private void setupView() { 99 | linePaint = new Paint(); 100 | linePaint.setAntiAlias(true);//抗锯齿 101 | linePaint.setStyle(Paint.Style.STROKE);//STROKE描边FILL填充 102 | linePaint.setColor(lineColor); 103 | linePaint.setStrokeWidth(dip2px(lineWidthDP));//边框宽度 104 | 105 | pointPaint = new Paint(); 106 | pointPaint.setAntiAlias(true); 107 | pointPaint.setStyle(Paint.Style.FILL); 108 | pointPaint.setColor(pointColor); 109 | pointPaint.setStrokeWidth(dip2px(pointWidthDP)); 110 | 111 | tablePaint = new Paint(); 112 | tablePaint.setAntiAlias(true); 113 | tablePaint.setStyle(Paint.Style.STROKE); 114 | tablePaint.setColor(tableColor); 115 | tablePaint.setStrokeWidth(dip2px(tableWidthDP)); 116 | 117 | textRulerPaint = new Paint(); 118 | textRulerPaint.setAntiAlias(true); 119 | textRulerPaint.setStyle(Paint.Style.FILL); 120 | textRulerPaint.setTextAlign(Paint.Align.CENTER); 121 | textRulerPaint.setColor(rulerTextColor);//文本颜色 122 | textRulerPaint.setTextSize(sp2px(rulerTextSizeSP));//字体大小 123 | 124 | textPointPaint = new Paint(); 125 | textPointPaint.setAntiAlias(true); 126 | textPointPaint.setStyle(Paint.Style.FILL); 127 | textPointPaint.setTextAlign(Paint.Align.CENTER); 128 | textPointPaint.setColor(pointTextColor);//文本颜色 129 | textPointPaint.setTextSize(sp2px(pointTextSizeSP));//字体大小 130 | 131 | linePath = new Path(); 132 | tablePath = new Path(); 133 | 134 | resetParam(); 135 | } 136 | 137 | private void initAnim() { 138 | valueAnimator = ValueAnimator.ofFloat(0f, 1f).setDuration(dataList.size() * 150); 139 | valueAnimator.setInterpolator(new AccelerateDecelerateInterpolator()); 140 | valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 141 | @Override 142 | public void onAnimationUpdate(ValueAnimator animation) { 143 | currentValue = (float) animation.getAnimatedValue(); 144 | postInvalidate(); 145 | } 146 | }); 147 | valueAnimator.addListener(new AnimatorListenerAdapter() { 148 | 149 | @Override 150 | public void onAnimationStart(Animator animation) { 151 | super.onAnimationStart(animation); 152 | currentValue = 0f; 153 | isAnimating = true; 154 | } 155 | 156 | @Override 157 | public void onAnimationEnd(Animator animation) { 158 | super.onAnimationEnd(animation); 159 | currentValue = 1f; 160 | isAnimating = false; 161 | isPlayAnim = false; 162 | } 163 | }); 164 | valueAnimator.setStartDelay(500); 165 | } 166 | 167 | private void resetParam() { 168 | linePath.reset(); 169 | tablePath.reset(); 170 | stepSpace = dip2px(stepSpaceDP); 171 | tablePadding = dip2px(tablePaddingDP); 172 | rulerValuePadding = dip2px(rulerValuePaddingDP); 173 | stepStart = tablePadding * (isShowTable ? 2 : 1); 174 | stepEnd = stepStart + stepSpace * (dataList.size() - 1); 175 | topSpace = bottomSpace = tablePadding; 176 | linePoints = new Point[dataList.size()]; 177 | 178 | initAnim(); 179 | isInitialized = false; 180 | } 181 | 182 | @Override 183 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 184 | int width = tablePadding + getTableEnd() + getPaddingLeft() + getPaddingRight();//计算自己的宽度 185 | int heightMode = MeasureSpec.getMode(heightMeasureSpec); 186 | int height = MeasureSpec.getSize(heightMeasureSpec);//父类期望的高度 187 | if (MeasureSpec.EXACTLY == heightMode) { 188 | height = getPaddingTop() + getPaddingBottom() + height; 189 | } 190 | setMeasuredDimension(width, height);//设置自己的宽度和高度 191 | } 192 | 193 | @Override 194 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 195 | super.onSizeChanged(w, h, oldw, oldh); 196 | mWidth = w; 197 | mHeight = h; 198 | } 199 | 200 | @Override 201 | protected void onFinishInflate() { 202 | super.onFinishInflate(); 203 | } 204 | 205 | @Override 206 | protected void onDraw(Canvas canvas) { 207 | super.onDraw(canvas); 208 | canvas.drawColor(Color.TRANSPARENT);//绘制背景颜色 209 | canvas.translate(0f, mHeight / 2f + (getViewDrawHeight() + topSpace + bottomSpace) / 2f);//设置画布中心点垂直居中 210 | 211 | if (!isInitialized) { 212 | setupLine(); 213 | } 214 | 215 | if (isShowTable) { 216 | drawTable(canvas);//绘制表格 217 | } 218 | drawLine(canvas);//绘制曲线 219 | drawLinePoints(canvas);//绘制曲线上的点 220 | } 221 | 222 | private void drawText(Canvas canvas, Paint textPaint, String text, float x, float y) { 223 | canvas.drawText(text, x, y, textPaint); 224 | } 225 | 226 | /** 227 | * 绘制标尺y轴文本 228 | * 229 | * @param canvas 230 | * @param text 231 | * @param x 232 | * @param y 233 | */ 234 | private void drawRulerYText(Canvas canvas, String text, float x, float y) { 235 | textRulerPaint.setTextAlign(Paint.Align.RIGHT); 236 | Paint.FontMetrics fontMetrics = textRulerPaint.getFontMetrics(); 237 | float fontTotalHeight = fontMetrics.bottom - fontMetrics.top; 238 | float offsetY = fontTotalHeight / 2 - fontMetrics.bottom; 239 | float newY = y + offsetY; 240 | float newX = x - rulerValuePadding; 241 | drawText(canvas, textRulerPaint, text, newX, newY); 242 | } 243 | 244 | /** 245 | * 绘制标尺x轴文本 246 | * 247 | * @param canvas 248 | * @param text 249 | * @param x 250 | * @param y 251 | */ 252 | private void drawRulerXText(Canvas canvas, String text, float x, float y) { 253 | textRulerPaint.setTextAlign(Paint.Align.CENTER); 254 | Paint.FontMetrics fontMetrics = textRulerPaint.getFontMetrics(); 255 | float fontTotalHeight = fontMetrics.bottom - fontMetrics.top; 256 | float offsetY = fontTotalHeight / 2 - fontMetrics.bottom; 257 | float newY = y + offsetY + rulerValuePadding; 258 | drawText(canvas, textRulerPaint, text, x, newY); 259 | } 260 | 261 | /** 262 | * 绘制曲线上锚点文本 263 | * 264 | * @param canvas 265 | * @param text 266 | * @param x 267 | * @param y 268 | */ 269 | private void drawLinePointText(Canvas canvas, String text, float x, float y) { 270 | textPointPaint.setTextAlign(Paint.Align.CENTER); 271 | float newY = y - rulerValuePadding; 272 | drawText(canvas, textPointPaint, text, x, newY); 273 | } 274 | 275 | private int getTableStart() { 276 | return isShowTable ? stepStart + tablePadding : stepStart; 277 | } 278 | 279 | private int getTableEnd() { 280 | return isShowTable ? stepEnd + tablePadding : stepEnd; 281 | } 282 | 283 | /** 284 | * 绘制背景表格 285 | * 286 | * @param canvas 287 | */ 288 | private void drawTable(Canvas canvas) { 289 | int tableEnd = getTableEnd(); 290 | 291 | int rulerCount = maxValue / rulerValue; 292 | int rulerMaxCount = maxValue % rulerValue > 0 ? rulerCount + 1 : rulerCount; 293 | int rulerMax = rulerValue * rulerMaxCount + rulerValueDefault; 294 | 295 | tablePath.moveTo(stepStart, -getValueHeight(rulerMax));//加上顶部的间隔 296 | tablePath.lineTo(stepStart, 0);//标尺y轴 297 | tablePath.lineTo(tableEnd, 0);//标尺x轴 298 | 299 | int startValue = minValue - (minValue > 0 ? 0 : minValue % rulerValue); 300 | int endValue = (maxValue + rulerValue); 301 | 302 | //标尺y轴连接线 303 | do { 304 | int startHeight = -getValueHeight(startValue); 305 | tablePath.moveTo(stepStart, startHeight); 306 | tablePath.lineTo(tableEnd, startHeight); 307 | //绘制y轴刻度单位 308 | drawRulerYText(canvas, String.valueOf(startValue), stepStart, startHeight); 309 | startValue += rulerValue; 310 | } while (startValue < endValue); 311 | 312 | canvas.drawPath(tablePath, tablePaint); 313 | //绘制x轴刻度单位 314 | drawRulerXValue(canvas); 315 | } 316 | 317 | /** 318 | * 绘制标尺x轴上所有文本 319 | * 320 | * @param canvas 321 | */ 322 | private void drawRulerXValue(Canvas canvas) { 323 | if (linePoints == null) return; 324 | for (int i = 0; i < linePoints.length; i++) { 325 | Point point = linePoints[i]; 326 | if (point == null) break; 327 | drawRulerXText(canvas, String.valueOf(i), linePoints[i].x, 0); 328 | } 329 | } 330 | 331 | /** 332 | * 绘制曲线 333 | * 334 | * @param canvas 335 | */ 336 | private void drawLine(Canvas canvas) { 337 | if (isPlayAnim) { 338 | Path dst = new Path(); 339 | PathMeasure measure = new PathMeasure(linePath, false); 340 | measure.getSegment(0, currentValue * measure.getLength(), dst, true); 341 | canvas.drawPath(dst, linePaint); 342 | } else { 343 | canvas.drawPath(linePath, linePaint); 344 | } 345 | } 346 | 347 | /** 348 | * 绘制曲线上的锚点 349 | * 350 | * @param canvas 351 | */ 352 | private void drawLinePoints(Canvas canvas) { 353 | if (linePoints == null) return; 354 | 355 | float pointWidth = dip2px(pointWidthDP) / 2; 356 | int pointCount = linePoints.length; 357 | if (isPlayAnim) { 358 | pointCount = Math.round(currentValue * linePoints.length); 359 | } 360 | for (int i = 0; i < pointCount; i++) { 361 | Point point = linePoints[i]; 362 | if (point == null) break; 363 | if (isCubePoint) { 364 | canvas.drawPoint(point.x, point.y, pointPaint); 365 | } else { 366 | canvas.drawCircle(point.x, point.y, pointWidth, pointPaint); 367 | } 368 | //绘制点的文本 369 | drawLinePointText(canvas, String.valueOf(dataList.get(i).getValue()), point.x, point.y); 370 | } 371 | } 372 | 373 | /** 374 | * 获取value值所占的view高度 375 | * 376 | * @param value 377 | * @return 378 | */ 379 | private int getValueHeight(int value) { 380 | float valuePercent = Math.abs(value - minValue) * 100f / (Math.abs(maxValue - minValue) * 100f);//计算value所占百分比 381 | return (int) (getViewDrawHeight() * valuePercent + bottomSpace + 0.5f);//底部加上间隔 382 | } 383 | 384 | /** 385 | * 获取绘制区域高度 386 | * 387 | * @return 388 | */ 389 | private float getViewDrawHeight() { 390 | return getMeasuredHeight() * heightPercent; 391 | } 392 | 393 | /** 394 | * 初始化曲线数据 395 | */ 396 | private void setupLine() { 397 | if (dataList.isEmpty()) return; 398 | 399 | int stepTemp = getTableStart(); 400 | Point pre = new Point(); 401 | pre.set(stepTemp, -getValueHeight(dataList.get(0).getValue()));//坐标系从0,0默认在第四象限绘制 402 | linePoints[0] = pre; 403 | linePath.moveTo(pre.x, pre.y); 404 | 405 | if (dataList.size() == 1) { 406 | isInitialized = true; 407 | return; 408 | } 409 | 410 | for (int i = 1; i < dataList.size(); i++) { 411 | Data data = dataList.get(i); 412 | Point next = new Point(); 413 | next.set(stepTemp += stepSpace, -getValueHeight(data.getValue())); 414 | 415 | if (isBezierLine) { 416 | int cW = pre.x + stepSpace / 2; 417 | 418 | Point p1 = new Point();//控制点1 419 | p1.set(cW, pre.y); 420 | 421 | Point p2 = new Point();//控制点2 422 | p2.set(cW, next.y); 423 | 424 | linePath.cubicTo(p1.x, p1.y, p2.x, p2.y, next.x, next.y);//创建三阶贝塞尔曲线 425 | } else { 426 | linePath.lineTo(next.x, next.y); 427 | } 428 | 429 | pre = next; 430 | linePoints[i] = next; 431 | } 432 | 433 | isInitialized = true; 434 | } 435 | 436 | private int dip2px(float dipValue) { 437 | final float scale = getResources().getDisplayMetrics().density; 438 | return (int) (dipValue * scale + 0.5f); 439 | } 440 | 441 | private int sp2px(float spValue) { 442 | final float fontScale = getResources().getDisplayMetrics().scaledDensity; 443 | return (int) (spValue * fontScale + 0.5f); 444 | } 445 | 446 | private void refreshLayout() { 447 | resetParam(); 448 | requestLayout(); 449 | postInvalidate(); 450 | } 451 | 452 | /*-------------可操作方法---------------*/ 453 | 454 | /** 455 | * 设置数据 456 | * 457 | * @param dataList 458 | */ 459 | public void setData(List dataList) { 460 | if (dataList == null) { 461 | throw new RuntimeException("dataList cannot is null!"); 462 | } 463 | if (dataList.isEmpty()) return; 464 | this.dataList.clear(); 465 | this.dataList.addAll(dataList); 466 | 467 | maxValue = Collections.max(this.dataList, new Comparator() { 468 | @Override 469 | public int compare(Data o1, Data o2) { 470 | return o1.getValue() - o2.getValue(); 471 | } 472 | }).getValue(); 473 | 474 | minValue = Collections.min(this.dataList, new Comparator() { 475 | @Override 476 | public int compare(Data o1, Data o2) { 477 | return o1.getValue() - o2.getValue(); 478 | } 479 | }).getValue(); 480 | 481 | refreshLayout(); 482 | } 483 | 484 | /** 485 | * 设置是否显示标尺表格 486 | * 487 | * @param showTable 488 | */ 489 | public void setShowTable(boolean showTable) { 490 | isShowTable = showTable; 491 | refreshLayout(); 492 | } 493 | 494 | /** 495 | * 设置是否是贝塞尔曲线 496 | * 497 | * @param isBezier 498 | */ 499 | public void setBezierLine(boolean isBezier) { 500 | isBezierLine = isBezier; 501 | refreshLayout(); 502 | } 503 | 504 | /** 505 | * 设置锚点形状 506 | * 507 | * @param isCube 508 | */ 509 | public void setCubePoint(boolean isCube) { 510 | isCubePoint = isCube; 511 | refreshLayout(); 512 | } 513 | 514 | /** 515 | * 设置标尺y轴间距 516 | * 517 | * @param space 518 | */ 519 | public void setRulerYSpace(int space) { 520 | if (space <= 0) { 521 | space = rulerValueDefault; 522 | } 523 | this.rulerValue = space; 524 | refreshLayout(); 525 | } 526 | 527 | /** 528 | * 设置曲线点的间距,标尺x轴间距 529 | * 530 | * @param dp 531 | */ 532 | public void setStepSpace(int dp) { 533 | if (dp < stepSpaceDefault) { 534 | dp = stepSpaceDefault; 535 | } 536 | this.stepSpaceDP = dp; 537 | refreshLayout(); 538 | } 539 | 540 | /** 541 | * 设置锚点尺寸 542 | * 543 | * @param dp 544 | */ 545 | public void setPointWidth(float dp) { 546 | if (dp <= 0) { 547 | dp = pointWidthDefault; 548 | } 549 | this.pointWidthDP = dp; 550 | refreshLayout(); 551 | } 552 | 553 | /** 554 | * 播放动画 555 | */ 556 | public void playAnim() { 557 | this.isPlayAnim = true; 558 | if (isAnimating) return; 559 | if (valueAnimator != null) { 560 | valueAnimator.start(); 561 | } 562 | } 563 | 564 | public static class Data { 565 | 566 | int value; 567 | 568 | public Data(int value) { 569 | this.value = value; 570 | } 571 | 572 | public int getValue() { 573 | return value; 574 | } 575 | 576 | } 577 | } 578 | -------------------------------------------------------------------------------- /app/src/main/java/com/jeanboy/linechart/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.jeanboy.linechart; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.view.View; 6 | import android.widget.SeekBar; 7 | import android.widget.TextView; 8 | 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | public class MainActivity extends AppCompatActivity { 13 | 14 | TextView tv_ruler_y; 15 | SeekBar sb_ruler_space; 16 | 17 | TextView tv_step_space; 18 | SeekBar sb_step_space; 19 | 20 | 21 | LineChartView lineChartView; 22 | 23 | private int[] dataArr = new int[]{200, 100, 300, -20, 50, -80, 200, 100, 300, 50, 200, 150, 160, 100, 300, 50, 200, 150, 24 | 300, 50, 200, 100, 150, 150}; 25 | 26 | @Override 27 | protected void onCreate(Bundle savedInstanceState) { 28 | super.onCreate(savedInstanceState); 29 | setContentView(R.layout.activity_main); 30 | 31 | lineChartView = (LineChartView) findViewById(R.id.line_chart_view); 32 | sb_ruler_space = (SeekBar) findViewById(R.id.sb_ruler_space); 33 | tv_ruler_y = (TextView) findViewById(R.id.tv_ruler_y); 34 | sb_step_space = (SeekBar) findViewById(R.id.sb_step_space); 35 | tv_step_space = (TextView) findViewById(R.id.tv_step_space); 36 | 37 | List datas = new ArrayList<>(); 38 | for (int value : dataArr) { 39 | LineChartView.Data data = new LineChartView.Data(value); 40 | datas.add(data); 41 | } 42 | lineChartView.setData(datas); 43 | 44 | sb_ruler_space.setMax(70); 45 | sb_ruler_space.setProgress(20); 46 | if (lineChartView != null) { 47 | lineChartView.setRulerYSpace(20); 48 | tv_ruler_y.setText(String.valueOf(20)); 49 | } 50 | sb_ruler_space.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { 51 | @Override 52 | public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 53 | if (lineChartView != null) { 54 | lineChartView.setRulerYSpace(progress); 55 | tv_ruler_y.setText(String.valueOf(progress)); 56 | } 57 | } 58 | 59 | @Override 60 | public void onStartTrackingTouch(SeekBar seekBar) { 61 | 62 | } 63 | 64 | @Override 65 | public void onStopTrackingTouch(SeekBar seekBar) { 66 | 67 | } 68 | }); 69 | 70 | sb_step_space.setMax(70); 71 | sb_step_space.setProgress(15); 72 | if (lineChartView != null) { 73 | lineChartView.setStepSpace(15); 74 | tv_step_space.setText(String.valueOf(15)); 75 | } 76 | sb_step_space.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { 77 | @Override 78 | public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 79 | if (lineChartView != null) { 80 | lineChartView.setStepSpace(progress); 81 | tv_step_space.setText(String.valueOf(progress)); 82 | } 83 | } 84 | 85 | @Override 86 | public void onStartTrackingTouch(SeekBar seekBar) { 87 | 88 | } 89 | 90 | @Override 91 | public void onStopTrackingTouch(SeekBar seekBar) { 92 | 93 | } 94 | }); 95 | } 96 | 97 | private boolean isShowTable = false; 98 | 99 | public void tableToggle(View view) { 100 | if (lineChartView != null) { 101 | isShowTable = !isShowTable; 102 | lineChartView.setShowTable(isShowTable); 103 | } 104 | } 105 | 106 | private boolean isBezier = false; 107 | 108 | public void bezierModelToggle(View view) { 109 | if (lineChartView != null) { 110 | isBezier = !isBezier; 111 | lineChartView.setBezierLine(isBezier); 112 | } 113 | } 114 | 115 | private boolean isCube = false; 116 | 117 | public void pointModelToggle(View view) { 118 | if (lineChartView != null) { 119 | isCube = !isCube; 120 | lineChartView.setCubePoint(isCube); 121 | } 122 | } 123 | 124 | public void doAnimation(View view) { 125 | if (lineChartView != null) { 126 | lineChartView.playAnim(); 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 15 | 16 | 21 | 22 | 26 | 27 | 31 | 32 | 33 | 34 | 35 | 39 | 40 |