Repository: xiesuichao/KLineView Branch: master Commit: 94818338859e Files: 41 Total size: 202.4 KB Directory structure: gitextract_4lzolz3h/ ├── .gitignore ├── .idea/ │ ├── codeStyles/ │ │ └── Project.xml │ ├── gradle.xml │ ├── misc.xml │ ├── runConfigurations.xml │ └── vcs.xml ├── README.md ├── apk/ │ └── app-debug.apk ├── app/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── com/ │ │ └── example/ │ │ └── admin/ │ │ └── klineview/ │ │ └── ExampleInstrumentedTest.java │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── com/ │ │ │ └── example/ │ │ │ └── admin/ │ │ │ └── klineview/ │ │ │ ├── DepthActivity.java │ │ │ ├── MainActivity.java │ │ │ ├── Print.java │ │ │ ├── depth/ │ │ │ │ ├── Depth.java │ │ │ │ └── DepthView.java │ │ │ └── kline/ │ │ │ ├── KData.java │ │ │ ├── KLineView.java │ │ │ ├── Pointer.java │ │ │ ├── QuotaThread.java │ │ │ └── QuotaUtil.java │ │ └── res/ │ │ ├── drawable/ │ │ │ └── ic_launcher_background.xml │ │ ├── drawable-v24/ │ │ │ └── ic_launcher_foreground.xml │ │ ├── layout/ │ │ │ ├── activity_depth.xml │ │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26/ │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ └── values/ │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test/ │ └── java/ │ └── com/ │ └── example/ │ └── admin/ │ └── klineview/ │ └── ExampleUnitTest.java ├── build.gradle ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *.iml .gradle /local.properties /.idea/caches /.idea/libraries /.idea/modules.xml /.idea/workspace.xml /.idea/navEditor.xml /.idea/assetWizardSettings.xml .DS_Store /build /captures .externalNativeBuild .release ================================================ FILE: .idea/codeStyles/Project.xml ================================================
xmlns:android ^$
xmlns:.* ^$ BY_NAME
.*:id http://schemas.android.com/apk/res/android
.*:name http://schemas.android.com/apk/res/android
name ^$
style ^$
.* ^$ BY_NAME
.* http://schemas.android.com/apk/res/android ANDROID_ATTRIBUTE_ORDER
.* .* BY_NAME
================================================ FILE: .idea/gradle.xml ================================================ ================================================ FILE: .idea/misc.xml ================================================ ================================================ FILE: .idea/runConfigurations.xml ================================================ ================================================ FILE: .idea/vcs.xml ================================================ ================================================ FILE: README.md ================================================ # KLineView 股票走势图K线控件 [![](https://jitpack.io/v/xiesuichao/KLineView.svg)](https://jitpack.io/#xiesuichao/KLineView) 主图指标:MA, EMA, BOLL 副图指标:MACD, KDJ, RSI 根目录下有个apk文件夹,内有最新的测试包,可以先安装看效果 新增深度图控件,如下图所示,详情见demo 支持实时刷新最后一条数据。 支持添加最新的单条数据。 支持滑动时的分页加载更多数据。 支持惯性滑动。 支持多指触控缩放。 支持长按拖动。 支持横屏显示 支持xml布局自定义颜色,字体大小属性 已对性能做优化,总数据量十万条以上对用户体验没有影响。 首次加载5000条数据,页面初始化到加载完成,总共耗时400+ms,不超过0.5秒。 分页加载5000条数据时,如果正在滑动过程中,添加数据的那一瞬间会稍微有一下卡顿,影响不大。 经测试,800块的华为荣耀6A 每次添加4000条以下数据不会有卡顿,很流畅。 建议每次添加数据在2000条左右。 已对滑动事件冲突做处理,可上下滑动的父类(ScrollView、NestedScrollView等)无需再考虑滑动冲突 ![image](https://github.com/xiesuichao/KLineView/raw/master/image/KLineUI.png) ![image](https://github.com/xiesuichao/KLineView/raw/master/image/a5.png) ![image](https://github.com/xiesuichao/KLineView/raw/master/image/a2.png) ![image](https://github.com/xiesuichao/KLineView/raw/master/image/a3.png) 1.K线控件: //初始化控件加载数据(仅作初始化用,数据重置请调用resetDataList) mKLineView.initKDataList(getKDataList(5)); //设置十字线移动模式,默认为0:固定指向收盘价 mKLineView.setCrossHairMoveMode(KLineView.CROSS_HAIR_MOVE_FREE); //分页加载时添加多条数据 mKLineView.addDataList(getKDataList(5)); //实时刷新时添加单条数据 mKLineView.addData(getKDataList(0.1).get(0)); @Override public void onClick(View v) { switch (v.getId()){ case R.id.btn_kline_reset: //重置数据,可用于分时加载,是否需要定位到重置前的时间点请看方法注释 //在做分时功能重新加载数据的时候,请务必调用该方法 mKLineView.resetDataList(getKDataList(0.1)); break; case R.id.btn_deputy: //是否显示副图 mKLineView.setDeputyPicShow(!mKLineView.getVicePicShow()); break; case R.id.btn_ma: //主图展示MA mKLineView.setMainImgType(KLineView.MAIN_IMG_MA); break; case R.id.btn_ema: //主图展示EMA mKLineView.setMainImgType(KLineView.MAIN_IMG_EMA); break; case R.id.btn_boll: //主图展示BOLL mKLineView.setMainImgType(KLineView.MAIN_IMG_BOLL); break; case R.id.btn_macd: //副图展示MACD mKLineView.setDeputyImgType(KLineView.DEPUTY_IMG_MACD); break; case R.id.btn_kdj: //副图展示KDJ mKLineView.setDeputyImgType(KLineView.DEPUTY_IMG_KDJ); break; case R.id.btn_rsi: //副图展示RSI mKLineView.setDeputyImgType(KLineView.DEPUTY_IMG_RSI); break; case R.id.btn_depth_activity: //跳转到深度图页面 startActivity(new Intent(getApplicationContext(), DepthActivity.class)); break; } } /** * 当控件显示数据属于总数据量的前三分之一时,会自动调用该接口,用于预加载数据,保证控件操作过程中的流畅性, * 虽然做了预加载,当总数据量较小时,也会出现用户滑到左边界了,但数据还未获取到,依然会有停顿。 * 所以数据量越大,越不会出现停顿,也就越流畅 */ mKLineView.setOnRequestDataListListener(new KLineView.OnRequestDataListListener() { @Override public void requestData() { //请求数据 } }); 2.深度图控件: //添加购买数据 depthView.setBuyDataList(getBuyDepthList()); //添加出售数据 depthView.setSellDataList(getSellDepthList()); //重置深度数据 depthView.resetAllData(getBuyDepthList(), getSellDepthList()); //设置横坐标中间值 depthView.setAbscissaCenterPrice(10.265); //设置数据详情的价钱说明 depthView.setDetailPriceTitle("价格(BTC):"); //设置数据详情的数量说明 depthView.setDetailVolumeTitle("累积交易量:"); //设置横坐标价钱小数位精度 depthView.setPricePrecision(4); //是否显示竖线 depthView.setShowDetailLine(true); //手指单击松开后,数据是否继续显示 depthView.setShowDetailSingleClick(true); //手指长按松开后,数据是否继续显示 depthView.setShowDetailLongPress(true); ================================================ FILE: app/.gitignore ================================================ /build ================================================ FILE: app/build.gradle ================================================ apply plugin: 'com.android.application' android { compileSdkVersion 27 defaultConfig { applicationId "com.example.admin.klineview" minSdkVersion 15 targetSdkVersion 27 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'com.android.support:appcompat-v7:27.1.1' implementation 'com.android.support.constraint:constraint-layout:1.1.2' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' } ================================================ FILE: app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile ================================================ FILE: app/src/androidTest/java/com/example/admin/klineview/ExampleInstrumentedTest.java ================================================ package com.example.admin.klineview; import android.content.Context; import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; import static org.junit.Assert.*; /** * Instrumented test, which will execute on an Android device. * * @see Testing documentation */ @RunWith(AndroidJUnit4.class) public class ExampleInstrumentedTest { @Test public void useAppContext() throws Exception { // Context of the app under test. Context appContext = InstrumentationRegistry.getTargetContext(); assertEquals("com.example.admin.klineview", appContext.getPackageName()); } } ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/java/com/example/admin/klineview/DepthActivity.java ================================================ package com.example.admin.klineview; import android.os.Bundle; import android.os.Handler; import android.support.annotation.Nullable; import android.support.v7.app.AppCompatActivity; import android.view.View; import android.widget.Button; import com.example.admin.klineview.depth.Depth; import com.example.admin.klineview.depth.DepthView; import java.util.ArrayList; import java.util.List; import java.util.Random; /** * 深度图 * Created by xiesuichao on 2018/9/24. */ public class DepthActivity extends AppCompatActivity { private DepthView depthView; private Button depthResetBtn; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_depth); initView(); initData(); setListener(); } private void initView(){ depthView = findViewById(R.id.dv_depth); depthResetBtn = findViewById(R.id.btn_depth_reset); } private void initData(){ //添加购买数据 depthView.setBuyDataList(getBuyDepthList()); //添加出售数据 depthView.setSellDataList(getSellDepthList()); //设置横坐标中间值 depthView.setAbscissaCenterPrice(10.265); //设置数据详情的价钱说明 depthView.setDetailPriceTitle("价格(BTC):"); //设置数据详情的数量说明 depthView.setDetailVolumeTitle("累积交易量:"); //设置横坐标价钱小数位精度 depthView.setPricePrecision(4); //是否显示竖线 depthView.setShowDetailLine(true); //手指单击松开后,数据是否继续显示 depthView.setShowDetailSingleClick(true); //手指长按松开后,数据是否继续显示 depthView.setShowDetailLongPress(true); } private void setListener(){ depthResetBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //重置深度数据 depthView.resetAllData(getBuyDepthList(), getSellDepthList()); } }); } //模拟深度数据 private List getBuyDepthList(){ List depthList = new ArrayList<>(); Random random = new Random(); for (int i = 0; i < 100; i++) { depthList.add(new Depth(100 - random.nextDouble() * 10, random.nextInt(10) * random.nextInt(10) * random.nextInt(10) + random.nextDouble(), 0)); } return depthList; } //模拟深度数据 private List getSellDepthList(){ List depthList = new ArrayList<>(); Random random = new Random(); for (int i = 0; i < 100; i++) { depthList.add(new Depth(100 + random.nextDouble() * 10, random.nextInt(10) * random.nextInt(10) * random.nextInt(10) + random.nextDouble(), 1)); } return depthList; } @Override protected void onDestroy() { super.onDestroy(); depthView.cancelCallback(); } } ================================================ FILE: app/src/main/java/com/example/admin/klineview/MainActivity.java ================================================ package com.example.admin.klineview; import android.content.Intent; import android.content.res.Configuration; import android.os.Handler; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.LinearLayout; import com.example.admin.klineview.depth.Depth; import com.example.admin.klineview.depth.DepthView; import com.example.admin.klineview.kline.KData; import com.example.admin.klineview.kline.KLineView; import java.util.ArrayList; import java.util.List; import java.util.Random; public class MainActivity extends AppCompatActivity implements View.OnClickListener { private Handler mHandler; private KLineView kLineView; private Button deputyBtn, maBtn, emaBtn, bollBtn, macdBtn, kdjBtn, depthJumpBtn, kLineResetBtn, rsiBtn, instantBtn; private Runnable dataListAddRunnable, singleDataAddRunnable; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initView(); initData(); setListener(); //切换横屏适配测试 if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp2px(280)); kLineView.setLayoutParams(params); } else { LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp2px(380)); kLineView.setLayoutParams(params); } } private void initView() { kLineView = findViewById(R.id.klv_main); deputyBtn = findViewById(R.id.btn_deputy); maBtn = findViewById(R.id.btn_ma); emaBtn = findViewById(R.id.btn_ema); bollBtn = findViewById(R.id.btn_boll); macdBtn = findViewById(R.id.btn_macd); kdjBtn = findViewById(R.id.btn_kdj); rsiBtn = findViewById(R.id.btn_rsi); kLineResetBtn = findViewById(R.id.btn_kline_reset); depthJumpBtn = findViewById(R.id.btn_depth_activity); instantBtn = findViewById(R.id.btn_instant); } private void initData() { //初始化控件加载数据,仅限于首次初始化赋值,不可用于更新数据 kLineView.initKDataList(getKDataList(10)); //设置十字线移动模式,默认为0:固定指向收盘价 kLineView.setCrossHairMoveMode(KLineView.CROSS_HAIR_MOVE_OPEN); mHandler = new Handler(); dataListAddRunnable = new Runnable() { @Override public void run() { //分页加载时添加多条数据 kLineView.addPreDataList(getKDataList(10), true); // kLineView.addPreDataList(null, true); } }; singleDataAddRunnable = new Runnable() { @Override public void run() { //实时刷新时添加单条数据 /*KData kData = kLineView.getTotalDataList().get(kLineView.getTotalDataList().size() - 1); KData kData1 = new KData(kData.getTime(), kData.getOpenPrice(), kData.getOpenPrice(), kData.getMaxPrice(), kData.getMinPrice(), kData.getVolume()); kLineView.addSingleData(kData1);*/ kLineView.addSingleData(getKDataList(0.1).get(0)); // mHandler.postDelayed(this, 1000); } }; // mHandler.postDelayed(singleDataAddRunnable, 2000); } private void setListener() { deputyBtn.setOnClickListener(this); maBtn.setOnClickListener(this); emaBtn.setOnClickListener(this); bollBtn.setOnClickListener(this); macdBtn.setOnClickListener(this); kdjBtn.setOnClickListener(this); rsiBtn.setOnClickListener(this); depthJumpBtn.setOnClickListener(this); kLineResetBtn.setOnClickListener(this); instantBtn.setOnClickListener(this); //当控件显示数据属于总数据量的前三分之一时,会自动调用该接口,用于预加载数据,保证控件操作过程中的流畅性, //虽然做了预加载,当总数据量较小时,也会出现用户滑到左边界了,但数据还未获取到,依然会有停顿。 //所以数据量越大,越不会出现停顿,也就越流畅 kLineView.setOnRequestDataListListener(new KLineView.OnRequestDataListListener() { @Override public void requestData() { //延时3秒执行,模拟网络请求耗时 mHandler.postDelayed(dataListAddRunnable, 3000); } }); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.btn_kline_reset: //重置数据,可用于分时加载,是否需要定位到重置前的时间点请看方法注释 //在做分时功能重新加载数据的时候,请务必调用该方法 kLineView.resetDataList(getKDataList(0.1)); break; case R.id.btn_deputy: //是否显示副图 kLineView.setDeputyPicShow(!kLineView.getVicePicShow()); break; case R.id.btn_ma: //主图展示MA kLineView.setMainImgType(KLineView.MAIN_IMG_MA); break; case R.id.btn_ema: //主图展示EMA kLineView.setMainImgType(KLineView.MAIN_IMG_EMA); break; case R.id.btn_boll: //主图展示BOLL kLineView.setMainImgType(KLineView.MAIN_IMG_BOLL); break; case R.id.btn_macd: //副图展示MACD kLineView.setDeputyImgType(KLineView.DEPUTY_IMG_MACD); break; case R.id.btn_kdj: //副图展示KDJ kLineView.setDeputyImgType(KLineView.DEPUTY_IMG_KDJ); break; case R.id.btn_rsi: //副图展示RSI kLineView.setDeputyImgType(KLineView.DEPUTY_IMG_RSI); break; case R.id.btn_instant: kLineView.setShowInstant(!kLineView.isShowInstant()); break; case R.id.btn_depth_activity: //跳转到深度图页面 startActivity(new Intent(getApplicationContext(), DepthActivity.class)); break; default: break; } } //模拟K线数据 private List getKDataList(double num) { long start = System.currentTimeMillis(); Random random = new Random(); List dataList = new ArrayList<>(); double openPrice = 100; double closePrice; double maxPrice; double minPrice; double volume; /*for (int i = 0; i < 2000; i++) { start += 60 * 1000 * 5; closePrice = 150; maxPrice = 200; minPrice = 80; volume = 300; dataList.add(new KData(start, openPrice, closePrice, maxPrice, minPrice, volume)); }*/ for (int x = 0; x < num * 10; x++) { for (int i = 0; i < 12; i++) { start += 60 * 1000 * 5; closePrice = openPrice + getAddRandomDouble(); maxPrice = closePrice + getAddRandomDouble(); minPrice = openPrice - getSubRandomDouble(); volume = random.nextInt(100) * 1000 + random.nextInt(10) * 10 + random.nextInt(10) + random.nextDouble(); dataList.add(new KData(start, openPrice, closePrice, maxPrice, minPrice, volume)); openPrice = closePrice; } for (int i = 0; i < 8; i++) { start += 60 * 1000 * 5; closePrice = openPrice - getSubRandomDouble(); maxPrice = openPrice + getAddRandomDouble(); minPrice = closePrice - getSubRandomDouble(); volume = random.nextInt(100) * 1000 + random.nextInt(10) * 10 + random.nextInt(10) + random.nextDouble(); dataList.add(new KData(start, openPrice, closePrice, maxPrice, minPrice, volume)); openPrice = closePrice; } } long end = System.currentTimeMillis(); return dataList; } private double getAddRandomDouble() { Random random = new Random(); return random.nextInt(5) * 5 + random.nextDouble(); } private double getSubRandomDouble() { Random random = new Random(); return random.nextInt(5) * 5 - random.nextDouble(); } private int dp2px(float dpValue) { final float scale = getResources().getDisplayMetrics().density; return (int) (dpValue * scale + 0.5f); } @Override protected void onDestroy() { super.onDestroy(); //退出页面时停止子线程并置空,便于回收,避免内存泄露 kLineView.cancelQuotaThread(); } } ================================================ FILE: app/src/main/java/com/example/admin/klineview/Print.java ================================================ package com.example.admin.klineview; import android.util.Log; /** * Created by xiesuichao on 2016/10/18. */ public class Print { private static boolean printAvailable = true; private static final String TAG_LOG = "KLine"; public static void log(Object obj) { if (printAvailable){ Log.w(TAG_LOG, "--x--" + getCurrentClassName() + "--:" + obj); } } public static void log(String title, Object obj) { if (printAvailable){ Log.w(TAG_LOG, "--x--" + getCurrentClassName() + "--:" + title + ":" + obj); } } private static String getCurrentClassName() { int level = 2; StackTraceElement[] stacks = new Throwable().getStackTrace(); String className = stacks[level].getClassName(); return className.substring(className.lastIndexOf(".") + 1); } } ================================================ FILE: app/src/main/java/com/example/admin/klineview/depth/Depth.java ================================================ package com.example.admin.klineview.depth; import android.support.annotation.NonNull; /** * 深度数据 * Created by xiesuichao on 2018/9/23. */ public class Depth implements Comparable{ private double price; private double volume; //buy:0, sell:1 private int tradeType; private float x; private float y; public Depth() { } public Depth(double price, double volume, int tradeType) { this.price = price; this.volume = volume; this.tradeType = tradeType; } public double getPrice() { return price; } public void setPrice(double price) { this.price = price; } public double getVolume() { return volume; } public void setVolume(double volume) { this.volume = volume; } public int getTradeType() { return tradeType; } public void setTradeType(int tradeType) { this.tradeType = tradeType; } public float getX() { return x; } public void setX(float x) { this.x = x; } public float getY() { return y; } public void setY(float y) { this.y = y; } @Override public int compareTo(@NonNull Depth o) { double diff = this.getPrice() - o.getPrice(); if (diff > 0){ return 1; }else if (diff < 0){ return -1; }else { return 0; } } @Override public String toString() { return "Depth{" + "price=" + price + ", volume=" + volume + ", tradeType=" + tradeType + ", x=" + x + ", y=" + y + '}'; } } ================================================ FILE: app/src/main/java/com/example/admin/klineview/depth/DepthView.java ================================================ package com.example.admin.klineview.depth; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Rect; import android.graphics.RectF; import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import com.example.admin.klineview.R; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * 深度控件 * Created by xiesuichao on 2018/9/23. */ public class DepthView extends View { //是否显示详情 private boolean isShowDetail = false; //是否长按 private boolean isLongPress = false; //是否显示竖线 private boolean isShowDetailLine = true; //手指单击松开后,数据是否继续显示 private boolean isShowDetailSingleClick = true; //单击松开,数据延时消失,单位毫秒 private final int DISAPPEAR_TIME = 500; //手指长按松开后,数据是否继续显示 private boolean isShowDetailLongPress = true; //长按触发时长,单位毫秒 private final int LONG_PRESS_TIME_OUT = 300; //横坐标中间值 private double abscissaCenterPrice = -1; private boolean isHorizontalMove; private Depth clickDepth; private String detailPriceTitle; private String detailVolumeTitle; private Paint strokePaint, fillPaint; private Rect textRect; private Path linePath; private List buyDataList, sellDataList; private double maxVolume, avgVolumeSpace, avgOrdinateSpace, depthImgHeight; private float leftStart, topStart, rightEnd, bottomEnd, longPressDownX, longPressDownY, singleClickDownX, singleClickDownY, detailLineWidth, dispatchDownX; private int buyLineCol, buyBgCol, sellLineCol, sellBgCol, ordinateTextCol, ordinateTextSize, abscissaTextCol, abscissaTextSize, detailBgCol, detailTextCol, detailTextSize, ordinateNum, buyLineStrokeWidth, sellLineStrokeWidth, detailLineCol, detailPointRadius, pricePrecision, moveLimitDistance; private Runnable longPressRunnable; private Runnable singleClickDisappearRunnable; private String leftPriceStr; private String rightPriceStr; public DepthView(Context context) { this(context, null); } public DepthView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public DepthView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs); } /** * 设置购买数据 */ public void setBuyDataList(List depthList) { buyDataList.clear(); buyDataList.addAll(depthList); //如果数据是无序的,则按价格进行排序。如果是有序的,则注释掉 Collections.sort(buyDataList); //计算累积交易量 for (int i = buyDataList.size() - 1; i >= 0; i--) { if (i < buyDataList.size() - 1) { buyDataList.get(i).setVolume(buyDataList.get(i).getVolume() + buyDataList.get(i + 1).getVolume()); } } requestLayout(); invalidate(); } /** * 设置出售数据 */ public void setSellDataList(List depthList) { sellDataList.clear(); sellDataList.addAll(depthList); //如果数据是无序的,则按价格进行排序。如果是有序的,则注释掉 Collections.sort(sellDataList); //计算累积交易量 for (int i = 0; i < sellDataList.size(); i++) { if (i > 0) { sellDataList.get(i).setVolume(sellDataList.get(i).getVolume() + sellDataList.get(i - 1).getVolume()); } } requestLayout(); invalidate(); } /** * 重置深度数据 */ public void resetAllData(List buyDataList, List sellDataList) { setBuyDataList(buyDataList); setSellDataList(sellDataList); isShowDetail = false; isLongPress = false; requestLayout(); } /** * 设置横坐标中间值 */ public void setAbscissaCenterPrice(double centerPrice) { this.abscissaCenterPrice = centerPrice; } /** * 是否显示竖线 */ public void setShowDetailLine(boolean isShowLine) { this.isShowDetailLine = isShowLine; } /** * 手指单击松开后,数据是否继续显示 */ public void setShowDetailSingleClick(boolean isShowDetailSingleClick) { this.isShowDetailSingleClick = isShowDetailSingleClick; } /** * 手指长按松开后,数据是否继续显示 */ public void setShowDetailLongPress(boolean isShowDetailLongPress) { this.isShowDetailLongPress = isShowDetailLongPress; } /** * 设置横坐标价钱小数位精度 */ public void setPricePrecision(int pricePrecision) { this.pricePrecision = pricePrecision; } /** * 设置数据详情的价钱说明 */ public void setDetailPriceTitle(String priceTitle) { this.detailPriceTitle = priceTitle; } /** * 设置数据详情的数量说明 */ public void setDetailVolumeTitle(String volumeTitle) { this.detailVolumeTitle = volumeTitle; } /** * 移除runnable */ public void cancelCallback() { removeCallbacks(longPressRunnable); removeCallbacks(singleClickDisappearRunnable); } private void init(Context context, AttributeSet attrs) { if (attrs != null) { TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.DepthView); buyLineCol = typedArray.getColor(R.styleable.DepthView_dvBuyLineCol, 0xff2BB8AB); buyLineStrokeWidth = typedArray.getInt(R.styleable.DepthView_dvBuyLineStrokeWidth, 1); buyBgCol = typedArray.getColor(R.styleable.DepthView_dvBuyBgCol, 0x662BB8AB); sellLineCol = typedArray.getColor(R.styleable.DepthView_dvSellLineCol, 0xffFF5442); sellLineStrokeWidth = typedArray.getInt(R.styleable.DepthView_dvSellLineStrokeWidth, 1); sellBgCol = typedArray.getColor(R.styleable.DepthView_dvSellBgCol, 0x66FF5442); ordinateTextCol = typedArray.getColor(R.styleable.DepthView_dvOrdinateCol, 0xff808F9E); ordinateTextSize = typedArray.getInt(R.styleable.DepthView_dvOrdinateTextSize, 8); ordinateNum = typedArray.getInt(R.styleable.DepthView_dvOrdinateNum, 5); abscissaTextCol = typedArray.getColor(R.styleable.DepthView_dvAbscissaCol, ordinateTextCol); abscissaTextSize = typedArray.getInt(R.styleable.DepthView_dvAbscissaTextSize, ordinateTextSize); detailBgCol = typedArray.getColor(R.styleable.DepthView_dvDetailBgCol, 0x99F3F4F6); detailTextCol = typedArray.getColor(R.styleable.DepthView_dvDetailTextCol, 0xff294058); detailTextSize = typedArray.getInt(R.styleable.DepthView_dvDetailTextSize, 10); detailLineCol = typedArray.getColor(R.styleable.DepthView_dvDetailLineCol, 0xff828EA2); detailLineWidth = typedArray.getFloat(R.styleable.DepthView_dvDetailLineWidth, 0); detailPointRadius = typedArray.getInt(R.styleable.DepthView_dvDetailPointRadius, 3); detailPriceTitle = typedArray.getString(R.styleable.DepthView_dvDetailPriceTitle); detailVolumeTitle = typedArray.getString(R.styleable.DepthView_dvDetailVolumeTitle); typedArray.recycle(); } buyDataList = new ArrayList<>(); sellDataList = new ArrayList<>(); strokePaint = new Paint(); strokePaint.setAntiAlias(true); strokePaint.setStyle(Paint.Style.STROKE); fillPaint = new Paint(); fillPaint.setAntiAlias(true); fillPaint.setStyle(Paint.Style.FILL); textRect = new Rect(); linePath = new Path(); pricePrecision = 8; if (TextUtils.isEmpty(detailPriceTitle)) { detailPriceTitle = "价格(BTC):"; } if (TextUtils.isEmpty(detailVolumeTitle)) { detailVolumeTitle = "累积交易量:"; } moveLimitDistance = ViewConfiguration.get(getContext()).getScaledTouchSlop(); longPressRunnable = new Runnable() { @Override public void run() { isLongPress = true; isShowDetail = true; getClickDepth(longPressDownX); invalidate(); } }; singleClickDisappearRunnable = new Runnable() { @Override public void run() { isShowDetail = false; invalidate(); } }; } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); leftStart = getPaddingLeft() + 1; topStart = getPaddingTop() + 1; rightEnd = getMeasuredWidth() - getPaddingRight() - 1; bottomEnd = getMeasuredHeight() - getPaddingBottom() - 1; double maxBuyVolume; double minBuyVolume; double maxSellVolume; double minSellVolume; if (!buyDataList.isEmpty()) { maxBuyVolume = buyDataList.get(0).getVolume(); minBuyVolume = buyDataList.get(buyDataList.size() - 1).getVolume(); } else { maxBuyVolume = minBuyVolume = 0; } if (!sellDataList.isEmpty()) { maxSellVolume = sellDataList.get(sellDataList.size() - 1).getVolume(); minSellVolume = sellDataList.get(0).getVolume(); } else { maxSellVolume = minSellVolume = 0; } maxVolume = Math.max(maxBuyVolume, maxSellVolume); double minVolume = Math.min(minBuyVolume, minSellVolume); resetStrokePaint(abscissaTextCol, abscissaTextSize, 0); if (!buyDataList.isEmpty()) { leftPriceStr = setPrecision(buyDataList.get(0).getPrice(), pricePrecision); } else if (!sellDataList.isEmpty()) { leftPriceStr = setPrecision(sellDataList.get(0).getPrice(), pricePrecision); } else { leftPriceStr = "0"; } if (!sellDataList.isEmpty()) { rightPriceStr = setPrecision(sellDataList.get(sellDataList.size() - 1).getPrice(), pricePrecision); } else if (!buyDataList.isEmpty()) { rightPriceStr = setPrecision(buyDataList.get(buyDataList.size() - 1).getPrice(), pricePrecision); } else { rightPriceStr = "0"; } strokePaint.getTextBounds(leftPriceStr, 0, leftPriceStr.length(), textRect); depthImgHeight = bottomEnd - topStart - textRect.height() - dp2px(4); double avgHeightPerVolume = depthImgHeight / (maxVolume - minVolume); double avgWidthPerSize = (rightEnd - leftStart) / (buyDataList.size() + sellDataList.size()); avgVolumeSpace = maxVolume / ordinateNum; avgOrdinateSpace = depthImgHeight / ordinateNum; for (int i = 0; i < buyDataList.size(); i++) { buyDataList.get(i).setX(leftStart + (float) avgWidthPerSize * i); buyDataList.get(i).setY(topStart + (float) ((maxVolume - buyDataList.get(i).getVolume()) * avgHeightPerVolume)); } for (int i = sellDataList.size() - 1; i >= 0; i--) { sellDataList.get(i).setX(rightEnd - (float) (avgWidthPerSize * (sellDataList.size() - 1 - i))); sellDataList.get(i).setY(topStart + (float) ((maxVolume - sellDataList.get(i).getVolume()) * avgHeightPerVolume)); } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (buyDataList.isEmpty() && sellDataList.isEmpty()) { return; } drawLineAndBg(canvas); drawCoordinateValue(canvas); drawDetailData(canvas); } @Override public boolean dispatchTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { longPressDownX = event.getX(); longPressDownY = event.getY(); dispatchDownX = event.getX(); isLongPress = false; postDelayed(longPressRunnable, LONG_PRESS_TIME_OUT); } else if (event.getAction() == MotionEvent.ACTION_MOVE) { //长按控制 float dispatchMoveX = event.getX(); float dispatchMoveY = event.getY(); float diffDispatchMoveX = Math.abs(dispatchMoveX - longPressDownX); float diffDispatchMoveY = Math.abs(dispatchMoveY - longPressDownY); float moveDistanceX = Math.abs(event.getX() - dispatchDownX); getParent().requestDisallowInterceptTouchEvent(true); if (isHorizontalMove || (diffDispatchMoveX > diffDispatchMoveY + dp2px(5) && diffDispatchMoveX > moveLimitDistance)) { isHorizontalMove = true; removeCallbacks(longPressRunnable); if (isLongPress && moveDistanceX > 2) { getClickDepth(event.getX()); if (clickDepth != null) { invalidate(); } } dispatchDownX = event.getX(); return isLongPress || super.dispatchTouchEvent(event); } else if (!isHorizontalMove && diffDispatchMoveY > diffDispatchMoveX + dp2px(5) && diffDispatchMoveY > moveLimitDistance) { removeCallbacks(longPressRunnable); getParent().requestDisallowInterceptTouchEvent(false); return false; } } else if (event.getAction() == MotionEvent.ACTION_UP) { isHorizontalMove = false; removeCallbacks(longPressRunnable); if (!isShowDetailLongPress) { isShowDetail = false; invalidate(); } getParent().requestDisallowInterceptTouchEvent(false); } return isLongPress || super.dispatchTouchEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: singleClickDownX = event.getX(); singleClickDownY = event.getY(); break; case MotionEvent.ACTION_UP: float diffTouchMoveX = event.getX() - singleClickDownX; float diffTouchMoveY = event.getY() - singleClickDownY; if (diffTouchMoveY < moveLimitDistance && diffTouchMoveX < moveLimitDistance) { isShowDetail = true; getClickDepth(singleClickDownX); if (clickDepth != null) { invalidate(); } } if (!isShowDetailSingleClick) { postDelayed(singleClickDisappearRunnable, DISAPPEAR_TIME); } break; } return true; } //获取单击位置数据 private void getClickDepth(float clickX) { clickDepth = null; if (sellDataList.isEmpty()) { for (int i = 0; i < buyDataList.size(); i++) { if (i + 1 < buyDataList.size() && clickX >= buyDataList.get(i).getX() && clickX < buyDataList.get(i + 1).getX()) { clickDepth = buyDataList.get(i); break; } else if (i == buyDataList.size() - 1 && clickX >= buyDataList.get(i).getX()) { clickDepth = buyDataList.get(i); break; } } } else if (clickX < sellDataList.get(0).getX()) { for (int i = 0; i < buyDataList.size(); i++) { if (i + 1 < buyDataList.size() && clickX >= buyDataList.get(i).getX() && clickX < buyDataList.get(i + 1).getX()) { clickDepth = buyDataList.get(i); break; } else if (i == buyDataList.size() - 1 && clickX >= buyDataList.get(i).getX() && clickX < sellDataList.get(0).getX()) { clickDepth = buyDataList.get(i); break; } } } else { for (int i = 0; i < sellDataList.size(); i++) { if (i + 1 < sellDataList.size() && clickX >= sellDataList.get(i).getX() && clickX < sellDataList.get(i + 1).getX()) { clickDepth = sellDataList.get(i); break; } else if (i == sellDataList.size() - 1 && clickX >= sellDataList.get(i).getX()) { clickDepth = sellDataList.get(i); break; } } } } //坐标轴 private void drawCoordinateValue(Canvas canvas) { //横轴 resetStrokePaint(abscissaTextCol, abscissaTextSize, 0); strokePaint.getTextBounds(rightPriceStr, 0, rightPriceStr.length(), textRect); //左边价格 canvas.drawText(leftPriceStr, leftStart, bottomEnd - dp2px(2), strokePaint); //右边价格 canvas.drawText(rightPriceStr, rightEnd - textRect.width(), bottomEnd - dp2px(2), strokePaint); //中间价格 if (abscissaCenterPrice != -1) { canvas.drawText(setPrecision(abscissaCenterPrice, pricePrecision), getWidth() / 2 - strokePaint.measureText(setPrecision(abscissaCenterPrice, pricePrecision)) / 2, bottomEnd - dp2px(2), strokePaint); } //纵轴 resetStrokePaint(ordinateTextCol, ordinateTextSize, 0); strokePaint.getTextBounds(maxVolume + "", 0, (maxVolume + "").length(), textRect); for (int i = 0; i < ordinateNum; i++) { String ordinateStr = formatNum(maxVolume - i * avgVolumeSpace); canvas.drawText(ordinateStr, rightEnd - strokePaint.measureText(ordinateStr), (float) (topStart + textRect.height() + i * avgOrdinateSpace), strokePaint); } } private void drawLineAndBg(Canvas canvas) { //买方背景 if (!buyDataList.isEmpty()) { linePath.reset(); for (int i = 0; i < buyDataList.size(); i++) { if (i == 0) { linePath.moveTo(buyDataList.get(i).getX(), buyDataList.get(i).getY()); } else { linePath.lineTo(buyDataList.get(i).getX(), buyDataList.get(i).getY()); } } if (!buyDataList.isEmpty() && buyDataList.get(buyDataList.size() - 1).getY() < topStart + depthImgHeight) { linePath.lineTo(buyDataList.get(buyDataList.size() - 1).getX(), (float) (topStart + depthImgHeight)); } linePath.lineTo(leftStart, (float) (topStart + depthImgHeight)); linePath.close(); fillPaint.setColor(buyBgCol); canvas.drawPath(linePath, fillPaint); //买方线条 linePath.reset(); for (int i = 0; i < buyDataList.size(); i++) { if (i == 0) { linePath.moveTo(buyDataList.get(i).getX(), buyDataList.get(i).getY()); } else { linePath.lineTo(buyDataList.get(i).getX(), buyDataList.get(i).getY()); } } resetStrokePaint(buyLineCol, 0, buyLineStrokeWidth); canvas.drawPath(linePath, strokePaint); } //卖方背景 if (!sellDataList.isEmpty()) { linePath.reset(); for (int i = sellDataList.size() - 1; i >= 0; i--) { if (i == sellDataList.size() - 1) { linePath.moveTo(sellDataList.get(i).getX(), sellDataList.get(i).getY()); } else { linePath.lineTo(sellDataList.get(i).getX(), sellDataList.get(i).getY()); } } if (!sellDataList.isEmpty() && sellDataList.get(0).getY() < (float) (topStart + depthImgHeight)) { linePath.lineTo(sellDataList.get(0).getX(), (float) (topStart + depthImgHeight)); } linePath.lineTo(rightEnd, (float) (topStart + depthImgHeight)); linePath.close(); fillPaint.setColor(sellBgCol); canvas.drawPath(linePath, fillPaint); //卖方线条 linePath.reset(); for (int i = 0; i < sellDataList.size(); i++) { if (i == 0) { linePath.moveTo(sellDataList.get(i).getX(), sellDataList.get(i).getY()); } else { linePath.lineTo(sellDataList.get(i).getX(), sellDataList.get(i).getY()); } } resetStrokePaint(sellLineCol, 0, sellLineStrokeWidth); canvas.drawPath(linePath, strokePaint); } } private void drawDetailData(Canvas canvas) { if (!isShowDetail || clickDepth == null) { return; } //游标线 if (isShowDetailLine) { resetStrokePaint(detailLineCol, 0, detailLineWidth); canvas.drawLine(clickDepth.getX(), topStart, clickDepth.getX(), topStart + (float) depthImgHeight, strokePaint); } if (sellDataList.isEmpty() || clickDepth.getX() < sellDataList.get(0).getX()) { fillPaint.setColor(buyLineCol); } else if (buyDataList.isEmpty() || clickDepth.getX() >= sellDataList.get(0).getX()) { fillPaint.setColor(sellLineCol); } canvas.drawCircle(clickDepth.getX(), clickDepth.getY(), dp2px(detailPointRadius), fillPaint); resetStrokePaint(detailTextCol, detailTextSize, 0); fillPaint.setColor(detailBgCol); String clickPriceStr = detailPriceTitle + formatNum(clickDepth.getPrice()); String clickVolumeStr = detailVolumeTitle + formatNum(clickDepth.getVolume()); strokePaint.getTextBounds(clickPriceStr, 0, clickPriceStr.length(), textRect); int priceStrWidth = textRect.width(); int priceStrHeight = textRect.height(); strokePaint.getTextBounds(clickVolumeStr, 0, clickVolumeStr.length(), textRect); int volumeStrWidth = textRect.width(); int maxWidth = Math.max(priceStrWidth, volumeStrWidth); float bgLeft, bgTop, bgRight, bgBottom, priceStrX, priceStrY, volumeStrY; if (clickDepth.getX() <= maxWidth + dp2px(15)) { bgLeft = clickDepth.getX() + dp2px(5); bgRight = clickDepth.getX() + dp2px(15) + maxWidth; priceStrX = clickDepth.getX() + dp2px(10); } else { bgLeft = clickDepth.getX() - dp2px(15) - maxWidth; bgRight = clickDepth.getX() - dp2px(5); priceStrX = clickDepth.getX() - dp2px(10) - maxWidth; } if (clickDepth.getY() < topStart + dp2px(7) + priceStrHeight) { bgTop = topStart; bgBottom = topStart + dp2px(14) + priceStrHeight * 2; priceStrY = topStart + dp2px(3) + priceStrHeight; volumeStrY = topStart + dp2px(7) + priceStrHeight * 2; } else if (clickDepth.getY() > topStart + depthImgHeight - dp2px(7) - priceStrHeight) { bgTop = topStart + (float) depthImgHeight - dp2px(14) - priceStrHeight * 2; bgBottom = topStart + (float) depthImgHeight; priceStrY = topStart + (float) depthImgHeight - dp2px(9) - priceStrHeight; volumeStrY = topStart + (float) depthImgHeight - dp2px(5); } else { bgTop = clickDepth.getY() - dp2px(10) - priceStrHeight; bgBottom = clickDepth.getY() + dp2px(10) + priceStrHeight; priceStrY = clickDepth.getY() - dp2px(2); volumeStrY = clickDepth.getY() + priceStrHeight; } RectF rectF = new RectF(bgLeft, bgTop, bgRight, bgBottom); canvas.drawRoundRect(rectF, 6, 6, fillPaint); canvas.drawText(clickPriceStr, priceStrX, priceStrY, strokePaint); canvas.drawText(clickVolumeStr, priceStrX, volumeStrY, strokePaint); } /** * 设置小数位精度 * * @param num * @param scale 保留几位小数 */ private String setPrecision(Double num, int scale) { BigDecimal bigDecimal = new BigDecimal(num); return bigDecimal.setScale(scale, BigDecimal.ROUND_DOWN).toPlainString(); } /** * 按量级格式化数量 */ private String formatNum(double num) { if (num < 1) { return setPrecision(num, 6); } else if (num < 10) { return setPrecision(num, 4); } else if (num < 100) { return setPrecision(num, 3); } else if (num < 10000) { return setPrecision(num / 1000, 1) + "K"; } else { return setPrecision(num / 10000, 2) + "万"; } } private void resetStrokePaint(int colorId, int textSize, float strokeWidth) { strokePaint.setColor(colorId); strokePaint.setTextSize(sp2px(textSize)); strokePaint.setStrokeWidth(dp2px(strokeWidth)); } private int dp2px(float dpValue) { final float scale = getContext().getResources().getDisplayMetrics().density; return (int) (dpValue * scale + 0.5f); } private int sp2px(float spValue) { final float fontScale = getContext().getResources().getDisplayMetrics().scaledDensity; return (int) (spValue * fontScale + 0.5f); } } ================================================ FILE: app/src/main/java/com/example/admin/klineview/kline/KData.java ================================================ package com.example.admin.klineview.kline; /** * K线数据 * Created by xiesuichao on 2018/6/29. */ public class KData { private long time;//时间戳 private double openPrice; private double closePrice; private double maxPrice; private double minPrice; private double volume; private double upDnAmount;//涨跌额 private double upDnRate;//涨跌幅 private double priceMa5; private double priceMa10; private double priceMa30; private double ema5; private double ema10; private double ema30; private double ema; private double volumeMa5; private double volumeMa10; private double bollMb; private double bollUp; private double bollDn; private double macd; private double dea; private double dif; private double k; private double d; private double j; private double rs1; private double rs2; private double rs3; private double leftX; private double rightX; private double centerX; private double closeY; private double openY; private boolean initFinish; public KData() { } public KData(double openPrice, double closedPrice, double maxPrice, double minPrice, double volume) { this.openPrice = openPrice; this.closePrice = closedPrice; this.maxPrice = maxPrice; this.minPrice = minPrice; this.volume = volume; } public KData(long time, double openPrice, double closePrice, double maxPrice, double minPrice, double volume){ this.time = time; this.openPrice = openPrice; this.closePrice = closePrice; this.maxPrice = maxPrice; this.minPrice = minPrice; this.volume = volume; } public double getCenterX() { return centerX; } public void setCenterX(double centerX) { this.centerX = centerX; } public double getEma() { return ema; } public void setEma(double ema) { this.ema = ema; } public double getOpenPrice() { return openPrice; } public void setOpenPrice(double openPrice) { this.openPrice = openPrice; } public double getClosePrice() { return closePrice; } public void setClosePrice(double closePrice) { this.closePrice = closePrice; } public double getMaxPrice() { return maxPrice; } public void setMaxPrice(double maxPrice) { this.maxPrice = maxPrice; } public double getMinPrice() { return minPrice; } public void setMinPrice(double minPrice) { this.minPrice = minPrice; } public double getLeftX() { return leftX; } public void setLeftX(double leftX) { this.leftX = leftX; } public double getRightX() { return rightX; } public void setRightX(double rightX) { this.rightX = rightX; } public long getTime() { return time; } public void setTime(long time) { this.time = time; } public double getVolume() { return volume; } public void setVolume(double volume) { this.volume = volume; } public double getPriceMa5() { return priceMa5; } public void setPriceMa5(double priceMa5) { this.priceMa5 = priceMa5; } public double getPriceMa10() { return priceMa10; } public void setPriceMa10(double priceMa10) { this.priceMa10 = priceMa10; } public double getPriceMa30() { return priceMa30; } public void setPriceMa30(double priceMa30) { this.priceMa30 = priceMa30; } public double getVolumeMa5() { return volumeMa5; } public void setVolumeMa5(double volumeMa5) { this.volumeMa5 = volumeMa5; } public double getVolumeMa10() { return volumeMa10; } public void setVolumeMa10(double volumeMa10) { this.volumeMa10 = volumeMa10; } public double getEma5() { return ema5; } public void setEma5(double ema5) { this.ema5 = ema5; } public double getEma10() { return ema10; } public void setEma10(double ema10) { this.ema10 = ema10; } public double getEma30() { return ema30; } public void setEma30(double ema30) { this.ema30 = ema30; } public double getBollMb() { return bollMb; } public void setBollMb(double bollMb) { this.bollMb = bollMb; } public double getBollUp() { return bollUp; } public void setBollUp(double bollUp) { this.bollUp = bollUp; } public double getBollDn() { return bollDn; } public void setBollDn(double bollDn) { this.bollDn = bollDn; } public double getMacd() { return macd; } public void setMacd(double macd) { this.macd = macd; } public double getDea() { return dea; } public void setDea(double dea) { this.dea = dea; } public double getDif() { return dif; } public void setDif(double dif) { this.dif = dif; } public double getK() { return k; } public void setK(double k) { this.k = k; } public double getD() { return d; } public void setD(double d) { this.d = d; } public double getJ() { return j; } public void setJ(double j) { this.j = j; } public double getRs1() { return rs1; } public void setRs1(double rs1) { this.rs1 = rs1; } public double getRs2() { return rs2; } public void setRs2(double rs2) { this.rs2 = rs2; } public double getRs3() { return rs3; } public void setRs3(double rs3) { this.rs3 = rs3; } public double getUpDnAmount() { return closePrice - openPrice; } public void setUpDnAmount(double upDnAmount) { this.upDnAmount = upDnAmount; } public double getUpDnRate() { return (closePrice - openPrice) / openPrice; } public void setUpDnRate(double upDnRate) { this.upDnRate = upDnRate; } public double getCloseY() { return closeY; } public void setCloseY(double closeY) { this.closeY = closeY; } public double getOpenY() { return openY; } public void setOpenY(double openY) { this.openY = openY; } public boolean isInitFinish() { return initFinish; } public void setInitFinish(boolean initFinish) { this.initFinish = initFinish; } @Override public String toString() { return "KData{" + "time=" + time + ", openPrice=" + openPrice + ", closePrice=" + closePrice + ", maxPrice=" + maxPrice + ", minPrice=" + minPrice + ", volume=" + volume + ", upDnAmount=" + upDnAmount + ", upDnRate=" + upDnRate + ", priceMa5=" + priceMa5 + ", priceMa10=" + priceMa10 + ", priceMa30=" + priceMa30 + ", ema5=" + ema5 + ", ema10=" + ema10 + ", ema30=" + ema30 + ", ema=" + ema + ", volumeMa5=" + volumeMa5 + ", volumeMa10=" + volumeMa10 + ", bollMb=" + bollMb + ", bollUp=" + bollUp + ", bollDn=" + bollDn + ", macd=" + macd + ", dea=" + dea + ", dif=" + dif + ", k=" + k + ", d=" + d + ", j=" + j + ", rs1=" + rs1 + ", rs2=" + rs2 + ", rs3=" + rs3 + ", leftX=" + leftX + ", rightX=" + rightX + ", closeY=" + closeY + ", openY=" + openY + ", initFinish=" + initFinish + '}'; } } ================================================ FILE: app/src/main/java/com/example/admin/klineview/kline/KLineView.java ================================================ package com.example.admin.klineview.kline; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.LinearGradient; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Shader; import android.os.Handler; import android.os.Message; import android.os.Process; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.AttributeSet; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import com.example.admin.klineview.Print; import com.example.admin.klineview.R; import java.math.BigDecimal; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; /** * 股票走势图 K线控件 * Created by xiesuichao on 2018/6/29. */ public class KLineView extends View implements View.OnTouchListener, Handler.Callback { //view显示的第一条数据在总数据list中的position private int startDataNum = 0; //首次加载显示的数据条数,可自行修改 private final int VIEW_DATA_NUM_INIT = 34; //放大时最少显示的数据条数,可自行修改 private final int VIEW_DATA_NUM_MIN = 18; //缩小时最多显示的数据条数,可自行修改 private final int VIEW_DATA_NUM_MAX = 140; //view显示的最大数据条数 private int maxViewDataNum = VIEW_DATA_NUM_INIT; //是否显示副图 private boolean isShowDeputy = false; //是否显示详情 private boolean isShowDetail = false; //是否长按 private boolean isLongPress = false; //长按触发时长 private final int LONG_PRESS_TIME_OUT = 300; //是否水平移动 private boolean isHorizontalMove = false; //是否需要请求前期的数据 private boolean isNeedRequestPreData = true; //是否双指触控 private boolean isDoubleFinger = false; //是否显示分时图 private boolean isShowInstant = false; //主图数据类型 0:MA, 1:EMA 2:BOLL public static final int MAIN_IMG_MA = 0; public static final int MAIN_IMG_EMA = 1; public static final int MAIN_IMG_BOLL = 2; private int mainImgType = MAIN_IMG_MA; //副图数据类型 0:MACD, 1:KDJ, 2:RSI public static final int DEPUTY_IMG_MACD = 0; public static final int DEPUTY_IMG_KDJ = 1; public static final int DEPUTY_IMG_RSI = 2; private int deputyImgType = DEPUTY_IMG_MACD; //十字线横线上下移动模式 0:固定指向收盘价,1:固定指向开盘价,2:上下自由滑动 public static final int CROSS_HAIR_MOVE_CLOSE = 0; public static final int CROSS_HAIR_MOVE_OPEN = 1; public static final int CROSS_HAIR_MOVE_FREE = 2; private int crossHairMoveMode = CROSS_HAIR_MOVE_CLOSE; private final String STR_MA5 = "Ma5:"; private final String STR_MA10 = "Ma10:"; private final String STR_MA30 = "Ma30:"; private final String STR_VOL = "VOL:"; private final String STR_MACD_TITLE = "MACD(12,26,9)"; private final String STR_MACD = "MACD:"; private final String STR_DIF = "DIF:"; private final String STR_DEA = "DEA:"; private final String STR_KDJ_TITLE = "KDJ(9,3,3)"; private final String STR_K = "K:"; private final String STR_D = "D:"; private final String STR_J = "J:"; private final String STR_RSI_TITLE = "RSI(6,12,24)"; private final String STR_RS1 = "RS1:"; private final String STR_RS2 = "RS2:"; private final String STR_RS3 = "RS3:"; private int initTotalListSize = 0; private Paint strokePaint, fillPaint, instantFillPaint; private Path curvePath, instantPath; private Rect topMa5Rect = new Rect(); private Rect topMa10Rect = new Rect(); private Rect topMa30Rect = new Rect(); private Rect detailTextRect = new Rect(); private String[] detailLeftTitleArr; private List totalDataList = new ArrayList<>(); private List viewDataList = new ArrayList<>(); private List endDataList = new ArrayList<>(); private List detailRightDataList = new ArrayList<>(); //水平线纵坐标 private List horizontalYList = new ArrayList<>(); //垂直线横坐标 private List verticalXList = new ArrayList<>(); private List mainMa5PointList = new ArrayList<>(); private List mainMa10PointList = new ArrayList<>(); private List mainMa30PointList = new ArrayList<>(); private List deputyMa5PointList = new ArrayList<>(); private List deputyMa10PointList = new ArrayList<>(); private List deputyMa30PointList = new ArrayList<>(); private List volumeMa5PointList = new ArrayList<>(); private List volumeMa10PointList = new ArrayList<>(); private KData lastKData; private OnRequestDataListListener requestListener; private QuotaThread quotaThread; private Runnable mDelayRunnable; private Runnable longPressRunnable; private GestureDetector gestureDetector; private int priceIncreaseCol, priceFallCol, priceMa5Col, priceMa10Col, priceMa30Col, priceMaxLabelCol, priceMinLabelCol, volumeTextCol, volumeMa5Col, volumeMa10Col, macdTextCol, macdPositiveCol, macdNegativeCol, difLineCol, deaLineCol, kLineCol, dLineCol, jLineCol, abscissaTextCol, ordinateTextCol, crossHairCol, crossHairRightLabelCol, crossHairBottomLabelCol, crossHairRightLabelTextCol, detailFrameCol, detailTextCol, tickMarkCol, detailBgCol, detailRectWidth, abscissaTextSize, volumeTextSize, crossHairBottomLabelTextCol, priceMaxLabelTextCol, priceMinLabelTextCol, priceMaxLabelTextSize, priceMinLabelTextSize, crossHairRightLabelTextSize, crossHairBottomLabelTextSize, ordinateTextSize, detailTextSize, topMaTextSize, detailRectHeight, moveLimitDistance; private float leftStart, topStart, rightEnd, bottomEnd, mulFirstDownX, mulFirstDownY, lastDiffMoveX, lastDiffMoveY, singleClickDownX, detailTextVerticalSpace, longPressMoveY, dispatchDownY, volumeImgBot, verticalSpace, flingVelocityX, priceImgBot, deputyTopY, deputyCenterY, singleClickDownY, mulSecondDownX, longPressDownX, longPressDownY, dispatchDownX; private double maxPrice, topPrice, maxPriceX, minPrice, botPrice, minPriceX, maxVolume, avgHeightPerPrice, avgPriceRectWidth, avgHeightPerVolume, avgHeightMacd, avgHeightDea, avgHeightDif, avgHeightK, avgHeightD, avgHeightJ, mMaxPriceY, avgHeightRSI, mMinPriceY, mMaxMacd, mMinMacd, mMaxK; public KLineView(Context context) { this(context, null); } public KLineView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public KLineView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initAttrs(context, attrs); initData(); } public interface OnRequestDataListListener { void requestData(); } public void setOnRequestDataListListener(OnRequestDataListListener requestListener) { this.requestListener = requestListener; } /** * ---仅限于首次初始化赋值,不可用于更新数据,如要更新,请调用resetDataList--- * 控件初始化时添加的数据量,建议每次添加数据在2000条左右 * 已对性能做优化,总数据量十万条以上对用户体验没有影响 * 首次加载5000条数据,页面初始化到加载完成,总共耗时400+ms,不超过0.5秒。 * 分页加载5000条数据时,如果正在滑动过程中,添加数据的那一瞬间会稍微有一下卡顿,影响不大。 * 经测试,800块的华为荣耀6A 每次添加4000条以下数据不会有卡顿,很流畅。 */ public void initKDataList(List dataList) { if (dataList == null || dataList.isEmpty() || totalDataList == null || totalDataList.size() > 0) { return; } this.totalDataList.addAll(dataList); startDataNum = totalDataList.size() - maxViewDataNum; QuotaUtil.initMa(totalDataList, false); resetViewData(); } /** * 获取总数据list */ public List getTotalDataList(){ return totalDataList; } /** * 获取显示的数据list */ public List getViewDataList(){ return viewDataList; } /** * 添加最新的单条数据 */ public void addSingleData(KData data) { if (data == null || endDataList == null || totalDataList == null) { return; } endDataList.clear(); int startIndex; if (totalDataList.size() >= 30) { startIndex = totalDataList.size() - 30; } else { startIndex = 0; } endDataList.addAll(totalDataList.subList(startIndex, totalDataList.size())); endDataList.add(data); if (quotaThread != null) { quotaThread.quotaSingleCalculate(endDataList); } } /** * 分页加载,向前期滑动时,进行分页加载添加数据,建议每次添加数据在2000条左右 * 配合setOnRequestDataListListener接口使用实现自动分页加载 * * @param isNeedReqPre 下次向前期滑动到边界时,是否需要自动调用接口请求数据 */ public void addPreDataList(List dataList, boolean isNeedReqPre) { if (dataList == null || dataList.isEmpty() || totalDataList == null) { return; } isNeedRequestPreData = isNeedReqPre; totalDataList.addAll(0, dataList); startDataNum += dataList.size(); if (quotaThread != null) { quotaThread.quotaListCalculate(totalDataList); } } /** * 分页加载,向前期滑动时,进行分页加载添加数据,建议每次添加数据在2000条左右 * 配合setOnRequestDataListListener接口使用实现自动分页加载 * 首次调用该方法后会记录该次list.size,后续继续调用时会将传进来的list.size与首次 * 的进行比较,如果比首次的size小,则判定为数据已拿完,不再自动调用接口请求数据。 *

* ---该方法仅在能保证每次分页加载拿到的数据list.size相同的情况下调用,否则,请调用 * 上面的方法,手动传入isNeedReqPre用来判断是否需要继续自动调用接口请求数据 */ public void addPreDataList(List dataList) { if (dataList == null || dataList.isEmpty() || totalDataList == null) { return; } if (initTotalListSize == 0) { initTotalListSize = dataList.size(); } isNeedRequestPreData = dataList.size() >= initTotalListSize; totalDataList.addAll(0, dataList); startDataNum += dataList.size(); if (quotaThread != null) { quotaThread.quotaListCalculate(totalDataList); } } /** * 重置所有数据,默认不作定位 */ public void resetDataList(List dataList) { if (dataList == null || dataList.isEmpty()){ return; } resetDataList(dataList, false); } /** * 重置所有数据 * * @param isNeedLocateCurrent 重置后的数据是否需要定位到重置前移动到的时间点,例如: * 重置前已经滑动到9月20号,true则在重置后会将新数据定位到9月20号。 * false则不作定位,view右边直接显示为最新的数据 */ public void resetDataList(List dataList, boolean isNeedLocateCurrent) { if (dataList == null || dataList.isEmpty() || viewDataList == null || totalDataList == null) { return; } long currentStartTime = 0; if (viewDataList.size() > 0) { currentStartTime = viewDataList.get(0).getTime(); } isShowDetail = false; this.totalDataList.clear(); this.totalDataList.addAll(dataList); QuotaUtil.initMa(totalDataList, false); switch (mainImgType) { case MAIN_IMG_EMA: QuotaUtil.initEma(totalDataList, false); break; case MAIN_IMG_BOLL: QuotaUtil.initBoll(totalDataList, false); break; default: break; } switch (deputyImgType) { case DEPUTY_IMG_MACD: QuotaUtil.initMACD(totalDataList, false); break; case DEPUTY_IMG_KDJ: QuotaUtil.initKDJ(totalDataList, false); break; case DEPUTY_IMG_RSI: QuotaUtil.initRSI(totalDataList, false); break; default: break; } if (isNeedLocateCurrent) { int halfSizeNum = totalDataList.size() / 2; startDataNum = -1; if (totalDataList.get(0).getTime() <= currentStartTime && totalDataList.get(halfSizeNum).getTime() > currentStartTime) { for (int i = 0; i < halfSizeNum; i++) { if (i + 1 < totalDataList.size() && totalDataList.get(i).getTime() <= currentStartTime && totalDataList.get(i + 1).getTime() > currentStartTime) { startDataNum = i; break; } } } else if (totalDataList.get(halfSizeNum).getTime() <= currentStartTime && totalDataList.get(totalDataList.size() - 1).getTime() >= currentStartTime) { for (int i = halfSizeNum; i < totalDataList.size(); i++) { if (i + 1 < totalDataList.size() && totalDataList.get(i).getTime() <= currentStartTime && totalDataList.get(i + 1).getTime() > currentStartTime) { startDataNum = i; break; } } } if (totalDataList.size() < maxViewDataNum) { startDataNum = 0; } else if (totalDataList.size() - startDataNum < maxViewDataNum || startDataNum == -1) { startDataNum = totalDataList.size() - maxViewDataNum; } } else { if (totalDataList.size() < maxViewDataNum) { startDataNum = 0; } else { startDataNum = totalDataList.size() - maxViewDataNum; } } resetViewData(); } /** * 设置主图显示类型 * MA: MAIN_IMG_MA * EMA: MAIN_IMG_EMA * BOLL: MAIN_IMG_BOLL */ public void setMainImgType(int type) { switch (type) { case MAIN_IMG_MA: QuotaUtil.initMa(totalDataList, false); break; case MAIN_IMG_EMA: QuotaUtil.initEma(totalDataList, false); break; case MAIN_IMG_BOLL: QuotaUtil.initBoll(totalDataList, false); break; default: break; } this.mainImgType = type; invalidate(); } /** * 是否显示副图 */ public void setDeputyPicShow(boolean showState) { this.isShowDeputy = showState; if (isShowDeputy) { setDeputyImgType(deputyImgType); } invalidate(); } /** * 设置副图显示类型 * MACD: DEPUTY_IMG_MACD * KDJ: DEPUTY_IMG_KDJ * RSI: DEPUTY_IMG_RSI */ public void setDeputyImgType(int type) { this.deputyImgType = type; switch (deputyImgType) { case DEPUTY_IMG_MACD: QuotaUtil.initMACD(totalDataList, false); break; case DEPUTY_IMG_KDJ: QuotaUtil.initKDJ(totalDataList, false); break; case DEPUTY_IMG_RSI: QuotaUtil.initRSI(totalDataList, false); break; default: break; } invalidate(); } /** * 设置十字线的横线上下移动模式 * 固定指向收盘价: CROSS_HAIR_MOVE_CLOSE * 固定指向开盘价: CROSS_HAIR_MOVE_OPEN * 上下自由滑动: CROSS_HAIR_MOVE_FREE */ public void setCrossHairMoveMode(int moveMode) { this.crossHairMoveMode = moveMode; } /** * 获取副图是否显示 */ public boolean getVicePicShow() { return this.isShowDeputy; } /** * 退出页面时停止子线程并置空,便于回收,避免内存泄露 */ public void cancelQuotaThread() { if (quotaThread != null) { quotaThread.setUIHandler(null); quotaThread.quit(); quotaThread = null; } removeCallbacks(mDelayRunnable); removeCallbacks(longPressRunnable); } /** * 是否显示分时图 */ public void setShowInstant(boolean state){ this.isShowInstant = state; invalidate(); } /** * 获取分时图是否显示 */ public boolean isShowInstant(){ return this.isShowInstant; } private void initAttrs(Context context, AttributeSet attrs) { if (attrs != null) { TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.KLineView); tickMarkCol = typedArray.getColor(R.styleable.KLineView_klTickMarkLineCol, 0xffF7F7FB); abscissaTextCol = typedArray.getColor(R.styleable.KLineView_klAbscissaTextCol, 0xff9BACBD); abscissaTextSize = typedArray.getInt(R.styleable.KLineView_klAbscissaTextSize, 8); ordinateTextCol = typedArray.getColor(R.styleable.KLineView_klOrdinateTextCol, abscissaTextCol); ordinateTextSize = typedArray.getInt(R.styleable.KLineView_klOrdinateTextSize, abscissaTextSize); topMaTextSize = typedArray.getInt(R.styleable.KLineView_klTopMaTextSize, 10); priceIncreaseCol = typedArray.getColor(R.styleable.KLineView_klPriceIncreaseCol, 0xffFF5442); priceFallCol = typedArray.getColor(R.styleable.KLineView_klPriceFallCol, 0xff2BB8AB); priceMa5Col = typedArray.getColor(R.styleable.KLineView_klPriceMa5LineCol, 0xffFFA800); priceMa10Col = typedArray.getColor(R.styleable.KLineView_klPriceMa10LineCol, 0xff2668FF); priceMa30Col = typedArray.getColor(R.styleable.KLineView_klPriceMa30LineCol, 0xffFF45A1); priceMaxLabelCol = typedArray.getColor(R.styleable.KLineView_klPriceMaxLabelCol, 0xffC0C6C9); priceMaxLabelTextCol = typedArray.getColor(R.styleable.KLineView_klPriceMaxLabelTextCol, 0xffffffff); priceMaxLabelTextSize = typedArray.getInt(R.styleable.KLineView_klPriceMaxLabelTextSize, 10); priceMinLabelCol = typedArray.getColor(R.styleable.KLineView_klPriceMinLabelCol, priceMaxLabelCol); priceMinLabelTextCol = typedArray.getColor(R.styleable.KLineView_klPriceMinLabelTextCol, priceMaxLabelTextCol); priceMinLabelTextSize = typedArray.getInt(R.styleable.KLineView_klPriceMinLabelTextSize, 10); volumeTextCol = typedArray.getColor(R.styleable.KLineView_klVolumeTextCol, 0xff9BACBD); volumeTextSize = typedArray.getInt(R.styleable.KLineView_klVolumeTextSize, 10); volumeMa5Col = typedArray.getColor(R.styleable.KLineView_klVolumeMa5LineCol, priceMa5Col); volumeMa10Col = typedArray.getColor(R.styleable.KLineView_klVolumeMa10LineCol, priceMa10Col); macdTextCol = typedArray.getColor(R.styleable.KLineView_klMacdTextCol, volumeTextCol); macdPositiveCol = typedArray.getColor(R.styleable.KLineView_klMacdPositiveCol, priceIncreaseCol); macdNegativeCol = typedArray.getColor(R.styleable.KLineView_klMacdNegativeCol, priceFallCol); difLineCol = typedArray.getColor(R.styleable.KLineView_klDifLineCol, priceMa10Col); deaLineCol = typedArray.getColor(R.styleable.KLineView_klDeaLineCol, priceMa30Col); kLineCol = typedArray.getColor(R.styleable.KLineView_klKLineCol, priceMa5Col); dLineCol = typedArray.getColor(R.styleable.KLineView_klDLineCol, priceMa10Col); jLineCol = typedArray.getColor(R.styleable.KLineView_klJLineCol, priceMa30Col); crossHairCol = typedArray.getColor(R.styleable.KLineView_klCrossHairCol, 0xff828EA2); crossHairRightLabelCol = typedArray.getColor(R.styleable.KLineView_klCrossHairRightLabelCol, 0xff3193FF); crossHairRightLabelTextCol = typedArray.getColor(R.styleable.KLineView_klCrossHairRightLabelTextCol, 0xffffffff); crossHairRightLabelTextSize = typedArray.getInt(R.styleable.KLineView_klCrossHairRightLabelTextSize, 10); crossHairBottomLabelCol = typedArray.getColor(R.styleable.KLineView_klCrossHairBottomLabelCol, priceMaxLabelCol); crossHairBottomLabelTextCol = typedArray.getColor(R.styleable.KLineView_klCrossHairBottomLabelTextCol, 0xffffffff); crossHairBottomLabelTextSize = typedArray.getInt(R.styleable.KLineView_klCrossHairBottomLabelTextSize, 8); detailFrameCol = typedArray.getColor(R.styleable.KLineView_klDetailFrameCol, 0xffB5C0D0); detailTextCol = typedArray.getColor(R.styleable.KLineView_klDetailTextCol, 0xff808F9E); detailTextSize = typedArray.getInt(R.styleable.KLineView_klDetailTextSize, 10); detailBgCol = typedArray.getColor(R.styleable.KLineView_klDetailBgCol, 0xe6ffffff); typedArray.recycle(); } } private void initData() { super.setOnTouchListener(this); super.setClickable(true); super.setFocusable(true); gestureDetector = new GestureDetector(getContext(), new CustomGestureListener()); moveLimitDistance = ViewConfiguration.get(getContext()).getScaledTouchSlop(); detailRectWidth = dp2px(103); detailRectHeight = dp2px(120); detailTextVerticalSpace = (detailRectHeight - dp2px(4)) / 8; detailLeftTitleArr = new String[]{"时间", "开", "高", "低", "收", "涨跌额", "涨跌幅", "成交量"}; initQuotaThread(); initStopDelay(); strokePaint = new Paint(); strokePaint.setAntiAlias(true); strokePaint.setTextSize(sp2px(abscissaTextSize)); strokePaint.setStyle(Paint.Style.STROKE); fillPaint = new Paint(); fillPaint.setAntiAlias(true); fillPaint.setStyle(Paint.Style.FILL); instantFillPaint = new Paint(); instantFillPaint.setAntiAlias(true); instantFillPaint.setStyle(Paint.Style.FILL); curvePath = new Path(); instantPath = new Path(); longPressRunnable = new Runnable() { @Override public void run() { isLongPress = true; isShowDetail = true; getClickKData(longPressDownX); invalidate(); } }; } private void initQuotaThread() { Handler uiHandler = new Handler(this); quotaThread = new QuotaThread("quotaThread", Process.THREAD_PRIORITY_BACKGROUND); quotaThread.setUIHandler(uiHandler); quotaThread.start(); } @Override public boolean handleMessage(Message msg) { if (msg.what == QuotaThread.HANDLER_QUOTA_LIST) { invalidate(); } else if (msg.what == QuotaThread.HANDLER_QUOTA_SINGLE) { if (endDataList == null || totalDataList == null) { return false; } KData endLastData = endDataList.get(endDataList.size() - 1); int totalSize = totalDataList.size(); KData totalLastData = totalDataList.get(totalSize - 1); if (endLastData.getTime() == totalLastData.getTime()) { totalDataList.remove(totalSize - 1); } totalDataList.add(endLastData); if (totalSize >= maxViewDataNum && startDataNum == totalSize - maxViewDataNum - 1) { startDataNum++; resetViewData(); } else { resetViewData(); } } return false; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); leftStart = getPaddingLeft(); topStart = getPaddingTop(); rightEnd = getMeasuredWidth() - getPaddingRight(); bottomEnd = getMeasuredHeight() - getPaddingBottom(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (totalDataList.isEmpty() || viewDataList.isEmpty()) { return; } resetData(); drawTickMark(canvas); drawAbscissa(canvas); drawOrdinate(canvas); drawCrossHairLine(canvas); drawVolume(canvas); if (isShowInstant){ crossHairMoveMode = CROSS_HAIR_MOVE_CLOSE; drawInstant(canvas); } else { drawMainDeputyRect(canvas); drawBezierCurve(canvas); drawTopPriceMAData(canvas); drawBotMAData(canvas); drawMaxMinPriceLabel(canvas); drawDetailData(canvas); } } @Override public boolean dispatchTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { longPressDownX = event.getX(); longPressDownY = event.getY(); dispatchDownX = event.getX(); dispatchDownY = event.getY(); isLongPress = false; postDelayed(longPressRunnable, LONG_PRESS_TIME_OUT); } else if (event.getAction() == MotionEvent.ACTION_MOVE) { //长按控制 float diffDispatchMoveX = Math.abs(event.getX() - longPressDownX); float diffDispatchMoveY = Math.abs(event.getY() - longPressDownY); float moveDistanceX = Math.abs(event.getX() - dispatchDownX); float moveDistanceY = Math.abs(event.getY() - dispatchDownY); longPressMoveY = event.getY(); if (getParent() != null) { getParent().requestDisallowInterceptTouchEvent(true); } if (isHorizontalMove || (diffDispatchMoveX > diffDispatchMoveY + dp2px(5) && diffDispatchMoveX > moveLimitDistance) || (isLongPress && diffDispatchMoveY > moveLimitDistance)) { isHorizontalMove = true; removeCallbacks(longPressRunnable); if (isLongPress && (moveDistanceX > 1 || moveDistanceY > 1)) { getClickKData(event.getX()); if (lastKData != null) { invalidate(); } } dispatchDownX = event.getX(); dispatchDownY = event.getY(); return isLongPress || super.dispatchTouchEvent(event); } else if (!isLongPress && !isHorizontalMove && !isDoubleFinger && diffDispatchMoveY > diffDispatchMoveX + dp2px(5) && diffDispatchMoveY > moveLimitDistance) { removeCallbacks(longPressRunnable); if (getParent() != null) { getParent().requestDisallowInterceptTouchEvent(false); } return false; } } else if (event.getAction() == MotionEvent.ACTION_UP) { isHorizontalMove = false; removeCallbacks(longPressRunnable); if (getParent() != null) { getParent().requestDisallowInterceptTouchEvent(false); } } return isLongPress || super.dispatchTouchEvent(event); } @Override public boolean onTouchEvent(@NonNull MotionEvent event) { switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: singleClickDownX = event.getX(); singleClickDownY = event.getY(); flingVelocityX = 0; mulFirstDownX = event.getX(0); mulFirstDownY = event.getY(0); break; case MotionEvent.ACTION_POINTER_DOWN: isShowDetail = false; isDoubleFinger = true; removeCallbacks(longPressRunnable); mulSecondDownX = event.getX(1); float mulSecondDownY = event.getY(1); lastDiffMoveX = Math.abs(mulSecondDownX - mulFirstDownX); lastDiffMoveY = Math.abs(mulSecondDownY - mulFirstDownY); break; case MotionEvent.ACTION_MOVE: if (event.getPointerCount() > 1) { float mulFirstMoveX = event.getX(0); float mulFirstMoveY = event.getY(0); float mulSecondMoveX = event.getX(1); float mulSecondMoveY = event.getY(1); float diffMoveX = Math.abs(mulSecondMoveX - mulFirstMoveX); float diffMoveY = Math.abs(mulSecondMoveY - mulFirstMoveY); //双指分开,放大显示 if ((diffMoveX >= diffMoveY && diffMoveX - lastDiffMoveX > 1) || (diffMoveY >= diffMoveX && diffMoveY - lastDiffMoveY > 1)) { if (maxViewDataNum <= VIEW_DATA_NUM_MIN) { maxViewDataNum = VIEW_DATA_NUM_MIN; //如果view中显示的数据量小于当前最大数据条数,则不管双指如何落点,都向左放大 } else if (viewDataList != null && viewDataList.size() < maxViewDataNum) { maxViewDataNum -= 2; startDataNum = totalDataList.size() - maxViewDataNum; //如果双指起始落点都在中线左侧,则左边不动,放大右边 } else if (verticalXList != null && mulFirstDownX < verticalXList.get(2) && mulSecondDownX <= verticalXList.get(2)) { maxViewDataNum -= 2; //如果双指起始落点在中线左右两侧,则左右同时放大 } else if ((verticalXList != null && mulFirstDownX <= verticalXList.get(2) && mulSecondDownX >= verticalXList.get(2)) || (verticalXList != null && mulFirstDownX >= verticalXList.get(2) && mulSecondDownX <= verticalXList.get(2))) { maxViewDataNum -= 2; startDataNum += 1; //如果双指起始落点在中线右侧,则右边不动,放大左边 } else if (verticalXList != null && mulFirstDownX >= verticalXList.get(2) && mulSecondDownX > verticalXList.get(2)) { maxViewDataNum -= 2; startDataNum += 2; } resetViewData(); //双指靠拢,缩小显示 } else if ((diffMoveX >= diffMoveY && diffMoveX - lastDiffMoveX < -1) || (diffMoveY >= diffMoveX && diffMoveY - lastDiffMoveY < -1)) { if (maxViewDataNum >= VIEW_DATA_NUM_MAX) { maxViewDataNum = VIEW_DATA_NUM_MAX; //如果view显示的数据是totalDataList最末尾的数据,则只向左边缩小 } else if (totalDataList != null && startDataNum + maxViewDataNum >= totalDataList.size()) { maxViewDataNum += 2; startDataNum = totalDataList.size() - maxViewDataNum; //如果view显示的数据是totalDataList最开始的数据,则只向右边缩小 } else if (startDataNum <= 0) { startDataNum = 0; maxViewDataNum += 2; //如果双指起始落点都在中线左侧,则左边不动,右边缩小 } else if (verticalXList != null && mulFirstDownX < verticalXList.get(2) && mulSecondDownX <= verticalXList.get(2)) { maxViewDataNum += 2; //如果双指起始落点在中线左右两侧,则左右同时缩小 } else if ((verticalXList != null && mulFirstDownX <= verticalXList.get(2) && mulSecondDownX >= verticalXList.get(2)) || (verticalXList != null && mulFirstDownX >= verticalXList.get(2) && mulSecondDownX <= verticalXList.get(2))) { maxViewDataNum += 2; startDataNum -= 1; //如果双指起始落点都在中线右侧,则右边不动,左边缩小 } else if (verticalXList != null && mulFirstDownX >= verticalXList.get(2) && mulSecondDownX > verticalXList.get(2)) { maxViewDataNum += 2; startDataNum -= 2; } resetViewData(); } lastDiffMoveX = Math.abs(mulSecondMoveX - mulFirstMoveX); lastDiffMoveY = Math.abs(mulSecondMoveY - mulFirstMoveY); } break; case MotionEvent.ACTION_UP: if (!isDoubleFinger) { float diffTouchMoveX = Math.abs(event.getX() - singleClickDownX); float diffTouchMoveY = Math.abs(event.getY() - singleClickDownY); if (diffTouchMoveY < moveLimitDistance && diffTouchMoveX < moveLimitDistance) { isShowDetail = true; if (crossHairMoveMode == CROSS_HAIR_MOVE_FREE) { longPressMoveY = event.getY(); } getClickKData(singleClickDownX); if (lastKData != null) { invalidate(); } } } isDoubleFinger = false; break; case MotionEvent.ACTION_CANCEL: isDoubleFinger = false; break; } return true; } @Override public boolean onTouch(View v, MotionEvent event) { return !isDoubleFinger && gestureDetector.onTouchEvent(event); } private class CustomGestureListener extends GestureDetector.SimpleOnGestureListener { @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { if ((startDataNum == 0 && distanceX < 0) || (totalDataList != null && startDataNum == totalDataList.size() - maxViewDataNum && distanceX > 0) || startDataNum < 0 || (viewDataList != null && viewDataList.size() < maxViewDataNum)) { if (isShowDetail) { isShowDetail = false; if (!viewDataList.isEmpty()) { lastKData = viewDataList.get(viewDataList.size() - 1); } invalidate(); } return true; } else { isShowDetail = false; if (Math.abs(distanceX) > 1) { moveData(distanceX); invalidate(); } } return true; } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { if (totalDataList != null && startDataNum > 0 && startDataNum < totalDataList.size() - 1 - maxViewDataNum) { if (velocityX > 8000) { flingVelocityX = 8000; } else if (velocityX < -8000) { flingVelocityX = -8000; } else { flingVelocityX = velocityX; } stopDelay(); } return true; } } private void stopDelay() { post(mDelayRunnable); } private void initStopDelay() { mDelayRunnable = new Runnable() { @Override public void run() { if (flingVelocityX < -200) { if (flingVelocityX < -6000) { startDataNum += 6; } else if (flingVelocityX < -4000) { startDataNum += 5; } else if (flingVelocityX < -2500) { startDataNum += 4; } else if (flingVelocityX < -1000) { startDataNum += 3; } else { startDataNum++; } flingVelocityX += 200; if (startDataNum > totalDataList.size() - maxViewDataNum) { startDataNum = totalDataList.size() - maxViewDataNum; } } else if (flingVelocityX > 200) { if (flingVelocityX > 6000) { startDataNum -= 6; } else if (flingVelocityX > 4000) { startDataNum -= 5; } else if (flingVelocityX > 2500) { startDataNum -= 4; } else if (flingVelocityX > 1000) { startDataNum -= 3; } else { startDataNum--; } flingVelocityX -= 200; if (startDataNum < 0) { startDataNum = 0; } } resetViewData(); requestNewData(); if (Math.abs(flingVelocityX) > 200) { postDelayed(this, 15); } } }; } private void moveData(float distanceX) { if (maxViewDataNum < 60) { setSpeed(distanceX, 10); } else { setSpeed(distanceX, 3.5); } if (startDataNum < 0) { startDataNum = 0; } if (totalDataList != null){ int size = totalDataList.size(); if (startDataNum > size - maxViewDataNum) { startDataNum = size - maxViewDataNum; } } requestNewData(); resetViewData(); } private void setSpeed(float distanceX, double num) { if (Math.abs(distanceX) > 1 && Math.abs(distanceX) < 2) { startDataNum += (int) (distanceX * 10) % 2; } else if (Math.abs(distanceX) < 10) { startDataNum += (int) distanceX % 2; } else { startDataNum += (int) distanceX / num; } } private void requestNewData() { if (totalDataList != null && startDataNum <= totalDataList.size() / 3 && isNeedRequestPreData) { isNeedRequestPreData = false; if (requestListener != null){ requestListener.requestData(); } } } private void resetViewData() { if (viewDataList == null || totalDataList == null){ return; } viewDataList.clear(); int currentViewDataNum = Math.min(maxViewDataNum, totalDataList.size()); if (startDataNum >= 0) { for (int i = 0; i < currentViewDataNum; i++) { if (i + startDataNum < totalDataList.size()) { viewDataList.add(totalDataList.get(i + startDataNum)); } } } else { for (int i = 0; i < currentViewDataNum; i++) { viewDataList.add(totalDataList.get(i)); } } if (viewDataList.size() > 0 && !isShowDetail) { lastKData = viewDataList.get(viewDataList.size() - 1); } else if (viewDataList.isEmpty()) { lastKData = null; } invalidate(); } private void resetData() { if (verticalXList == null || horizontalYList == null || rightEnd == 0) { return; } //垂直刻度线 float horizontalSpace = (rightEnd - leftStart - (dp2px(46))) / 4; verticalXList.clear(); for (int i = 0; i < 5; i++) { verticalXList.add(leftStart + horizontalSpace * (i) + dp2px(6)); } //水平刻度线 verticalSpace = (bottomEnd - topStart - dp2px(38)) / 5; horizontalYList.clear(); for (int i = 0; i < 6; i++) { horizontalYList.add(topStart + verticalSpace * i + dp2px(18)); } //副图顶线 deputyTopY = horizontalYList.get(4) + dp2px(12); if (verticalXList == null || horizontalYList == null || viewDataList == null) { return; } avgPriceRectWidth = (verticalXList.get(verticalXList.size() - 1) - verticalXList.get(0)) / maxViewDataNum; maxPrice = viewDataList.get(0).getMaxPrice(); minPrice = viewDataList.get(0).getMinPrice(); maxVolume = viewDataList.get(0).getVolume(); mMaxMacd = viewDataList.get(0).getMacd(); mMinMacd = viewDataList.get(0).getMacd(); double maxDea = viewDataList.get(0).getDea(); double minDea = viewDataList.get(0).getDea(); double maxDif = viewDataList.get(0).getDif(); double minDif = viewDataList.get(0).getDif(); mMaxK = viewDataList.get(0).getK(); double maxD = viewDataList.get(0).getD(); double maxJ = viewDataList.get(0).getJ(); int viewDataSize = viewDataList.size(); for (int i = 0; i < viewDataSize; i++) { KData viewKData = viewDataList.get(i); double rightX = verticalXList.get(verticalXList.size() - 1) - (viewDataList.size() - i - 1) * avgPriceRectWidth; double leftX = rightX - avgPriceRectWidth; double centerX = rightX - avgPriceRectWidth/2; viewKData.setLeftX(leftX); viewKData.setRightX(rightX); viewKData.setCenterX(centerX); if (viewKData.getMaxPrice() >= maxPrice) { maxPrice = viewKData.getMaxPrice(); maxPriceX = viewKData.getLeftX() + avgPriceRectWidth / 2; } if (viewKData.getMinPrice() <= minPrice) { minPrice = viewKData.getMinPrice(); minPriceX = viewKData.getLeftX() + avgPriceRectWidth / 2; } if (viewKData.getVolume() >= maxVolume) { maxVolume = viewKData.getVolume(); } if (!isShowDeputy || isShowInstant){ continue; } if (deputyImgType == DEPUTY_IMG_MACD) { if (viewKData.getMacd() >= mMaxMacd) { mMaxMacd = viewKData.getMacd(); } if (viewKData.getMacd() <= mMinMacd) { mMinMacd = viewKData.getMacd(); } if (viewKData.getDea() >= maxDea) { maxDea = viewKData.getDea(); } if (viewKData.getDea() <= minDea) { minDea = viewKData.getDea(); } if (viewKData.getDif() >= maxDif) { maxDif = viewKData.getDif(); } if (viewKData.getDif() <= minDif) { minDif = viewKData.getDif(); } } else if (deputyImgType == DEPUTY_IMG_KDJ) { if (viewKData.getK() >= mMaxK) { mMaxK = viewKData.getK(); } if (viewKData.getD() >= maxD) { maxD = viewKData.getD(); } if (viewKData.getJ() >= maxJ) { maxJ = viewKData.getJ(); } } } topPrice = maxPrice + (maxPrice - minPrice) * 0.1; botPrice = minPrice - (maxPrice - minPrice) * 0.1; if (!isShowDeputy) { priceImgBot = horizontalYList.get(4); volumeImgBot = horizontalYList.get(5); } else { priceImgBot = horizontalYList.get(3); volumeImgBot = horizontalYList.get(4); } //priceData avgHeightPerPrice = (priceImgBot - horizontalYList.get(0)) / (topPrice - botPrice); mMaxPriceY = (horizontalYList.get(0) + (topPrice - maxPrice) * avgHeightPerPrice); mMinPriceY = (horizontalYList.get(0) + (topPrice - minPrice) * avgHeightPerPrice); //volumeData avgHeightPerVolume = (horizontalYList.get(horizontalYList.size() - 1) - deputyTopY) / maxVolume; for (KData kData : viewDataList) { double openPrice = kData.getOpenPrice(); double closedPrice = kData.getClosePrice(); kData.setCloseY((float) (horizontalYList.get(0) + (topPrice - closedPrice) * avgHeightPerPrice)); kData.setOpenY((float) (horizontalYList.get(0) + (topPrice - openPrice) * avgHeightPerPrice)); } if (!isShowDeputy){ return; } switch (deputyImgType) { case DEPUTY_IMG_MACD: //MACD if (mMaxMacd > 0 && mMinMacd < 0) { avgHeightMacd = (horizontalYList.get(horizontalYList.size() - 1) - deputyTopY) / Math.abs(mMaxMacd - mMinMacd); deputyCenterY = (float) (deputyTopY + mMaxMacd * avgHeightMacd); } else if (mMaxMacd <= 0) { avgHeightMacd = (horizontalYList.get(horizontalYList.size() - 1) - deputyTopY) / Math.abs(mMinMacd); deputyCenterY = deputyTopY; } else if (mMinMacd >= 0) { avgHeightMacd = (horizontalYList.get(horizontalYList.size() - 1) - deputyTopY) / Math.abs(mMaxMacd); deputyCenterY = horizontalYList.get(horizontalYList.size() - 1); } //DEA if (maxDea > 0 && minDea < 0) { avgHeightDea = (horizontalYList.get(horizontalYList.size() - 1) - deputyTopY - dp2px(24)) / (maxDea - minDea); } else if (maxDea <= 0) { avgHeightDea = (horizontalYList.get(horizontalYList.size() - 1) - deputyTopY - dp2px(24)) / Math.abs(minDea); } else if (minDea >= 0) { avgHeightDea = (horizontalYList.get(horizontalYList.size() - 1) - deputyTopY - dp2px(24)) / Math.abs(maxDea); } //DIF if (maxDif > 0 && minDif < 0) { avgHeightDif = (horizontalYList.get(horizontalYList.size() - 1) - deputyTopY - dp2px(24)) / (maxDif - minDif); } else if (maxDif <= 0) { avgHeightDif = (horizontalYList.get(horizontalYList.size() - 1) - deputyTopY - dp2px(24)) / Math.abs(minDif); } else if (minDif >= 0) { avgHeightDif = (horizontalYList.get(horizontalYList.size() - 1) - deputyTopY - dp2px(24)) / Math.abs(maxDif); } break; case DEPUTY_IMG_KDJ: //K avgHeightK = (horizontalYList.get(horizontalYList.size() - 1) - deputyTopY - dp2px(12)) / mMaxK; //D avgHeightD = (horizontalYList.get(horizontalYList.size() - 1) - deputyTopY - dp2px(12)) / maxD; //J avgHeightJ = (horizontalYList.get(horizontalYList.size() - 1) - deputyTopY - dp2px(12)) / maxJ; break; case DEPUTY_IMG_RSI: avgHeightRSI = (horizontalYList.get(horizontalYList.size() - 1) - deputyTopY) / 100; break; } } //刻度线 private void drawTickMark(Canvas canvas) { if (verticalXList == null || horizontalYList == null){ return; } resetStrokePaint(tickMarkCol, 0); for (Float aFloat : verticalXList) { canvas.drawLine(aFloat, topStart + dp2px(18), aFloat, bottomEnd - dp2px(20), strokePaint); } float horizontalRightEnd; for (int i = 0; i < horizontalYList.size(); i++) { if (i == 0 || i == 5 || i == 4 || (isShowDeputy && i == 3)) { horizontalRightEnd = rightEnd; } else { horizontalRightEnd = verticalXList.get(verticalXList.size() - 1); } canvas.drawLine(leftStart + dp2px(6), horizontalYList.get(i), horizontalRightEnd, horizontalYList.get(i), strokePaint); } canvas.drawLine(leftStart + dp2px(6), horizontalYList.get(4) + verticalSpace / 2, verticalXList.get(verticalXList.size() - 1), horizontalYList.get(4) + verticalSpace / 2, strokePaint); //数量中线 if (isShowDeputy) { canvas.drawLine(leftStart + dp2px(6), horizontalYList.get(3) + verticalSpace / 2, verticalXList.get(verticalXList.size() - 1), horizontalYList.get(3) + verticalSpace / 2, strokePaint); } } //主副图蜡烛图 private void drawMainDeputyRect(Canvas canvas) { int viewDataSize = viewDataList.size(); //drawPriceRectAndLine for (KData viewKData : viewDataList) { double openPrice = viewKData.getOpenPrice(); double closedPrice = viewKData.getClosePrice(); double higherPrice; double lowerPrice; if (openPrice >= closedPrice) { higherPrice = openPrice; lowerPrice = closedPrice; fillPaint.setColor(priceFallCol); resetStrokePaint(priceFallCol, 0); } else { higherPrice = closedPrice; lowerPrice = openPrice; fillPaint.setColor(priceIncreaseCol); resetStrokePaint(priceIncreaseCol, 0); } //如果开盘价==收盘价,则给1px的高度 float upPriceCoordinate = (float) (mMaxPriceY + (maxPrice - higherPrice) * avgHeightPerPrice); float downPriceCoordinate = (float) (mMaxPriceY + (maxPrice - lowerPrice) * avgHeightPerPrice); if (upPriceCoordinate == downPriceCoordinate){ downPriceCoordinate = upPriceCoordinate + 1; } //priceRect canvas.drawRect((float) viewKData.getLeftX() + dp2px(0.5f), upPriceCoordinate, (float) viewKData.getRightX() - dp2px(0.5f), downPriceCoordinate, fillPaint); //priceLine canvas.drawLine((float) (viewKData.getCenterX()), (float) (mMaxPriceY + (maxPrice - viewKData.getMaxPrice()) * avgHeightPerPrice), (float) (viewKData.getCenterX()), (float) (mMaxPriceY + (maxPrice - viewKData.getMinPrice()) * avgHeightPerPrice), strokePaint); //MACD if (isShowDeputy && deputyImgType == DEPUTY_IMG_MACD) { double macd = viewKData.getMacd(); if (macd > 0) { fillPaint.setColor(macdPositiveCol); canvas.drawRect((float) (viewKData.getLeftX() + dp2px(0.5f)), (float) (deputyCenterY - macd * avgHeightMacd), (float) viewKData.getRightX() - dp2px(0.5f), deputyCenterY, fillPaint); } else { fillPaint.setColor(macdNegativeCol); canvas.drawRect((float) (viewKData.getLeftX() + dp2px(0.5f)), deputyCenterY, (float) viewKData.getRightX() - dp2px(0.5f), (float) (deputyCenterY + Math.abs(macd) * avgHeightMacd), fillPaint); } } } } private void drawVolume(Canvas canvas){ for (KData kData : viewDataList) { //volumeRect if (!isShowInstant){ double openPrice = kData.getOpenPrice(); double closedPrice = kData.getClosePrice(); if (openPrice >= closedPrice) { fillPaint.setColor(priceFallCol); } else { fillPaint.setColor(priceIncreaseCol); } } else { fillPaint.setColor(0xff4db7f3); } canvas.drawRect((float) (kData.getLeftX() + dp2px(0.5f)), (float) (volumeImgBot - kData.getVolume() * avgHeightPerVolume), (float) kData.getRightX() - dp2px(0.5f), volumeImgBot, fillPaint); } } //贝塞尔曲线 private void drawBezierCurve(Canvas canvas) { mainMa5PointList.clear(); mainMa10PointList.clear(); mainMa30PointList.clear(); volumeMa5PointList.clear(); volumeMa10PointList.clear(); deputyMa5PointList.clear(); deputyMa10PointList.clear(); deputyMa30PointList.clear(); for (KData kData : viewDataList) { if (!kData.isInitFinish()) { break; } //volumeMA Pointer volumeMa5Point = new Pointer(); if (kData.getVolumeMa5() > 0) { volumeMa5Point.setX((float) (kData.getLeftX() + avgPriceRectWidth / 2)); volumeMa5Point.setY((float) (volumeImgBot - kData.getVolumeMa5() * avgHeightPerVolume)); volumeMa5PointList.add(volumeMa5Point); } Pointer volumeMa10Point = new Pointer(); if (kData.getVolumeMa10() > 0) { volumeMa10Point.setX((float) (kData.getLeftX() + avgPriceRectWidth / 2)); volumeMa10Point.setY((float) (volumeImgBot - kData.getVolumeMa10() * avgHeightPerVolume)); volumeMa10PointList.add(volumeMa10Point); } switch (mainImgType) { //priceMA case MAIN_IMG_MA: Pointer priceMa5Point = new Pointer(); if (kData.getPriceMa5() > 0) { priceMa5Point.setX((float) (kData.getLeftX() + avgPriceRectWidth / 2)); priceMa5Point.setY((float) (mMaxPriceY + (maxPrice - kData.getPriceMa5()) * avgHeightPerPrice)); mainMa5PointList.add(priceMa5Point); } Pointer priceMa10Point = new Pointer(); if (kData.getPriceMa10() > 0) { priceMa10Point.setX((float) (kData.getLeftX() + avgPriceRectWidth / 2)); priceMa10Point.setY((float) (mMaxPriceY + (maxPrice - kData.getPriceMa10()) * avgHeightPerPrice)); mainMa10PointList.add(priceMa10Point); } Pointer priceMa30Point = new Pointer(); if (kData.getPriceMa30() > 0) { priceMa30Point.setX((float) (kData.getLeftX() + avgPriceRectWidth / 2)); priceMa30Point.setY((float) (mMaxPriceY + (maxPrice - kData.getPriceMa30()) * avgHeightPerPrice)); mainMa30PointList.add(priceMa30Point); } break; //priceEMA case MAIN_IMG_EMA: Pointer ema5Point = new Pointer(); if (kData.getEma5() > 0) { ema5Point.setX((float) (kData.getLeftX() + avgPriceRectWidth / 2)); ema5Point.setY((float) (mMaxPriceY + (maxPrice - kData.getEma5()) * avgHeightPerPrice)); mainMa5PointList.add(ema5Point); } Pointer ema10Point = new Pointer(); if (kData.getEma10() > 0) { ema10Point.setX((float) (kData.getLeftX() + avgPriceRectWidth / 2)); ema10Point.setY((float) (mMaxPriceY + (maxPrice - kData.getEma10()) * avgHeightPerPrice)); mainMa10PointList.add(ema10Point); } Pointer ema30Point = new Pointer(); if (kData.getEma30() > 0) { ema30Point.setX((float) (kData.getLeftX() + avgPriceRectWidth / 2)); ema30Point.setY((float) (mMaxPriceY + (maxPrice - kData.getEma30()) * avgHeightPerPrice)); mainMa30PointList.add(ema30Point); } break; //priceBOLL case MAIN_IMG_BOLL: Pointer bollMbPoint = new Pointer(); if (kData.getBollMb() > 0) { bollMbPoint.setX((float) (kData.getLeftX() + avgPriceRectWidth / 2)); bollMbPoint.setY((float) (mMaxPriceY + (maxPrice - kData.getBollMb()) * avgHeightPerPrice)); mainMa5PointList.add(bollMbPoint); } Pointer bollUpPoint = new Pointer(); if (kData.getBollUp() > 0) { bollUpPoint.setX((float) (kData.getLeftX() + avgPriceRectWidth / 2)); bollUpPoint.setY((float) (mMaxPriceY + (maxPrice - kData.getBollUp()) * avgHeightPerPrice)); mainMa10PointList.add(bollUpPoint); } Pointer bollDnPoint = new Pointer(); if (kData.getBollDn() > 0) { bollDnPoint.setX((float) (kData.getLeftX() + avgPriceRectWidth / 2)); bollDnPoint.setY((float) (mMaxPriceY + (maxPrice - kData.getBollDn()) * avgHeightPerPrice)); mainMa30PointList.add(bollDnPoint); } break; } if (isShowDeputy && deputyImgType == DEPUTY_IMG_MACD) { Pointer difPoint = new Pointer(); if (kData.getDif() > 0) { difPoint.setX((float) (kData.getLeftX() + avgPriceRectWidth / 2)); difPoint.setY((float) (deputyCenterY - kData.getDif() * avgHeightDif)); } else { difPoint.setX((float) (kData.getLeftX() + avgPriceRectWidth / 2)); difPoint.setY((float) (deputyCenterY + Math.abs(kData.getDif() * avgHeightDif))); } deputyMa10PointList.add(difPoint); Pointer deaPoint = new Pointer(); if (kData.getDea() > 0) { deaPoint.setX((float) (kData.getLeftX() + avgPriceRectWidth / 2)); deaPoint.setY((float) (deputyCenterY - kData.getDea() * avgHeightDea)); } else { deaPoint.setX((float) (kData.getLeftX() + avgPriceRectWidth / 2)); deaPoint.setY((float) (deputyCenterY + Math.abs(kData.getDea() * avgHeightDea))); } deputyMa30PointList.add(deaPoint); } else if (isShowDeputy && deputyImgType == DEPUTY_IMG_KDJ) { Pointer kPoint = new Pointer(); if (kData.getK() > 0) { kPoint.setX((float) (kData.getLeftX() + avgPriceRectWidth / 2)); kPoint.setY((float) (horizontalYList.get(5) - kData.getK() * avgHeightK)); deputyMa5PointList.add(kPoint); } Pointer dPoint = new Pointer(); if (kData.getD() > 0) { dPoint.setX((float) (kData.getLeftX() + avgPriceRectWidth / 2)); dPoint.setY((float) (horizontalYList.get(5) - kData.getD() * avgHeightD)); deputyMa10PointList.add(dPoint); } Pointer jPoint = new Pointer(); if (kData.getJ() > 0) { jPoint.setX((float) (kData.getLeftX() + avgPriceRectWidth / 2)); jPoint.setY((float) (horizontalYList.get(5) - kData.getJ() * avgHeightJ)); deputyMa30PointList.add(jPoint); } } else if (isShowDeputy && deputyImgType == DEPUTY_IMG_RSI) { Pointer rs1Point = new Pointer(); if (kData.getRs1() >= 0) { rs1Point.setX((float) (kData.getLeftX() + avgPriceRectWidth / 2)); rs1Point.setY((float) (horizontalYList.get(5) - kData.getRs1() * avgHeightRSI)); deputyMa5PointList.add(rs1Point); } Pointer rs2Point = new Pointer(); if (kData.getRs2() >= 0) { rs2Point.setX((float) (kData.getLeftX() + avgPriceRectWidth / 2)); rs2Point.setY((float) (horizontalYList.get(5) - kData.getRs2() * avgHeightRSI)); deputyMa10PointList.add(rs2Point); } Pointer rs3Point = new Pointer(); if (kData.getRs3() >= 0) { rs3Point.setX((float) (kData.getLeftX() + avgPriceRectWidth / 2)); rs3Point.setY((float) (horizontalYList.get(5) - kData.getRs3() * avgHeightRSI)); deputyMa30PointList.add(rs3Point); } } } drawVolumeBezierCurve(canvas); drawMainBezierCurve(canvas); if (isShowDeputy) { drawDeputyCurve(canvas); } } //主图 MA曲线 private void drawMainBezierCurve(@NonNull Canvas canvas) { QuotaUtil.setBezierPath(mainMa5PointList, curvePath); resetStrokePaint(priceMa5Col, 0); canvas.drawPath(curvePath, strokePaint); QuotaUtil.setBezierPath(mainMa10PointList, curvePath); resetStrokePaint(priceMa10Col, 0); canvas.drawPath(curvePath, strokePaint); QuotaUtil.setBezierPath(mainMa30PointList, curvePath); resetStrokePaint(priceMa30Col, 0); canvas.drawPath(curvePath, strokePaint); } //volume MA曲线 private void drawVolumeBezierCurve(@NonNull Canvas canvas) { QuotaUtil.setBezierPath(volumeMa5PointList, curvePath); resetStrokePaint(priceMa5Col, 0); canvas.drawPath(curvePath, strokePaint); QuotaUtil.setBezierPath(volumeMa10PointList, curvePath); resetStrokePaint(priceMa10Col, 0); canvas.drawPath(curvePath, strokePaint); } //副图 曲线 private void drawDeputyCurve(@NonNull Canvas canvas) { QuotaUtil.setLinePath(deputyMa5PointList, curvePath); resetStrokePaint(priceMa5Col, 0); canvas.drawPath(curvePath, strokePaint); QuotaUtil.setLinePath(deputyMa10PointList, curvePath); resetStrokePaint(priceMa10Col, 0); canvas.drawPath(curvePath, strokePaint); QuotaUtil.setLinePath(deputyMa30PointList, curvePath); resetStrokePaint(priceMa30Col, 0); canvas.drawPath(curvePath, strokePaint); } //获取单击位置的数据 private void getClickKData(float clickX) { if (isShowDetail) { detailRightDataList.clear(); for (int i = 0; i < viewDataList.size(); i++) { if (viewDataList.get(i).getLeftX() <= clickX && viewDataList.get(i).getRightX() >= clickX) { lastKData = viewDataList.get(i); detailRightDataList.add(formatDate(lastKData.getTime())); detailRightDataList.add(setPrecision(lastKData.getOpenPrice(), 2)); detailRightDataList.add(setPrecision(lastKData.getMaxPrice(), 2)); detailRightDataList.add(setPrecision(lastKData.getMinPrice(), 2)); detailRightDataList.add(setPrecision(lastKData.getClosePrice(), 2)); double upDnAmount = lastKData.getUpDnAmount(); if (upDnAmount > 0) { detailRightDataList.add("+" + setPrecision(upDnAmount, 2)); detailRightDataList.add("+" + setPrecision(lastKData.getUpDnRate() * 100, 2) + "%"); } else { detailRightDataList.add(setPrecision(upDnAmount, 2)); detailRightDataList.add(setPrecision(lastKData.getUpDnRate() * 100, 2) + "%"); } detailRightDataList.add(setPrecision(lastKData.getVolume(), 2)); break; } else { lastKData = null; } } } else { lastKData = viewDataList.get(viewDataList.size() - 1); } } //十字线 private void drawCrossHairLine(Canvas canvas) { if (lastKData == null || !isShowDetail) { return; } //垂直 resetStrokePaint(crossHairCol, 0); canvas.drawLine((float) (lastKData.getLeftX() + avgPriceRectWidth / 2), horizontalYList.get(0), (float) (lastKData.getLeftX() + avgPriceRectWidth / 2), horizontalYList.get(horizontalYList.size() - 1), strokePaint); //水平 double moveY; switch (crossHairMoveMode) { case CROSS_HAIR_MOVE_OPEN: moveY = lastKData.getOpenY(); break; case CROSS_HAIR_MOVE_FREE: moveY = longPressMoveY; break; case CROSS_HAIR_MOVE_CLOSE: default: moveY = lastKData.getCloseY(); break; } if (moveY < horizontalYList.get(0)) { moveY = horizontalYList.get(0); } else if (moveY > priceImgBot) { moveY = priceImgBot; } resetStrokePaint(crossHairCol, 0); canvas.drawLine(verticalXList.get(0), (float) moveY, verticalXList.get(verticalXList.size() - 1), (float) moveY, strokePaint); //底部标签 RectF grayRectF = new RectF((float) (lastKData.getLeftX() + avgPriceRectWidth / 2 - dp2px(25)), bottomEnd - dp2px(20), (float) (lastKData.getLeftX() + avgPriceRectWidth / 2 + dp2px(25)), bottomEnd); fillPaint.setColor(crossHairBottomLabelCol); canvas.drawRoundRect(grayRectF, 4, 4, fillPaint); //底部标签text String moveTime = formatDate(lastKData.getTime()); resetStrokePaint(crossHairBottomLabelTextCol, crossHairBottomLabelTextSize); canvas.drawText(moveTime, (float) (lastKData.getLeftX() + avgPriceRectWidth / 2 - strokePaint.measureText(moveTime) / 2), bottomEnd - dp2px(7), strokePaint); //右侧标签 RectF blueRectF = new RectF(rightEnd - dp2px(38), (float) moveY - dp2px(7), rightEnd - dp2px(1), (float) moveY + dp2px(7)); fillPaint.setColor(crossHairRightLabelCol); canvas.drawRoundRect(blueRectF, 4, 4, fillPaint); curvePath.reset(); curvePath.moveTo(verticalXList.get(verticalXList.size() - 1), (float) moveY); curvePath.lineTo(rightEnd - dp2px(37), (float) moveY - dp2px(3)); curvePath.lineTo(rightEnd - dp2px(37), (float) moveY + dp2px(3)); curvePath.close(); canvas.drawPath(curvePath, fillPaint); double avgPricePerHeight; if (!isShowDeputy) { avgPricePerHeight = (topPrice - botPrice) / (horizontalYList.get(4) - horizontalYList.get(0)); } else { avgPricePerHeight = (topPrice - botPrice) / (horizontalYList.get(3) - horizontalYList.get(0)); } String movePrice = formatKDataNum(topPrice - avgPricePerHeight * ((float) moveY - horizontalYList.get(0))); Rect textRect = new Rect(); resetStrokePaint(crossHairRightLabelTextCol, crossHairRightLabelTextSize); strokePaint.getTextBounds(movePrice, 0, movePrice.length(), textRect); canvas.drawText(movePrice, rightEnd - dp2px(38) + (blueRectF.width() - textRect.width()) / 2, (float) moveY + dp2px(7) - (blueRectF.height() - textRect.height()) / 2, strokePaint); } //最高价、最低价标签 private void drawMaxMinPriceLabel(Canvas canvas) { //maxPrice Rect maxPriceRect = new Rect(); String maxPriceStr = setPrecision(maxPrice, 2); resetStrokePaint(priceMaxLabelTextCol, priceMaxLabelTextSize); strokePaint.getTextBounds(maxPriceStr, 0, maxPriceStr.length(), maxPriceRect); RectF maxRectF; float maxPriceTextX; if (maxPriceX + maxPriceRect.width() + dp2px(8) < verticalXList.get(verticalXList.size() - 1)) { maxRectF = new RectF((float) (maxPriceX + dp2px(3)), (float) mMaxPriceY - dp2px(7), (float) (maxPriceX + maxPriceRect.width() + dp2px(8)), (float) mMaxPriceY + dp2px(7)); curvePath.reset(); curvePath.moveTo((float) maxPriceX, (float) mMaxPriceY); curvePath.lineTo((float) (maxPriceX + dp2px(4)), (float) mMaxPriceY - dp2px(3)); curvePath.lineTo((float) (maxPriceX + dp2px(4)), (float) mMaxPriceY + dp2px(3)); curvePath.close(); maxPriceTextX = (float) (maxPriceX + dp2px(5)); } else { maxRectF = new RectF((float) (maxPriceX - dp2px(3)), (float) mMaxPriceY - dp2px(7), (float) (maxPriceX - maxPriceRect.width() - dp2px(8)), (float) mMaxPriceY + dp2px(7)); curvePath.reset(); curvePath.moveTo((float) maxPriceX, (float) mMaxPriceY); curvePath.lineTo((float) (maxPriceX - dp2px(4)), (float) mMaxPriceY - dp2px(3)); curvePath.lineTo((float) (maxPriceX - dp2px(4)), (float) mMaxPriceY + dp2px(3)); curvePath.close(); maxPriceTextX = (float) (maxPriceX - dp2px(5)) - maxPriceRect.width(); } fillPaint.setColor(priceMaxLabelCol); canvas.drawRoundRect(maxRectF, 4, 4, fillPaint); canvas.drawPath(curvePath, fillPaint); resetStrokePaint(priceMaxLabelTextCol, priceMaxLabelTextSize); canvas.drawText(maxPriceStr, maxPriceTextX, (float) mMaxPriceY + maxPriceRect.height() / 2f, strokePaint); //minPrice Rect minPriceRect = new Rect(); String minPriceStr = setPrecision(minPrice, 2); resetStrokePaint(priceMinLabelTextCol, priceMinLabelTextSize); strokePaint.getTextBounds(minPriceStr, 0, minPriceStr.length(), minPriceRect); RectF minRectF; float minPriceTextX; if (minPriceX + minPriceRect.width() + dp2px(8) < verticalXList.get(verticalXList.size() - 1)) { minRectF = new RectF((float) (minPriceX + dp2px(3)), (float) mMinPriceY - dp2px(7), (float) (minPriceX + minPriceRect.width() + dp2px(8)), (float) mMinPriceY + dp2px(7)); curvePath.reset(); curvePath.moveTo((float) minPriceX, (float) mMinPriceY); curvePath.lineTo((float) (minPriceX + dp2px(4)), (float) mMinPriceY - dp2px(3)); curvePath.lineTo((float) (minPriceX + dp2px(4)), (float) mMinPriceY + dp2px(3)); curvePath.close(); minPriceTextX = (float) (minPriceX + dp2px(5)); } else { minRectF = new RectF((float) (minPriceX - dp2px(3)), (float) mMinPriceY - dp2px(7), (float) (minPriceX - minPriceRect.width() - dp2px(8)), (float) mMinPriceY + dp2px(7)); curvePath.reset(); curvePath.moveTo((float) minPriceX, (float) mMinPriceY); curvePath.lineTo((float) (minPriceX - dp2px(4)), (float) mMinPriceY - dp2px(3)); curvePath.lineTo((float) (minPriceX - dp2px(4)), (float) mMinPriceY + dp2px(3)); curvePath.close(); minPriceTextX = (float) (minPriceX - dp2px(5)) - minPriceRect.width(); } fillPaint.setColor(priceMinLabelCol); canvas.drawRoundRect(minRectF, 4, 4, fillPaint); canvas.drawPath(curvePath, fillPaint); resetStrokePaint(priceMinLabelTextCol, priceMinLabelTextSize); canvas.drawText(minPriceStr, minPriceTextX, (float) mMinPriceY + minPriceRect.height() / 2f, strokePaint); } private void drawDetailData(Canvas canvas) { if (lastKData == null || !isShowDetail) { return; } resetStrokePaint(detailTextCol, detailTextSize); strokePaint.getTextBounds(detailLeftTitleArr[0], 0, detailLeftTitleArr[0].length(), detailTextRect); if (lastKData.getLeftX() + avgPriceRectWidth / 2 <= getMeasuredWidth() / 2f) { //边框(右侧) fillPaint.setColor(detailBgCol); canvas.drawRect(verticalXList.get(verticalXList.size() - 1) - detailRectWidth, horizontalYList.get(0), verticalXList.get(verticalXList.size() - 1), horizontalYList.get(0) + detailRectHeight, fillPaint); resetStrokePaint(detailFrameCol, 0); canvas.drawLine(verticalXList.get(verticalXList.size() - 1) - detailRectWidth, horizontalYList.get(0), verticalXList.get(verticalXList.size() - 1) - detailRectWidth, horizontalYList.get(0) + detailRectHeight, strokePaint); canvas.drawLine(verticalXList.get(verticalXList.size() - 1) - detailRectWidth, horizontalYList.get(0), verticalXList.get(verticalXList.size() - 1), horizontalYList.get(0), strokePaint); canvas.drawLine(verticalXList.get(verticalXList.size() - 1), horizontalYList.get(0), verticalXList.get(verticalXList.size() - 1), horizontalYList.get(0) + detailRectHeight, strokePaint); canvas.drawLine(verticalXList.get(verticalXList.size() - 1) - detailRectWidth, horizontalYList.get(0) + detailRectHeight, verticalXList.get(verticalXList.size() - 1), horizontalYList.get(0) + detailRectHeight, strokePaint); //详情字段 resetStrokePaint(detailTextCol, detailTextSize); for (int i = 0; i < detailLeftTitleArr.length; i++) { canvas.drawText(detailLeftTitleArr[i], verticalXList.get(verticalXList.size() - 1) - detailRectWidth + dp2px(4), horizontalYList.get(0) + detailTextVerticalSpace * i + detailTextRect.height() + (detailTextVerticalSpace - detailTextRect.height()) / 2, strokePaint); } //详情数据 for (int i = 0; i < detailRightDataList.size(); i++) { if (i == 5 || i == 6) { if (lastKData.getUpDnAmount() > 0) { resetStrokePaint(priceIncreaseCol, detailTextSize); } else { resetStrokePaint(priceFallCol, detailTextSize); } } else { resetStrokePaint(detailTextCol, detailTextSize); } canvas.drawText(detailRightDataList.get(i), verticalXList.get(verticalXList.size() - 1) - dp2px(4) - strokePaint.measureText(detailRightDataList.get(i)), horizontalYList.get(0) + detailTextVerticalSpace * i + detailTextRect.height() + (detailTextVerticalSpace - detailTextRect.height()) / 2, strokePaint); } } else { //边框(左侧) fillPaint.setColor(detailBgCol); canvas.drawRect(verticalXList.get(0), horizontalYList.get(0), verticalXList.get(0) + detailRectWidth, horizontalYList.get(0) + detailRectHeight, fillPaint); resetStrokePaint(detailFrameCol, 0); canvas.drawLine(verticalXList.get(0), horizontalYList.get(0), verticalXList.get(0), horizontalYList.get(0) + detailRectHeight, strokePaint); canvas.drawLine(verticalXList.get(0), horizontalYList.get(0), verticalXList.get(0) + detailRectWidth, horizontalYList.get(0), strokePaint); canvas.drawLine(verticalXList.get(0) + detailRectWidth, horizontalYList.get(0), verticalXList.get(0) + detailRectWidth, horizontalYList.get(0) + detailRectHeight, strokePaint); canvas.drawLine(verticalXList.get(0), horizontalYList.get(0) + detailRectHeight, verticalXList.get(0) + detailRectWidth, horizontalYList.get(0) + detailRectHeight, strokePaint); //文字详情 resetStrokePaint(detailTextCol, detailTextSize); for (int i = 0; i < detailLeftTitleArr.length; i++) { canvas.drawText(detailLeftTitleArr[i], verticalXList.get(0) + dp2px(4), horizontalYList.get(0) + detailTextVerticalSpace * i + detailTextRect.height() + (detailTextVerticalSpace - detailTextRect.height()) / 2, strokePaint); } //详情数据 for (int i = 0; i < detailRightDataList.size(); i++) { if (i == 5 || i == 6) { if (lastKData.getUpDnAmount() > 0) { resetStrokePaint(priceIncreaseCol, detailTextSize); } else { resetStrokePaint(priceFallCol, detailTextSize); } } else { resetStrokePaint(detailTextCol, detailTextSize); } canvas.drawText(detailRightDataList.get(i), verticalXList.get(0) + detailRectWidth - dp2px(4) - strokePaint.measureText(detailRightDataList.get(i)), horizontalYList.get(0) + detailTextVerticalSpace * i + detailTextRect.height() + (detailTextVerticalSpace - detailTextRect.height()) / 2, strokePaint); } } } //顶部价格MA private void drawTopPriceMAData(Canvas canvas) { if (lastKData == null) { return; } String ma5Str = STR_MA5 + setPrecision(lastKData.getPriceMa5(), 2); String ma10Str = STR_MA10 + setPrecision(lastKData.getPriceMa10(), 2); String ma30Str = STR_MA30 + setPrecision(lastKData.getPriceMa30(), 2); resetStrokePaint(priceMa5Col, topMaTextSize); strokePaint.getTextBounds(ma5Str, 0, ma5Str.length(), topMa5Rect); canvas.drawText(ma5Str, leftStart + dp2px(6), topStart + topMa5Rect.height() + dp2px(6), strokePaint); resetStrokePaint(priceMa10Col, topMaTextSize); strokePaint.getTextBounds(ma10Str, 0, ma10Str.length(), topMa10Rect); canvas.drawText(ma10Str, leftStart + dp2px(6) + topMa5Rect.width() + dp2px(10), topStart + topMa5Rect.height() + dp2px(6), strokePaint); resetStrokePaint(priceMa30Col, topMaTextSize); strokePaint.getTextBounds(ma30Str, 0, ma30Str.length(), topMa30Rect); canvas.drawText(ma30Str, leftStart + dp2px(6) + topMa5Rect.width() + topMa10Rect.width() + dp2px(10) * 2, topStart + topMa5Rect.height() + dp2px(6), strokePaint); } //数量MA private void drawBotMAData(Canvas canvas) { if (lastKData == null) { return; } //VOL String volStr = STR_VOL + setPrecision(lastKData.getVolume(), 2); Rect volRect = new Rect(); resetStrokePaint(volumeTextCol, volumeTextSize); strokePaint.getTextBounds(volStr, 0, volStr.length(), volRect); canvas.drawText(volStr, verticalXList.get(0), priceImgBot + volRect.height() + dp2px(2), strokePaint); String ma5Str = STR_MA5 + setPrecision(lastKData.getVolumeMa5(), 2); Rect volMa5Rect = new Rect(); resetStrokePaint(priceMa5Col, volumeTextSize); strokePaint.getTextBounds(ma5Str, 0, ma5Str.length(), volMa5Rect); canvas.drawText(ma5Str, verticalXList.get(0) + volRect.width() + dp2px(10), priceImgBot + volRect.height() + dp2px(2), strokePaint); String ma10Str = STR_MA10 + setPrecision(lastKData.getVolumeMa10(), 2); resetStrokePaint(priceMa10Col, volumeTextSize); canvas.drawText(ma10Str, verticalXList.get(0) + volMa5Rect.width() + volRect.width() + dp2px(10) * 2, priceImgBot + volRect.height() + dp2px(2), strokePaint); String titleStr = ""; String firstStr = ""; String secondStr = ""; String thirdStr = ""; if (isShowDeputy && deputyImgType == DEPUTY_IMG_MACD) { titleStr = STR_MACD_TITLE; firstStr = STR_MACD + setPrecision(lastKData.getMacd(), 2); secondStr = STR_DIF + setPrecision(lastKData.getDif(), 2); thirdStr = STR_DEA + setPrecision(lastKData.getDea(), 2); } else if (isShowDeputy && deputyImgType == DEPUTY_IMG_KDJ) { titleStr = STR_KDJ_TITLE; firstStr = STR_K + setPrecision(lastKData.getK(), 2); secondStr = STR_D + setPrecision(lastKData.getD(), 2); thirdStr = STR_J + setPrecision(lastKData.getJ(), 2); } else if (isShowDeputy && deputyImgType == DEPUTY_IMG_RSI) { titleStr = STR_RSI_TITLE; firstStr = STR_RS1 + setPrecision(lastKData.getRs1(), 2); secondStr = STR_RS2 + setPrecision(lastKData.getRs2(), 2); thirdStr = STR_RS3 + setPrecision(lastKData.getRs3(), 2); } Rect titleRect = new Rect(); resetStrokePaint(volumeTextCol, volumeTextSize); strokePaint.getTextBounds(titleStr, 0, titleStr.length(), titleRect); canvas.drawText(titleStr, verticalXList.get(0), horizontalYList.get(4) + titleRect.height(), strokePaint); resetStrokePaint(priceMa5Col, volumeTextSize); canvas.drawText(firstStr, verticalXList.get(0) + titleRect.width() + dp2px(10), horizontalYList.get(4) + titleRect.height(), strokePaint); float firstWidth = strokePaint.measureText(firstStr); resetStrokePaint(priceMa10Col, volumeTextSize); canvas.drawText(secondStr, verticalXList.get(0) + titleRect.width() + dp2px(20) + firstWidth, horizontalYList.get(4) + titleRect.height(), strokePaint); float secondWidth = strokePaint.measureText(secondStr); resetStrokePaint(priceMa30Col, volumeTextSize); canvas.drawText(thirdStr, verticalXList.get(0) + titleRect.width() + dp2px(30) + firstWidth + secondWidth, horizontalYList.get(4) + titleRect.height(), strokePaint); } //横坐标 private void drawAbscissa(Canvas canvas) { resetStrokePaint(abscissaTextCol, abscissaTextSize); for (int i = 0; i < verticalXList.size(); i++) { if (i == 0 && viewDataList.get(0).getLeftX() <= verticalXList.get(0) + avgPriceRectWidth / 2 && viewDataList.get(0).getRightX() > verticalXList.get(0)) { canvas.drawText(formatDate(viewDataList.get(0).getTime()), leftStart + dp2px(6), bottomEnd - dp2px(7), strokePaint); } else if (i == verticalXList.size() - 1) { String dateStr = formatDate(viewDataList.get(viewDataList.size() - 1).getTime()); canvas.drawText(dateStr, rightEnd - dp2px(41) - strokePaint.measureText(dateStr), bottomEnd - dp2px(7), strokePaint); } else { for (KData data : viewDataList) { if (data.getLeftX() <= verticalXList.get(i) && data.getRightX() >= verticalXList.get(i)) { String dateStr = formatDate(data.getTime()); canvas.drawText(dateStr, verticalXList.get(i) - strokePaint.measureText(dateStr) / 2, bottomEnd - dp2px(7), strokePaint); break; } } } } } //纵坐标 private void drawOrdinate(@NonNull Canvas canvas) { Rect rect = new Rect(); resetStrokePaint(ordinateTextCol, ordinateTextSize); //最高价 strokePaint.getTextBounds(formatKDataNum(topPrice), 0, formatKDataNum(topPrice).length(), rect); canvas.drawText(formatKDataNum(topPrice), verticalXList.get(verticalXList.size() - 1) + dp2px(4), horizontalYList.get(0) + rect.height() + dp2px(2), strokePaint); //最低价 strokePaint.getTextBounds(formatKDataNum(botPrice), 0, formatKDataNum(botPrice).length(), rect); canvas.drawText(formatKDataNum(botPrice), verticalXList.get(verticalXList.size() - 1) + dp2px(4), priceImgBot - dp2px(2), strokePaint); if (!isShowDeputy) { double avgPrice = (topPrice - botPrice) / 4; for (int i = 0; i < 3; i++) { canvas.drawText(formatKDataNum(topPrice - avgPrice * (i + 1)), verticalXList.get(verticalXList.size() - 1) + dp2px(4), horizontalYList.get(i + 1) + rect.height() / 2f, strokePaint); } } else { double avgPrice = (topPrice - botPrice) / 3; for (int i = 0; i < 2; i++) { canvas.drawText(formatKDataNum(topPrice - avgPrice * (i + 1)), verticalXList.get(verticalXList.size() - 1) + dp2px(4), horizontalYList.get(i + 1) + rect.height() / 2f, strokePaint); } String topDeputy = ""; String botDeputy = ""; String centerDeputy = ""; if (deputyImgType == DEPUTY_IMG_MACD) { if (mMaxMacd > 0 && mMinMacd < 0) { topDeputy = setPrecision(mMaxMacd, 2); botDeputy = setPrecision(mMinMacd, 2); centerDeputy = setPrecision((mMaxMacd - mMinMacd) / 2, 2); } else if (mMaxMacd <= 0) { topDeputy = "0"; botDeputy = setPrecision(mMinMacd, 2); centerDeputy = setPrecision((mMinMacd - mMaxMacd) / 2, 2); } else if (mMinMacd >= 0) { topDeputy = setPrecision(mMaxMacd, 2); botDeputy = "0"; centerDeputy = setPrecision((mMaxMacd - mMinMacd) / 2, 2); } } else if (deputyImgType == DEPUTY_IMG_KDJ) { topDeputy = setPrecision(mMaxK, 2); centerDeputy = setPrecision(mMaxK / 2, 2); botDeputy = "0"; } else if (deputyImgType == DEPUTY_IMG_RSI) { topDeputy = "100"; centerDeputy = "50"; botDeputy = "0"; } canvas.drawText(topDeputy, verticalXList.get(verticalXList.size() - 1) + dp2px(4), horizontalYList.get(horizontalYList.size() - 2) + rect.height() + dp2px(2), strokePaint); canvas.drawText(centerDeputy, verticalXList.get(verticalXList.size() - 1) + dp2px(4), horizontalYList.get(horizontalYList.size() - 1) - verticalSpace / 2 + rect.height() / 2f, strokePaint); canvas.drawText(botDeputy, verticalXList.get(verticalXList.size() - 1) + dp2px(4), horizontalYList.get(horizontalYList.size() - 1) - dp2px(2), strokePaint); } //最高量 strokePaint.getTextBounds(formatVolNum(maxVolume), 0, formatVolNum(maxVolume).length(), rect); canvas.drawText(formatVolNum(maxVolume), verticalXList.get(verticalXList.size() - 1) + dp2px(4), priceImgBot + rect.height() + dp2px(2), strokePaint); //最高量/2 canvas.drawText(formatVolNum(maxVolume / 2), verticalXList.get(verticalXList.size() - 1) + dp2px(4), volumeImgBot - verticalSpace / 2 + rect.height() / 2f, strokePaint); //数量 0 canvas.drawText("0", verticalXList.get(verticalXList.size() - 1) + dp2px(4), volumeImgBot - dp2px(2), strokePaint); } //分时图 private void drawInstant(Canvas canvas){ if (canvas == null || curvePath == null || viewDataList == null || strokePaint == null){ return; } curvePath.reset(); instantPath.reset(); float startX = (float) viewDataList.get(0).getCenterX(); float startY = (float) viewDataList.get(0).getCloseY(); curvePath.moveTo(startX, startY); instantPath.moveTo(startX, startY); int viewDataSize = viewDataList.size(); for (int i = 1; i < viewDataSize; i++) { KData viewData = viewDataList.get(i); curvePath.lineTo((float) viewData.getCenterX(), (float) viewData.getCloseY()); instantPath.lineTo((float) viewData.getCenterX(), (float) viewData.getCloseY()); if (i == viewDataSize - 1){ instantPath.lineTo(verticalXList.get(verticalXList.size() - 1), (float) viewData.getCloseY()); } } resetStrokePaint(0xff1aa3f0, 0); canvas.drawPath(curvePath, strokePaint); instantPath.lineTo(verticalXList.get(verticalXList.size() - 1), horizontalYList.get(horizontalYList.size() - 2)); instantPath.lineTo(startX, horizontalYList.get(horizontalYList.size() - 2)); instantPath.close(); LinearGradient gradient = new LinearGradient(0, (int)mMaxPriceY, 0, horizontalYList.get(horizontalYList.size() - 2), 0x801aa3f0, 0x0d1aa3f0, Shader.TileMode.CLAMP); instantFillPaint.setShader(gradient); canvas.drawPath(instantPath, instantFillPaint); } private int dp2px(float dpValue) { final float scale = getContext().getResources().getDisplayMetrics().density; return (int) (dpValue * scale + 0.5f); } private int sp2px(float spValue) { final float fontScale = getContext().getResources().getDisplayMetrics().scaledDensity; return (int) (spValue * fontScale + 0.5f); } private String formatDate(long timeStamp) { if (timeStamp <= 0) { return ""; } SimpleDateFormat format = new SimpleDateFormat("MM-dd HH:mm"); return format.format(new Date(timeStamp)); } /** * 设置小数位精度 * * @param scale 保留几位小数 */ private String setPrecision(Double num, int scale) { BigDecimal bigDecimal = new BigDecimal(num); return bigDecimal.setScale(scale, BigDecimal.ROUND_DOWN).toPlainString(); } /** * 按量级格式化价格 */ private String formatKDataNum(double num) { if (num < 1) { return setPrecision(num, 6); } else if (num < 10) { return setPrecision(num, 4); } else if (num < 100) { return setPrecision(num, 3); } else if (num < 10000) { return setPrecision(num, 2); } else if (num < 100000000) { return setPrecision(num / 10000, 2) + "万"; } else { return setPrecision(num / 100000000, 2) + "亿"; } } /** * 按量级格式化数量 */ private String formatVolNum(double num) { if (num < 10000) { return setPrecision(num, 2); } else if (num < 100000000) { return setPrecision(num / 10000, 2) + "万"; } else { return setPrecision(num / 100000000, 2) + "亿"; } } private void resetStrokePaint(int colorId, int textSize) { strokePaint.setColor(colorId); strokePaint.setTextSize(sp2px(textSize)); } } ================================================ FILE: app/src/main/java/com/example/admin/klineview/kline/Pointer.java ================================================ package com.example.admin.klineview.kline; /** * Created by xiesuichao on 2018/7/3. */ public class Pointer { private float x; private float y; public Pointer() { } public Pointer(float x, float y) { this.x = x; this.y = y; } public float getX() { return x; } public void setX(float x) { this.x = x; } public float getY() { return y; } public void setY(float y) { this.y = y; } @Override public String toString() { return "Pointer{" + "x=" + x + ", y=" + y + '}'; } } ================================================ FILE: app/src/main/java/com/example/admin/klineview/kline/QuotaThread.java ================================================ package com.example.admin.klineview.kline; import android.os.Handler; import android.os.HandlerThread; import android.os.Message; import android.support.annotation.NonNull; import java.util.List; /** * 子线程计算五项数据 * Created by xiesuichao on 2018/8/18. */ public class QuotaThread extends HandlerThread implements Handler.Callback { public static final int HANDLER_QUOTA_LIST = 100; public static final int HANDLER_QUOTA_SINGLE = 101; private Handler uiHandler; private Handler workHandler; public QuotaThread(String name, int priority) { super(name, priority); } public void setUIHandler(Handler uiHandler) { this.uiHandler = uiHandler; } public void quotaListCalculate(List dataList) { if (workHandler == null) { return; } Message message = Message.obtain(null, HANDLER_QUOTA_LIST); message.obj = dataList; workHandler.sendMessage(message); } public void quotaSingleCalculate(List dataList){ if (workHandler == null) { return; } Message message = Message.obtain(null, HANDLER_QUOTA_SINGLE); message.obj = dataList; workHandler.sendMessage(message); } private void calculateKDataQuota(List dataList, boolean isEndData) { QuotaUtil.initEma(dataList, isEndData); QuotaUtil.initBoll(dataList, isEndData); QuotaUtil.initMACD(dataList, isEndData); QuotaUtil.initKDJ(dataList, isEndData); QuotaUtil.initRSI(dataList, isEndData); QuotaUtil.initMa(dataList, isEndData); } @Override protected void onLooperPrepared() { super.onLooperPrepared(); this.workHandler = new Handler(getLooper(), this); } @Override public boolean handleMessage(@NonNull Message msg) { if (msg.what == HANDLER_QUOTA_LIST) { handleData(msg, HANDLER_QUOTA_LIST, false); }else if (msg.what == HANDLER_QUOTA_SINGLE){ handleData(msg, HANDLER_QUOTA_SINGLE, true); } return true; } private void handleData(Message msg, int whatId, boolean isEndData){ if (msg == null || uiHandler == null) { return; } try { List dataList = (List) msg.obj; calculateKDataQuota(dataList, isEndData); Message message = Message.obtain(null, whatId); uiHandler.sendMessage(message); } catch (Exception e) { e.printStackTrace(); } } } ================================================ FILE: app/src/main/java/com/example/admin/klineview/kline/QuotaUtil.java ================================================ package com.example.admin.klineview.kline; import android.graphics.Path; import java.util.ArrayList; import java.util.List; /** * 五项数据计算公式 * Created by xiesuichao on 2018/8/12. */ public class QuotaUtil { private static final int QUOTA_DAY5 = 5; private static final int QUOTA_DAY10 = 10; private static final int QUOTA_DAY30 = 30; private static final float BEZIER_RATIO = 0.16f; private static List cacheList = new ArrayList<>(); /** * 初始化 MA5,MA10, MA30 * * @param isEndData 是否是添加到list末尾的数据 */ public static void initMa(List dataList, boolean isEndData) { if (dataList == null || dataList.isEmpty() || cacheList == null) { return; } cacheList.clear(); cacheList.addAll(dataList); int size = dataList.size(); for (int i = 0; i < size; i++) { if (i + QUOTA_DAY5 <= size) { //priceMa5 dataList.get(i + QUOTA_DAY5 - 1).setPriceMa5(getPriceMa(cacheList.subList(i, i + QUOTA_DAY5))); //volumeMa5 dataList.get(i + QUOTA_DAY5 - 1).setVolumeMa5(getVolumeMa(cacheList.subList(i, i + QUOTA_DAY5))); } if (i + QUOTA_DAY10 <= size) { //priceMa10 dataList.get(i + QUOTA_DAY10 - 1).setPriceMa10(getPriceMa(cacheList.subList(i, i + QUOTA_DAY10))); //volumeMa10 dataList.get(i + QUOTA_DAY10 - 1).setVolumeMa10(getVolumeMa(cacheList.subList(i, i + QUOTA_DAY10))); } if (i + QUOTA_DAY30 <= size) { //priceMa30 if (dataList.get(i + QUOTA_DAY30 - 1).getPriceMa30() != 0 && !isEndData) { break; } else { dataList.get(i + QUOTA_DAY30 - 1).setPriceMa30(getPriceMa(cacheList.subList(i, i + QUOTA_DAY30))); } } dataList.get(i).setInitFinish(true); } } private static double getVolumeMa(List dataList) { if (dataList == null || dataList.isEmpty()) { return -1; } double sum = 0; for (KData data : dataList) { sum += data.getVolume(); } return sum / dataList.size(); } private static double getPriceMa(List dataList) { if (dataList == null || dataList.isEmpty()) { return -1; } double sum = 0; for (KData data : dataList) { sum += data.getClosePrice(); } return sum / dataList.size(); } /** * 初始化 EMA5, EMA10, EMA30 */ public static void initEma(List dataList, boolean isEndData) { if (dataList == null || dataList.isEmpty()) { return; } double lastEma5 = dataList.get(0).getClosePrice(); double lastEma10 = dataList.get(0).getClosePrice(); double lastEma30 = dataList.get(0).getClosePrice(); dataList.get(0).setEma5(lastEma5); dataList.get(0).setEma10(lastEma10); dataList.get(0).setEma30(lastEma30); int size = dataList.size(); for (int i = 1; i < size; i++) { if (dataList.get(i).getEma30() != 0 && !isEndData) { break; } double currentEma5 = 2 * (dataList.get(i).getClosePrice() - lastEma5) / (QUOTA_DAY5 + 1) + lastEma5; double currentEma10 = 2 * (dataList.get(i).getClosePrice() - lastEma10) / (QUOTA_DAY10 + 1) + lastEma10; double currentEma30 = 2 * (dataList.get(i).getClosePrice() - lastEma30) / (QUOTA_DAY30 + 1) + lastEma30; dataList.get(i).setEma5(currentEma5); dataList.get(i).setEma10(currentEma10); dataList.get(i).setEma30(currentEma30); lastEma5 = currentEma5; lastEma10 = currentEma10; lastEma30 = currentEma30; } } /** * BOLL(n)计算公式: * MA=n日内的收盘价之和÷n。 * MD=(n-1)日的平方根(C-MA)的两次方之和除以n * MB=(n-1)日的MA * UP=MB+k×MD * DN=MB-k×MD * K为参数,可根据股票的特性来做相应的调整,一般默认为2 * * @param dataList 数据集合 * @param period 周期,一般为26 * @param k 参数,可根据股票的特性来做相应的调整,一般默认为2 */ public static void initBOLL(List dataList, int period, int k, boolean isEndData) { if (dataList == null || dataList.isEmpty() || period < 0 || period > dataList.size() - 1) { return; } double mb;//上轨线 double up;//中轨线 double dn;//下轨线 //n日 double sum = 0; //n-1日 double sum2 = 0; //n日MA double ma; //n-1日MA double ma2; double md; int size = dataList.size(); for (int i = 0; i < size; i++) { if (dataList.get(i).getBollMb() != 0 && !isEndData) { break; } KData quotes = dataList.get(i); sum += quotes.getClosePrice(); sum2 += quotes.getClosePrice(); if (i > period - 1) sum -= dataList.get(i - period).getClosePrice(); if (i > period - 2) sum2 -= dataList.get(i - period + 1).getClosePrice(); //这个范围不计算,在View上的反应就是不显示这个范围的boll线 if (i < period - 1) continue; ma = sum / period; ma2 = sum2 / (period - 1); md = 0; for (int j = i + 1 - period; j <= i; j++) { //n-1日 md += Math.pow(dataList.get(j).getClosePrice() - ma, 2); } md = Math.sqrt(md / period); //(n-1)日的MA mb = ma2; up = mb + k * md; dn = mb - k * md; quotes.setBollMb(mb); quotes.setBollUp(up); quotes.setBollDn(dn); } } public static void initBoll(List dataList, boolean isEndData) { initBOLL(dataList, 26, 2, isEndData); } /** * MACD * * @param dataList * @param fastPeriod 日快线移动平均,标准为12,按照标准即可 * @param slowPeriod 日慢线移动平均,标准为26,可理解为天数 * @param signalPeriod 日移动平均,标准为9,按照标准即可 */ public static void initMACD(List dataList, int fastPeriod, int slowPeriod, int signalPeriod, boolean isEndData) { if (dataList == null || dataList.isEmpty()) { return; } double preEma_12 = 0; double preEma_26 = 0; double preDEA = 0; double ema_12 = 0; double ema_26 = 0; double dea = 0; double dif = 0; double macd = 0; int size = dataList.size(); for (int i = 0; i < size; i++) { if (dataList.get(i).getMacd() != 0 && !isEndData) { break; } ema_12 = preEma_12 * (fastPeriod - 1) / (fastPeriod + 1) + dataList.get(i).getClosePrice() * 2 / (fastPeriod + 1); ema_26 = preEma_26 * (slowPeriod - 1) / (slowPeriod + 1) + dataList.get(i).getClosePrice() * 2 / (slowPeriod + 1); dif = ema_12 - ema_26; dea = preDEA * (signalPeriod - 1) / (signalPeriod + 1) + dif * 2 / (signalPeriod + 1); macd = 2 * (dif - dea); preEma_12 = ema_12; preEma_26 = ema_26; preDEA = dea; dataList.get(i).setMacd(macd); dataList.get(i).setDea(dea); dataList.get(i).setDif(dif); } } public static void initMACD(List dataList, boolean isEndData) { initMACD(dataList, 12, 26, 9, isEndData); } /** * KDJ * * @param n1 标准值9 * @param n2 标准值3 * @param n3 标准值3 */ public static void initKDJ(List dataList, int n1, int n2, int n3, boolean isEndData) { if (dataList == null || dataList.isEmpty()) { return; } //K值 double[] mK = new double[dataList.size()]; //D值 double[] mD = new double[dataList.size()]; //J值 double jValue; double highValue = dataList.get(0).getMaxPrice(); double lowValue = dataList.get(0).getMinPrice(); //记录最高价位置 int highPosition = 0; //记录最低价位置 int lowPosition = 0; double rSV = 0.0; int size = dataList.size(); for (int i = 0; i < size; i++) { if (dataList.get(i).getK() != 0 && !isEndData) { break; } if (i == 0) { // mK[0] = 33.33; // mD[0] = 11.11; // jValue = 77.78; mK[0] = 50; mD[0] = 50; jValue = 50; } else { //对最高价和最低价赋值 if (highValue <= dataList.get(i).getMaxPrice()) { highValue = dataList.get(i).getMaxPrice(); highPosition = i; } if (lowValue >= dataList.get(i).getMinPrice()) { lowValue = dataList.get(i).getMinPrice(); lowPosition = i; } if (i > (n1 - 1)) { //判断存储的最高价是否高于当前最高价 if (highValue > dataList.get(i).getMaxPrice()) { //判断最高价是不是在最近n天内,若不在最近n天内,则从最近n天找出最高价并赋值 if (highPosition < (i - (n1 - 1))) { highValue = dataList.get(i - (n1 - 1)).getMaxPrice(); for (int j = (i - (n1 - 2)); j <= i; j++) { if (highValue <= dataList.get(j).getMaxPrice()) { highValue = dataList.get(j).getMaxPrice(); highPosition = j; } } } } if ((lowValue < dataList.get(i).getMinPrice())) { if (lowPosition < i - (n1 - 1)) { lowValue = dataList.get(i).getMinPrice(); for (int k = i - (n1 - 2); k <= i; k++) { if (lowValue >= dataList.get(k).getMinPrice()) { lowValue = dataList.get(k).getMinPrice(); lowPosition = k; } } } } } if (highValue != lowValue) { rSV = (dataList.get(i).getClosePrice() - lowValue) / (highValue - lowValue) * 100; } mK[i] = (mK[i - 1] * (n2 - 1) + rSV) / n2; mD[i] = (mD[i - 1] * (n3 - 1) + mK[i]) / n3; jValue = 3 * mK[i] - 2 * mD[i]; } dataList.get(i).setK(mK[i]); dataList.get(i).setD(mD[i]); dataList.get(i).setJ(jValue); } } public static void initKDJ(List dataList, boolean isEndData) { initKDJ(dataList, 9, 3, 3, isEndData); } /** * 初始化RSI * * @param period1 标准值6 * @param period2 标准值12 * @param period3 标准值24 */ public static void initRSI(List dataList, int period1, int period2, int period3, boolean isEndData) { if (dataList == null || dataList.isEmpty()) { return; } double upRateSum; int upRateCount; double dnRateSum; int dnRateCount; int size = dataList.size(); for (int i = 0; i < size; i++) { if (dataList.get(i).getRs3() != 0 && !isEndData) { break; } upRateSum = 0; upRateCount = 0; dnRateSum = 0; dnRateCount = 0; if (i >= period1 - 1) { for (int x = i; x >= i + 1 - period1; x--) { if (dataList.get(x).getUpDnRate() >= 0) { upRateSum += dataList.get(x).getUpDnRate(); upRateCount++; } else { dnRateSum += dataList.get(x).getUpDnRate(); dnRateCount++; } } double avgUpRate = 0; double avgDnRate = 0; if (upRateSum > 0) { avgUpRate = upRateSum / upRateCount; } if (dnRateSum < 0) { avgDnRate = dnRateSum / dnRateCount; } dataList.get(i).setRs1(avgUpRate / (avgUpRate + Math.abs(avgDnRate)) * 100); } upRateSum = 0; upRateCount = 0; dnRateSum = 0; dnRateCount = 0; if (i >= period2 - 1) { for (int x = i; x >= i + 1 - period2; x--) { if (dataList.get(x).getUpDnRate() >= 0) { upRateSum += dataList.get(x).getUpDnRate(); upRateCount++; } else { dnRateSum += dataList.get(x).getUpDnRate(); dnRateCount++; } } double avgUpRate = 0; double avgDnRate = 0; if (upRateSum > 0) { avgUpRate = upRateSum / upRateCount; } if (dnRateSum < 0) { avgDnRate = dnRateSum / dnRateCount; } dataList.get(i).setRs2(avgUpRate / (avgUpRate + Math.abs(avgDnRate)) * 100); } upRateSum = 0; upRateCount = 0; dnRateSum = 0; dnRateCount = 0; if (i >= period3 - 1) { for (int x = i; x >= i + 1 - period3; x--) { if (dataList.get(x).getUpDnRate() >= 0) { upRateSum += dataList.get(x).getUpDnRate(); upRateCount++; } else { dnRateSum += dataList.get(x).getUpDnRate(); dnRateCount++; } } double avgUpRate = 0; double avgDnRate = 0; if (upRateSum > 0) { avgUpRate = upRateSum / upRateCount; } if (dnRateSum < 0) { avgDnRate = dnRateSum / dnRateCount; } dataList.get(i).setRs3(avgUpRate / (avgUpRate + Math.abs(avgDnRate)) * 100); } } } public static void initRSI(List dataList, boolean isEndData) { initRSI(dataList, 6, 12, 24, isEndData); } /** * 三阶贝塞尔曲线控制点 * * @param pointList * @param path */ public static void setBezierPath(List pointList, Path path) { if (path == null){ return; } path.reset(); if (pointList == null || pointList.isEmpty()) { return; } path.moveTo(pointList.get(0).getX(), pointList.get(0).getY()); Pointer leftControlPointer = new Pointer(); Pointer rightControlPointer = new Pointer(); int size = pointList.size(); for (int i = 0; i < size; i++) { if (i == 0 && size > 2) { leftControlPointer.setX(pointList.get(i).getX() + BEZIER_RATIO * (pointList.get(i + 1).getX() - pointList.get(0).getX())); leftControlPointer.setY(pointList.get(i).getY() + BEZIER_RATIO * (pointList.get(i + 1).getY() - pointList.get(0).getY())); rightControlPointer.setX(pointList.get(i + 1).getX() - BEZIER_RATIO * (pointList.get(i + 2).getX() - pointList.get(i).getX())); rightControlPointer.setY(pointList.get(i + 1).getY() - BEZIER_RATIO * (pointList.get(i + 2).getY() - pointList.get(i).getY())); } else if (i == size - 2 && i > 1) { leftControlPointer.setX(pointList.get(i).getX() + BEZIER_RATIO * (pointList.get(i + 1).getX() - pointList.get(i - 1).getX())); leftControlPointer.setY(pointList.get(i).getY() + BEZIER_RATIO * (pointList.get(i + 1).getY() - pointList.get(i - 1).getY())); rightControlPointer.setX(pointList.get(i + 1).getX() - BEZIER_RATIO * (pointList.get(i + 1).getX() - pointList.get(i).getX())); rightControlPointer.setY(pointList.get(i + 1).getY() - BEZIER_RATIO * (pointList.get(i + 1).getY() - pointList.get(i).getY())); } else if (i > 0 && i < size - 2) { leftControlPointer.setX(pointList.get(i).getX() + BEZIER_RATIO * (pointList.get(i + 1).getX() - pointList.get(i - 1).getX())); leftControlPointer.setY(pointList.get(i).getY() + BEZIER_RATIO * (pointList.get(i + 1).getY() - pointList.get(i - 1).getY())); rightControlPointer.setX(pointList.get(i + 1).getX() - BEZIER_RATIO * (pointList.get(i + 2).getX() - pointList.get(i).getX())); rightControlPointer.setY(pointList.get(i + 1).getY() - BEZIER_RATIO * (pointList.get(i + 2).getY() - pointList.get(i).getY())); } if (i < size - 1) { path.cubicTo(leftControlPointer.getX(), leftControlPointer.getY(), rightControlPointer.getX(), rightControlPointer.getY(), pointList.get(i + 1).getX(), pointList.get(i + 1).getY()); } } } public static void setLinePath(List pointerList, Path path) { if (path == null){ return; } if (pointerList == null || pointerList.size() <= 1) { return; } path.moveTo(pointerList.get(0).getX(), pointerList.get(0).getY()); int size = pointerList.size(); for (int i = 1; i < size; i++) { path.lineTo(pointerList.get(i).getX(), pointerList.get(i).getY()); } } } ================================================ FILE: app/src/main/res/drawable/ic_launcher_background.xml ================================================ ================================================ FILE: app/src/main/res/drawable-v24/ic_launcher_foreground.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_depth.xml ================================================