This commit is contained in:
coco
2026-07-03 15:56:07 +08:00
commit caef23209c
5767 changed files with 1004268 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
/build
+62
View File
@@ -0,0 +1,62 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion 28
defaultConfig {
minSdkVersion 16
targetSdkVersion 28
versionCode 1
versionName "1.0"
}
lintOptions {
abortOnError false
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
api 'androidx.appcompat:appcompat:1.0.2'
api "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
api "com.google.android.material:material:1.2.1"
api "androidx.constraintlayout:constraintlayout:1.1.3"
//Google AAC(Android Architecture Components)android架构组件
// implementation rootProject.ext.dependencies["lifecycle-extensions"]
def lifecycle_version = "2.2.0"
api "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
api "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
api "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
api "com.yanzhenjie.permission:x:2.0.1"
//Rx
api "com.uber.autodispose:autodispose-android-archcomponents:1.1.0"
api "io.reactivex.rxjava3:rxjava:3.0.0"
api "io.reactivex.rxjava3:rxandroid:3.0.0"
}
repositories {
mavenCentral()
}
// must be applied after your artifact generating plugin (eg. java / com.android.library)
//apply plugin: 'com.novoda.bintray-release'
//publish {
// userOrg = 'oxo1' //bintray.com 账户下某个组织id
// groupId = 'com.openxu.viewlib' //jcenter上的路径,一般为包名
// artifactId = 'OXViewLib' //项目名称
// publishVersion = '1.0.3' //版本号
// desc = 'custom chart for android' //项目介绍,可以不写
// website = '' //项目主页,可以不写
//}
+92
View File
@@ -0,0 +1,92 @@
/**③.发布到JCenter*/
apply plugin: 'com.jfrog.bintray'
apply plugin: 'com.github.dcendents.android-maven'
def siteUrl = 'https://github.com/openXu/OXChart' //项目主页
def gitUrl = 'https://github.com/openXu/OXChart.git'//git仓库
group = 'com.openxu.viewlib' //groupid 一般为包名
version = '1.0.0' //版本号
install {
repositories.mavenInstaller {
pom {
project {
packaging 'aar'
name 'OXViewLib'// 可选,项目名称
description 'custom chart for android'// 可选,项目描述
url siteUrl //项目地址
// 软件开源协议
licenses {
license {
name 'The Apache Software License, Version 2.0'
url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
}
}
//开发者信息
developers {
developer {
id 'OPEN-XU' //yourId
name 'OPEN-XU' //yourName
email '17373613414@yeah.net' //yourEmail
}
}
scm {
connection gitUrl
developerConnection gitUrl
url siteUrl
}
}
}
}
}
task javadoc(type: Javadoc) {
options {
encoding "UTF-8"
charSet 'UTF-8'
author true
version true
links "http://docs.oracle.com/javase/7/docs/api"
}
source = android.sourceSets.main.java.srcDirs
classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
options.encoding = "UTF-8"
failOnError false // 忽略注释语法错误,如果用jdk1.8你的注释写的不规范就编译不过。
}
// 生成jar包的task
task sourcesJar(type: Jar) {
classifier = 'sources'
from android.sourceSets.main.java.srcDirs
}
// 生成javaDoc的jar,不需要修改。
task javadocJar(type: Jar, dependsOn: javadoc) {
classifier = 'javadoc'
from javadoc.destinationDir
}
artifacts {
archives javadocJar
archives sourcesJar
// archives androidJavadocsJar
}
bintray {
Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())
user = properties.getProperty("bintray.user")
key = properties.getProperty("bintray.apikey")
configurations = ['archives']
pkg {
repo = 'maven'
name = 'OXViewLib' //发布到JCenter上项目名
userOrg = 'oxo1'
websiteUrl = siteUrl
vcsUrl = gitUrl
licenses = ['Apache-2.0']
publish = true
}
}
+13
View File
@@ -0,0 +1,13 @@
/**①.发布到本地maven仓库*/
apply plugin: 'maven'
//添加下面配置后,在Gradle projects窗口中:OXViewLib->Tasks中会多出一个upload目录,里面就有一个名为uploadArchives的task
//uploadArchives就是用于发布到仓库的task
uploadArchives{
repositories.mavenDeployer{
// 配置本地仓库路径,项目根目录下的repository目录中
repository(url: uri('../repository'))
pom.groupId = "com.openxu.viewlib"// 唯一标识(通常为模块包名,也可以任意)
pom.artifactId = "OXViewLib" // 项目名称(通常为类库模块名称,也可以任意)
pom.version = "1.0.0" // 版本号
}
}
+37
View File
@@ -0,0 +1,37 @@
/**②.发布到私有服务器maven仓库*/
apply plugin: 'maven'
/*task androidJavadocs(type: Javadoc) {
source = android.sourceSets.main.java.srcDirs
classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
}*/
//打包java文档的 task
/*task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) {
classifier = 'javadoc'
from androidJavadocs.destinationDir
}*/
//打包main目录下文件的 task
task androidSourcesJar(type: Jar) {
classifier = 'sources'
from android.sourceSets.main.java.srcDirs
}
//配置需要上传到maven仓库的文件
artifacts {
archives androidSourcesJar
// archives androidJavadocsJar
}
//上传到Maven仓库的task
uploadArchives {
repositories {
mavenDeployer {
//指定maven仓库url
repository(url: "http://localhost:8081/nexus/content/repositories/releases/") {
//nexus登录默认用户名和密码
authentication(userName: "admin", password: "admin123")
}
pom.groupId = "com.openxu.viewlib"// 唯一标识(通常为模块包名,也可以任意)
pom.artifactId = "OXViewLib" // 项目名称(通常为类库模块名称,也可以任意)
pom.version = "1.0.0" // 版本号
}
}
}
+21
View File
@@ -0,0 +1,21 @@
# 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
@@ -0,0 +1,2 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.openxu.cview" />
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

@@ -0,0 +1,38 @@
package com.openxu;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import java.util.Stack;
/**
* Author: openX
* Time: 2019/2/23 14:49
* class: BaseApplication
* Description: Application请继承该基类
*/
public class MyApplication extends Application {
private static Stack<Activity> activityStack = new Stack<>();
private static Context mcontext;
protected static MyApplication application;
@Override
public void onCreate() {
application = this;
mcontext = getApplicationContext();
super.onCreate();
}
public static MyApplication getApplication() {
return application;
}
public static Context getContext() {
return mcontext;
}
}
@@ -0,0 +1,609 @@
package com.openxu.cview;
import android.app.Activity;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
import com.openxu.utils.DensityUtil;
/**
* autour: openXu
* date: 2017/3/10 13:32
* className: TitleLayout
* version:
* description: 通用的titlebar
* Sample:(属性详情参照attrs.xml-TitleView)
*/
public class TitleLayout extends RelativeLayout {
private String TAG = "TitleLayout";
private View view_tran_bar; //状态栏预留占位
/*总容器*/
private RelativeLayout titleLayout; //title总布局
/*左侧部分*/
private LinearLayout ll_left; //左侧容器
private LinearLayout ll_back;
// private View view_left;
private ImageView iv_left_back, iv_left_icon; //返回键、图标
private TextView tv_left_text; //左侧文字
/*中间*/
private RelativeLayout rl_center; //中间容器
private TextView tv_center_text; //中间文字
/*右侧*/
private LinearLayout ll_right; //右侧容器
private RelativeLayout rl_right_icon; //右侧图标(可带气泡)
private TextView tv_right_text, tv_right_pop; //右侧文字
private ImageView iv_right_icon; //菜单图标、气泡
private LinearLayout ll_right_icon_content; //其他菜单容器
/**
* 属性值
*/
private String textLeft, textcenter, textRight, numRightPop; //文字
private int iconBack, iconLeft, iconCenterRow, iconRight; //图标
private int[] rightIconArr; //其他菜单容器
private float titleHeight; //文字高度
private float textSize; //文字大小
private int textColor; //文字颜色
private float textIconSpace; //item之间的间距
private float leftSpace; //标题左侧的空隙
private float rightSpace; //标题右侧的空隙
private float centerRowSpace; //中间箭头和文字的距离
private int background;
private float leftUsed;
private float rightUsed;
public TitleLayout(Context context) {
this(context, null);
}
public TitleLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TitleLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr);
}
private void init(Context context, AttributeSet attrs, int defStyleAttr) {
LayoutInflater.from(context).inflate(R.layout.title_layout, this, true);
view_tran_bar = findViewById(R.id.view_tran_bar);
//设置状态栏占位高度
setStatusBarHeight(getStatusBarHeight(getContext()));
//title总布局
titleLayout = (RelativeLayout) findViewById(R.id.titleLayout);
/*左侧部分*/
ll_left = (LinearLayout) findViewById(R.id.ll_left);
ll_back = (LinearLayout) findViewById(R.id.ll_back);
// view_left = findViewById(R.id.view_left);
iv_left_back = (ImageView) findViewById(R.id.iv_left_back);
iv_left_icon = (ImageView) findViewById(R.id.iv_left_icon);
tv_left_text = (TextView) findViewById(R.id.tv_left_text);
/*中间*/
rl_center = (RelativeLayout) findViewById(R.id.rl_center);
tv_center_text = (TextView) findViewById(R.id.tv_center_text);
/*右侧*/
ll_right = (LinearLayout) findViewById(R.id.ll_right);
rl_right_icon = (RelativeLayout) findViewById(R.id.rl_right_icon);
tv_right_text = (TextView) findViewById(R.id.tv_right_text);
iv_right_icon = (ImageView) findViewById(R.id.iv_right_icon);
tv_right_pop = (TextView) findViewById(R.id.tv_right_pop);
ll_right_icon_content = (LinearLayout) findViewById(R.id.ll_right_icon_content);
if(attrs!=null){
//获取自定义属性的值
TypedArray ta = context.getTheme().obtainStyledAttributes(attrs, R.styleable.TitleView, defStyleAttr, 0);
textLeft = ta.getString(R.styleable.TitleView_textLeft);
textcenter = ta.getString(R.styleable.TitleView_textcenter);
textRight = ta.getString(R.styleable.TitleView_textRight);
iconBack = ta.getResourceId(R.styleable.TitleView_iconBack, 0);
iconLeft = ta.getResourceId(R.styleable.TitleView_iconLeft, 0);
iconCenterRow = ta.getResourceId(R.styleable.TitleView_iconCenterRow, 0);
iconRight = ta.getResourceId(R.styleable.TitleView_iconRight, 0);
textSize = ta.getDimension(R.styleable.TitleView_android_textSize, 1);
titleHeight = ta.getDimension(R.styleable.TitleView_titleHeight, 0);
textColor = ta.getColor(R.styleable.TitleView_android_textColor, Color.WHITE);
textIconSpace = ta.getDimension(R.styleable.TitleView_textIconSpace, 0);
leftSpace = ta.getDimension(R.styleable.TitleView_leftSpace, 0);
rightSpace = ta.getDimension(R.styleable.TitleView_rightSpace, 0);
centerRowSpace = ta.getDimension(R.styleable.TitleView_centerRowSpace, 0);
/*
Drawable backgroundDrawable = getBackground();
LogUtil.i(TAG, "backgroundDrawable="+backgroundDrawable);
int defTitleColor = getResources().getColor(R.color.title_bar_color);
//background包括color和Drawable,这里分开取值
if(null == backgroundDrawable){
}else if(backgroundDrawable instanceof ColorDrawable){
background = ta.getColor(R.styleable.TitleView_android_background, defTitleColor);
LogUtil.i(TAG, "设置title背景色"+background);
setBackgroundColor(background);
}else{
background = ta.getResourceId(R.styleable.TitleView_android_background, -1);
LogUtil.i(TAG, "设置title背景资源图片"+background);
setBackgroundResource(background);
}
LogUtil.i(TAG, "background = "+background);*/
// LogUtil.i(TAG, "textLeft = "+textLeft);
// LogUtil.i(TAG, "textcenter = "+textcenter);
// LogUtil.i(TAG, "textRight = "+textRight);
// LogUtil.i(TAG, "iconBack = " + iconBack);
// LogUtil.i(TAG, "iconLeft = "+iconLeft);
// LogUtil.i(TAG, "iconCenterRow = "+iconCenterRow);
// LogUtil.i(TAG, "iconRight = "+iconRight);
// LogUtil.i(TAG, "iconRight = "+iconRight);
// LogUtil.i(TAG, "textSize = "+textSize);
// LogUtil.i(TAG, "titleHeight = "+titleHeight);
// LogUtil.i(TAG, "textColor = "+textColor);
// LogUtil.i(TAG, "textIconSpace = "+textIconSpace);
// LogUtil.i(TAG, "leftSpace = " + leftSpace);
// LogUtil.i(TAG, "rightSpace = "+rightSpace);
// LogUtil.i(TAG, "centerRowSpace = "+centerRowSpace);
ta.recycle();
}
show();
}
/**
* 显示,当设置属性值后,调用此方法
*/
public void show() {
setParams();
setGone();
setVisibleAndLayout();
}
private void setParams() {
tv_left_text.setTextColor(textColor);
tv_left_text.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
tv_center_text.setTextColor(textColor);
tv_center_text.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
tv_right_text.setTextColor(textColor);
tv_right_text.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
tv_right_pop.setTextColor(textColor);
// LogUtil.d(TAG, "设置字体");
//设置标题高度
ViewGroup.LayoutParams params = titleLayout.getLayoutParams();
if (params == null)
return;
params.height = (int) titleHeight;
titleLayout.setLayoutParams(params);
/*设置左边距*/
/*设置左边距*/
/* RelativeLayout.LayoutParams rl_params = (RelativeLayout.LayoutParams) ll_left.getLayoutParams();
rl_params.leftMargin = (int)leftSpace;
ll_left.setLayoutParams(rl_params);*/
LinearLayout.LayoutParams ll_params = (LinearLayout.LayoutParams) iv_left_back.getLayoutParams();
ll_params.leftMargin = (int) leftSpace;
ll_params.rightMargin = (int) leftSpace;
iv_left_back.setLayoutParams(ll_params);
// iv_left_back.setPadding((int)leftSpace, 0, 0, 0);
// ll_back.setPadding((int)leftSpace, ll_back.getPaddingTop(), ll_back.getPaddingRight(), ll_back.get);
/*设置右边距*/
LayoutParams rl_params1 = (LayoutParams) ll_right.getLayoutParams();
rl_params1.rightMargin = (int) rightSpace;
ll_right.setLayoutParams(rl_params1);
leftUsed = leftSpace;
rightUsed = rightSpace;
}
public TitleLayout setStatusBarHeight(int height) {
if(view_tran_bar!=null){
LinearLayout.LayoutParams barparams = (LinearLayout.LayoutParams) view_tran_bar.getLayoutParams();
barparams.height = height;
view_tran_bar.setLayoutParams(barparams);
}
return this;
}
public static int getStatusBarHeight(Context context){
int barHeight = 0;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
/**由于app的标题栏背景是图片而不是纯色,所以version>4.4的版本都显示在状态栏之上,需要添加一个状态栏高度的占位控件*/
// LogUtil.i(TAG, "版本:"+Build.VERSION.SDK_INT+" >= "+Build.VERSION_CODES.KITKAT+",需要显示状态栏占位");
int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
barHeight = context.getResources().getDimensionPixelSize(resourceId);
}
}
return barHeight;
}
private void setGone() {
tv_left_text.setVisibility(View.GONE);
ll_back.setVisibility(View.GONE);
iv_left_icon.setVisibility(View.GONE);
ll_left.setVisibility(View.GONE);
tv_center_text.setVisibility(View.GONE);
rl_center.setVisibility(View.GONE);
tv_right_text.setVisibility(View.GONE);
iv_right_icon.setVisibility(View.GONE);
tv_right_pop.setVisibility(View.GONE);
rl_right_icon.setVisibility(View.GONE);
ll_right_icon_content.setVisibility(View.GONE);
ll_right.setVisibility(View.GONE);
}
public OnMenuClickListener getOnMenuClickListener() {
return listener;
}
/**
* 控制item显示
*/
private void setVisibleAndLayout() {
// LogUtil.d(TAG, "设置可见");
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
if (!TextUtils.isEmpty(textLeft) || iconBack != 0 || iconLeft != 0) {
// LogUtil.d(TAG, "左侧可见");
ll_left.setVisibility(View.VISIBLE);
if (!TextUtils.isEmpty(textLeft)) {
tv_left_text.setVisibility(View.VISIBLE);
tv_left_text.setText(textLeft);
tv_left_text.measure(0, 0);
// LogUtil.d(TAG, "左侧文字可见"+ tv_left_text.getMeasuredWidth());
// LogUtil.d(TAG, "左侧文字可见"+ FontUtil.getFontlength(textPaint, textLeft));
leftUsed += tv_left_text.getMeasuredWidth();
tv_left_text.setOnClickListener(v -> {
if (listener != null) {
listener.onClick(MENU_NAME.MENU_LEFT_TEXT, tv_left_text);
}
});
}
if (iconBack != 0) {
ll_back.setVisibility(View.VISIBLE);
iv_left_back.setImageResource(iconBack);
// iv_left_back.setPadding(20, 20, 20, 20);
BitmapFactory.decodeResource(getResources(), iconBack, options);
int width = options.outWidth;
// LogUtil.d(TAG, "左侧返回可见"+width);
leftUsed += width;
ll_back.setOnClickListener(v -> {
((Activity) getContext()).onBackPressed();
});
}
if (iconLeft != 0) {
iv_left_icon.setVisibility(View.VISIBLE);
iv_left_icon.setImageResource(iconLeft);
BitmapFactory.decodeResource(getResources(), iconLeft, options);
int width = options.outWidth;
// LogUtil.d(TAG, "左侧图标可见"+width);
leftUsed += width;
/*iv_left_icon.setOnClickListener(v->{
if(listener!=null)
listener.onClick(MENU_NAME.MENU_LEFT_ICON);
});*/
}
if (!TextUtils.isEmpty(textLeft) && (iconLeft != 0 || iconBack != 0)) {
LinearLayout.LayoutParams ll_params = (LinearLayout.LayoutParams) tv_left_text.getLayoutParams();
ll_params.leftMargin = (int) textIconSpace;
leftUsed += textIconSpace;
tv_left_text.setLayoutParams(ll_params);
}
if (iconBack == 0 && (!TextUtils.isEmpty(textLeft) || iconLeft != 0)) {
//返回键不显示时,流出左边空隙
LayoutParams rl_params = (LayoutParams) ll_left.getLayoutParams();
rl_params.leftMargin = (int) leftSpace;
ll_left.setLayoutParams(rl_params);
}
}
if (!TextUtils.isEmpty(textcenter) || iconCenterRow != 0) {
rl_center.setVisibility(View.VISIBLE);
// LogUtil.d(TAG, "中间可见");
if (!TextUtils.isEmpty(textcenter)) {
// LogUtil.d(TAG, "中间文字可见:" + textcenter);
tv_center_text.setVisibility(View.VISIBLE);
tv_center_text.setText(textcenter);
if (iconCenterRow != 0) {
// LogUtil.d(TAG, "中间箭头可见");
Drawable iconCenterDraw = getResources().getDrawable(iconCenterRow);
iconCenterDraw.setBounds(0, 0, iconCenterDraw.getMinimumWidth(), iconCenterDraw.getMinimumHeight());
tv_center_text.setCompoundDrawables(null, null, iconCenterDraw, null);
tv_center_text.setCompoundDrawablePadding((int) centerRowSpace);
rl_center.setOnClickListener(v -> {
if (listener != null)
listener.onClick(MENU_NAME.MENU_CENTER, rl_center);
});
}
}
}
if (!TextUtils.isEmpty(textRight) || iconRight != 0 || !TextUtils.isEmpty(numRightPop)) {
ll_right.setVisibility(View.VISIBLE);
// LogUtil.d(TAG, "右侧可见");
if (!TextUtils.isEmpty(textRight)) {
tv_right_text.setVisibility(View.VISIBLE);
tv_right_text.setText(textRight);
tv_right_text.measure(0, 0);
// LogUtil.d(TAG, "右侧文字可见"+tv_left_text.getMeasuredWidth());
// LogUtil.d(TAG, "右侧文字可见"+FontUtil.getFontlength(textPaint, textRight));
rightUsed += tv_left_text.getMeasuredWidth();
tv_right_text.setOnClickListener(v -> {
if (listener != null) {
listener.onClick(MENU_NAME.MENU_RIGHT_TEXT, tv_right_text);
}
});
}
if (iconRight != 0) {
rl_right_icon.setVisibility(View.VISIBLE);
iv_right_icon.setVisibility(View.VISIBLE);
iv_right_icon.setImageResource(iconRight);
rl_right_icon.setOnClickListener(v -> {
if (listener != null) {
listener.onClick(MENU_NAME.MENU_RIGHT_ICON, rl_right_icon);
}
});
// LogUtil.d(TAG, "右侧图标可见");
if (!TextUtils.isEmpty(numRightPop)) {
tv_right_pop.setVisibility(View.VISIBLE);
tv_right_pop.setText(numRightPop + "");
}
}
/*设置右侧menu图标和气泡的间距*/
BitmapFactory.decodeResource(getResources(), iconRight, options);
int width = options.outWidth;
int height = options.outHeight;
// LogUtil.i(TAG, "右侧图标的宽高:"+width+" * "+height);
LinearLayout.LayoutParams ll_params = (LinearLayout.LayoutParams) rl_right_icon.getLayoutParams();
if (!TextUtils.isEmpty(numRightPop)) {
tv_right_pop.measure(0, 0);
// LogUtil.d(TAG, "气泡大小"+tv_right_pop.getMeasuredWidth());
int widthPop = tv_right_pop.getMeasuredWidth();
int heightPop = tv_right_pop.getMeasuredHeight();
// LogUtil.i(TAG, "气泡的宽高:"+widthPop+" * "+heightPop);
ll_params.width = width + widthPop;
ll_params.height = height + heightPop;
} else {
ll_params.width = width;
ll_params.height = height;
}
rl_right_icon.setLayoutParams(ll_params);
rightUsed += ll_params.width;
if (!TextUtils.isEmpty(textRight) && iconRight != 0) {
ll_params = (LinearLayout.LayoutParams) rl_right_icon.getLayoutParams();
ll_params.leftMargin = (int) textIconSpace;
rl_right_icon.setLayoutParams(ll_params);
rightUsed += textIconSpace;
}
if(null!=rightIconArr && rightIconArr.length>0){
ll_right_icon_content.setVisibility(View.VISIBLE);
for(int id : rightIconArr){
LinearLayout.LayoutParams content_params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
content_params.leftMargin = DensityUtil.dip2px(getContext(), 10);
ImageView iv = new ImageView(getContext());
iv.setImageResource(id);
ll_right_icon_content.addView(iv ,content_params);
iv.setOnClickListener(v -> {
if (contentIconListener != null) {
contentIconListener.onClick(id, v);
}
});
}
}
}
if (!TextUtils.isEmpty(textcenter)) {
/*避免中间的内容超出范围*/
// LogUtil.e(TAG, "左边占用:" + leftUsed + " 右边占用:" + rightUsed);
float max = leftUsed > rightUsed ? leftUsed : rightUsed;
int widthRow = 0;
if (iconCenterRow != 0) {
BitmapFactory.decodeResource(getResources(), iconCenterRow, options);
widthRow = options.outWidth;
}
WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
int centerTextMaxWidth = wm.getDefaultDisplay().getWidth() -
(int) (2 * (max + textIconSpace)) - widthRow
- (int) (widthRow == 0 ? 0 : centerRowSpace + widthRow);
// LogUtil.e(TAG, " 剩余:" + centerTextMaxWidth);
tv_center_text.measure(0, 0);
// LogUtil.d(TAG, "中间标题长度:" + tv_center_text.getMeasuredWidth());
float centerTextWidth = tv_center_text.getMeasuredWidth() > centerTextMaxWidth ?
centerTextMaxWidth : tv_center_text.getMeasuredWidth();
LayoutParams rl_params = (LayoutParams) tv_center_text.getLayoutParams();
rl_params.width = (int) centerTextWidth;
tv_center_text.setLayoutParams(rl_params);
}
requestLayout();
}
/**
* 点击事件类型
*/
public enum MENU_NAME {
MENU_BACK,
// MENU_LEFT_ICON,
MENU_LEFT_TEXT,
MENU_CENTER,
MENU_RIGHT_ICON,
MENU_RIGHT_TEXT,
MENU_RIGHT_CONTENTICON
}
public TitleLayout setRightIconArr(int[] rightIconArr) {
this.rightIconArr = rightIconArr;
return this;
}
public void setLeftIconVisible(boolean showIcon){
if(showIcon){
ll_back.setVisibility(VISIBLE);
}else{
ll_back.setVisibility(GONE);
}
}
public interface OnMenuClickListener {
void onClick(MENU_NAME menu, View view);
}
public interface OnContentIconClickListener {
void onClick(int srcId, View view);
}
private OnMenuClickListener listener;
private OnContentIconClickListener contentIconListener;
public TitleLayout setOnMenuClickListener(OnMenuClickListener listener) {
this.listener = listener;
return this;
}
public TitleLayout setOnContentIconClickListener(OnContentIconClickListener listener) {
this.contentIconListener = listener;
return this;
}
public TitleLayout setBackgroundColor1(int Color) {
titleLayout.setBackgroundColor(Color);
view_tran_bar.setBackgroundColor(Color);
return this;
}
/**
* 设置内容
*/
public TitleLayout setTextLeft(String textLeft) {
this.textLeft = textLeft;
tv_left_text.setText(textLeft);
return this;
}
public TitleLayout setTextcenter(String textcenter) {
this.textcenter = textcenter;
tv_center_text.setText(textcenter);
return this;
}
public TitleLayout setTextRight(String textRight) {
this.textRight = textRight;
tv_right_text.setText(textRight);
return this;
}
public TitleLayout setIconBack(int iconBack) {
this.iconBack = iconBack;
iv_left_back.setImageResource(iconBack);
return this;
}
public TitleLayout setIconLeft(int iconLeft) {
this.iconLeft = iconLeft;
iv_left_icon.setImageResource(iconLeft);
return this;
}
public TitleLayout setIconCenterRow(int iconCenterRow) {
this.iconCenterRow = iconCenterRow;
return this;
}
public TitleLayout setIconRight(int iconRight) {
this.iconRight = iconRight;
iv_right_icon.setImageResource(iconRight);
return this;
}
public TitleLayout setNumRightPop(String numRightPop) {
this.numRightPop = numRightPop;
tv_right_pop.setText(numRightPop);
return this;
}
public TitleLayout setTitleHeight(int titleHeight) {
this.titleHeight = DensityUtil.dip2px(getContext(), titleHeight);
return this;
}
public TitleLayout setTextSize(int textSize) {
this.textSize = textSize;
return this;
}
public TitleLayout setTextColor(int textColor) {
this.textColor = textColor;
return this;
}
public TitleLayout setTextIconSpace(int textIconSpace) {
this.textIconSpace = DensityUtil.dip2px(getContext(), textIconSpace);
return this;
}
public TitleLayout setLeftSpace(int leftSpace) {
this.leftSpace = DensityUtil.dip2px(getContext(), leftSpace);
return this;
}
public TitleLayout setRightSpace(int rightSpace) {
this.rightSpace = DensityUtil.dip2px(getContext(), rightSpace);
return this;
}
public TitleLayout setCenterRowSpace(int centerRowSpace) {
this.centerRowSpace = DensityUtil.dip2px(getContext(), centerRowSpace);
return this;
}
public String getTextLeft() {
return textLeft;
}
public String getTextcenter() {
return textcenter;
}
public String getTextRight() {
return textRight;
}
}
@@ -0,0 +1,358 @@
package com.openxu.cview.chart;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import com.openxu.cview.BuildConfig;
import com.openxu.cview.R;
import com.openxu.cview.chart.anim.AngleEvaluator;
import com.openxu.utils.FontUtil;
import com.openxu.utils.LogUtil;
import com.openxu.utils.SharedData;
/**
* autour : openXu
* date : 2017/7/24 10:46
* className : BaseChart
* version : 1.0
* description : 图表基类
*/
public abstract class BaseChart extends View {
protected String TAG = "BaseChart";
protected int ScrWidth,ScrHeight; //屏幕宽高
protected RectF rectChart; //图表矩形
protected PointF centerPoint; //chart中心点坐标
protected int total; //总数量
/**可设置属性*/
protected int startAngle = -90; //开始的角度
protected int backColor = Color.WHITE;
protected int lineWidth = 1; //辅助线宽度
protected String loadingStr = "loading...";
protected int defColor = Color.rgb(220, 220, 220); //底色
protected Paint paint;
protected Paint paintEffect;
protected Paint paintLabel;
/**动画相关统一属性,也可以设置,需要写set方法*/
protected long animDuration = 1000;
protected ValueAnimator anim;
protected boolean startDraw = false;
/**正在加载*/
protected boolean isLoading = true;
protected boolean drawLine = true;
protected boolean drawBottomLine = true;
protected boolean debug = SharedData.getInstance().getSp().getBoolean(SharedData.KEY_DEBUG, false);
/**手指抬起后,动画*/
protected ValueAnimator touchAnim;
protected GestureDetector mGestureDetector;
public void setLoading(boolean loading) {
isLoading = loading;
invalidate();
}
public void setLineWidth(int lineWidth) {
this.lineWidth = lineWidth;
}
public BaseChart(Context context) {
this(context, null);
}
public BaseChart(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public BaseChart(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
init(context, attrs, defStyle);
}
public void setBackColor(int backColor) {
this.backColor = backColor;
}
public void init() {
TAG = getClass().getSimpleName();
DisplayMetrics dm = getResources().getDisplayMetrics();
ScrHeight = dm.heightPixels;
ScrWidth = dm.widthPixels;
//画笔初始化
paint = new Paint();
paint.setAntiAlias(true);
paintLabel = new Paint();
paintLabel.setAntiAlias(true);
paintEffect = new Paint();
paintEffect.setAntiAlias(true);
paintEffect.setStyle(Paint.Style.FILL);
paintEffect.setStrokeWidth(lineWidth);
paintEffect.setColor(Color.RED);
mGestureDetector = new GestureDetector(getContext(), new MyOnGestureListener());
}
public abstract void init(Context context, AttributeSet attrs, int defStyleAttr);
public int getTotal() {
return total;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
centerPoint = new PointF(getMeasuredWidth()/2, getMeasuredHeight()/2);
rectChart = new RectF(getPaddingLeft(),getPaddingTop(),getMeasuredWidth()-getPaddingRight(),
getMeasuredHeight()-getPaddingBottom());
}
protected TOUCH_EVENT_TYPE touchEventType = TOUCH_EVENT_TYPE.EVENT_NULL;
/**需要拦截的事件方向*/
public enum TOUCH_EVENT_TYPE{
EVENT_NULL, /*不处理事件*/
EVENT_X, /*拦截X轴方向的事件*/
EVENT_Y, /*拦截Y轴方向的事件*/
EVENT_XY /*拦截XY轴方向的事件*/
}
/**设置事件方向*/
public void setTouchEventType(TOUCH_EVENT_TYPE touchEventType) {
this.touchEventType = touchEventType;
}
protected boolean touchEnable = false; //是否超界,控件的大小是否足以显示内容,是否需要滑动来展示。子控件根据计算赋值
protected float mDownX, mDownY;
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
LogUtil.d(TAG, "dispatchTouchEvent "+touchEventType);
if(!touchEnable){
LogUtil.w(TAG, "没超界");
}else if(TOUCH_EVENT_TYPE.EVENT_NULL == touchEventType){
LogUtil.w(TAG, "不需要处理事件");
}else if(TOUCH_EVENT_TYPE.EVENT_XY == touchEventType){
LogUtil.w(TAG, "需要拦截XY方向的事件");
getParent().requestDisallowInterceptTouchEvent(true);
}else{
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownX = event.getX();
mDownY = event.getY();
if(null!=touchAnim && touchAnim.isRunning())
touchAnim.cancel();
getParent().requestDisallowInterceptTouchEvent(true);//ACTION_DOWN的时候,赶紧把事件hold住
break;
case MotionEvent.ACTION_MOVE:
if(TOUCH_EVENT_TYPE.EVENT_X == touchEventType){
if(Math.abs(event.getY()-mDownY)> Math.abs(event.getX() - mDownX)) {
getParent().requestDisallowInterceptTouchEvent(false);
LogUtil.i(TAG, "竖直滑动的距离大于水平的时候,将事件还给父控件");
}else {
getParent().requestDisallowInterceptTouchEvent(true);
LogUtil.i(TAG, "正常请求事件");
dispatchTouchEvent1(event);
}
}else if(TOUCH_EVENT_TYPE.EVENT_Y == touchEventType){
if(Math.abs(event.getX() - mDownX)> Math.abs(event.getY()-mDownY)) {
getParent().requestDisallowInterceptTouchEvent(false);
LogUtil.i(TAG, "水平滑动的距离大于竖直的时候,将事件还给父控件");
}else {
getParent().requestDisallowInterceptTouchEvent(true);
dispatchTouchEvent1(event);
LogUtil.i(TAG, "正常请求事件");
}
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
break;
}
}
return super.dispatchTouchEvent(event);
}
protected void dispatchTouchEvent1(MotionEvent event){
}
protected PointF lastTouchPoint;
@Override
public boolean onTouchEvent(MotionEvent event) {
if(!touchEnable)
return false;
boolean result = mGestureDetector.onTouchEvent(event);
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
lastTouchPoint = new PointF(event.getX(), event.getY());
onTouchMoved(lastTouchPoint);
return true;
case MotionEvent.ACTION_MOVE:
int move = 0;
if(TOUCH_EVENT_TYPE.EVENT_X == touchEventType){
move = (int)(event.getX() - lastTouchPoint.x);
}else if(TOUCH_EVENT_TYPE.EVENT_Y == touchEventType){
move = (int)(event.getY() - lastTouchPoint.y);
}
LogUtil.i(TAG, "MotionEvent.ACTION_MOVE"+move);
lastTouchPoint.x = (int)event.getX();
lastTouchPoint.y = (int)event.getY();
onTouchMoved(lastTouchPoint);
evaluatorFling(move);
invalidate();
return true;
case MotionEvent.ACTION_UP:
lastTouchPoint.x = 0;
lastTouchPoint.y = 0;
onTouchMoved(null);
return true;
}
return result;
}
protected void onTouchMoved(PointF point){
}
public void onDraw(Canvas canvas){
//画布背景
// canvas.drawColor(backColor);
if(debug) {
drawDebug(canvas);
}
drawDefult(canvas);
if(isLoading){
drawLoading(canvas);
return;
}
// drawChart(canvas);
if(!startDraw){
startDraw = true;
startAnimation(canvas);
}else{
drawChart(canvas);
}
}
public void drawLoading(Canvas canvas) {
paintLabel.setTextSize(35);
float NullTextLead = FontUtil.getFontLeading(paintLabel);
float NullTextHeight = FontUtil.getFontHeight(paintLabel);
float textY = centerPoint.y-NullTextHeight/2+NullTextLead;
paintLabel.setColor(getContext().getResources().getColor(R.color.text_color_def));
canvas.drawText(loadingStr, centerPoint.x- FontUtil.getFontlength(paintLabel, loadingStr)/2, textY, paintLabel);
}
/**绘制图表基本框架*/
public abstract void drawDefult(Canvas canvas);
/**绘制debug辅助线*/
public void drawDebug(Canvas canvas){
paint.setStyle(Paint.Style.STROKE);//设置空心
paint.setStrokeWidth(lineWidth);
//绘制边界--chart区域
paint.setColor(Color.BLUE);
RectF r = new RectF(0,0,getMeasuredWidth(), getMeasuredHeight());
canvas.drawRect(r, paint);
paint.setColor(Color.RED);
canvas.drawRect(rectChart, paint);
}
/**绘制图表*/
public abstract void drawChart(Canvas canvas);
/**创建动画*/
protected abstract ValueAnimator initAnim();
/**动画值变化之后计算数据*/
protected abstract void evaluatorData(ValueAnimator animation);
public void setAnimDuration(long duration){
this.animDuration = duration;
}
protected void startAnimation(Canvas canvas) {
if(anim!=null){
anim.cancel();
}
LogUtil.w(TAG, "开始绘制动画");
anim = initAnim();
if(anim==null){
drawChart(canvas);
}else{
anim.setInterpolator(new AccelerateDecelerateInterpolator());
anim.addUpdateListener((ValueAnimator animation)->{
evaluatorData(animation);
invalidate();
});
anim.setDuration(animDuration);
anim.start();
}
}
/**动画值变化之后计算数据*/
protected void evaluatorFling(float velocityAnim){}
protected int mMoveLen = 0; //滚动距离
protected boolean moveOver = false; //是否滚动到头了
/**手指松开后滑动动画效果*/
protected void startFlingAnimation(float velocity) {
if(touchAnim!=null){
touchAnim.cancel();
}
touchAnim = ValueAnimator.ofObject(new AngleEvaluator(), 1f, 0f);
touchAnim.setInterpolator(new DecelerateInterpolator()); //越来越慢
touchAnim.addUpdateListener((ValueAnimator animation)->{
float velocityAnim = (float)animation.getAnimatedValue();
evaluatorFling(velocity /100 * velocityAnim);
invalidate();
});
touchAnim.setDuration(1000+(int)+Math.abs(velocity)/4);
touchAnim.start();
}
class MyOnGestureListener extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
LogUtil.e(TAG,"onFling------------>velocityX="+velocityX+" velocityY="+velocityY);
if(TOUCH_EVENT_TYPE.EVENT_X == touchEventType){
startFlingAnimation(velocityX);
}else if(TOUCH_EVENT_TYPE.EVENT_Y == touchEventType){
startFlingAnimation(velocityY);
}
return false;
}
}
}
@@ -0,0 +1,164 @@
package com.openxu.cview.chart;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.animation.AccelerateDecelerateInterpolator;
import com.openxu.cview.chart.anim.AngleEvaluator;
import com.openxu.cview.chart.bean.ChartLable;
import com.openxu.utils.DensityUtil;
import com.openxu.utils.FontUtil;
import java.util.List;
/**
* autour : openXu
* date : 2017/7/24 10:46
* className : ProgressPieChart
* version : 1.0
* description : 进度图表
*/
public class ProgressPieChart extends BaseChart {
private int total; //总数量
private int progress; //占比数量
private float animPro; //动画计算的占比数量
private List<ChartLable> lableList; //需要绘制的lable集合
private int textSpace = DensityUtil.dip2px(getContext(), 3); //文字间距
private int raidus; //圆环半径
private int proSize = DensityUtil.dip2px(getContext(), 8); //圈宽度
private int proColor = Color.rgb(0, 255, 0); //进度环颜色
public ProgressPieChart(Context context) {
super(context, null);
}
public ProgressPieChart(Context context, AttributeSet attrs) {
super(context, attrs, 0);
}
public ProgressPieChart(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public void init(Context context, AttributeSet attrs, int defStyleAttr) {
startAngle = -90; //开始的角度
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getMeasuredWidth();
int height = getMeasuredHeight();
if(width<height){
rectChart = new RectF(0,(height-width)/2,width, (height+width)/2);
}else{
rectChart = new RectF((width-height)/2, 0, (width+height)/2, height);
}
raidus = (width<height?width:height)/2;
}
public void setProSize(int proSize) {
this.proSize = proSize;
}
public void setDefColor(int defColor) {
this.defColor = defColor;
}
public void setProColor(int proColor) {
this.proColor = proColor;
}
public void setData(int total, int progress, List<ChartLable> lableList){
this.total = total;
this.progress = progress;
this.lableList = lableList;
startDraw = false;
invalidate();
}
/**绘制图表基本框架*/
@Override
public void drawDefult(Canvas canvas) {
paint.setStyle(Paint.Style.STROKE);//设置空心
paint.setStrokeWidth(proSize);
paint.setColor(defColor);
canvas.drawCircle(centerPoint.x,centerPoint.y,raidus-proSize/2, paint);
}
/**绘制图表*/
@Override
public void drawChart(Canvas canvas) {
if(progress>0) {
paint.setStrokeWidth(proSize);
paint.setColor(proColor);
//1绘制开始圆点
// paint.setStyle(Paint.Style.FILL);
// canvas.drawCircle(centerPoint.x, centerPoint.y - raidus + proSize / 2, proSize / 2, paint);
//2绘制进度圆弧
paint.setStyle(Paint.Style.STROKE);//设置空心
int r = raidus - proSize / 2;
float arcLeft = centerPoint.x - r;
float arcTop = centerPoint.y - r;
float arcRight = centerPoint.x + r;
float arcBottom = centerPoint.y + r;
canvas.drawArc(new RectF(arcLeft, arcTop, arcRight, arcBottom), startAngle, animPro, false, paint);
//3绘制结束圆点
// paint.setStyle(Paint.Style.FILL);
// float endX = centerPoint.x + (float) (r * Math.cos(Math.toRadians(startAngle + animPro)));
// float endY = centerPoint.y + (float) (r * Math.sin(Math.toRadians(startAngle + animPro)));
// canvas.drawCircle(endX, endY, proSize / 2, paint);
}
//4绘制文字
if(lableList!=null &&lableList.size()>0){
float textAllHeight = 0;
float textW, textH, textL;
for(ChartLable lable : lableList){
paintLabel.setTextSize(lable.getTextSize());
textH = FontUtil.getFontHeight(paintLabel);
textAllHeight += (textH+textSpace);
}
textAllHeight -= textSpace;
int top = (int)(centerPoint.y-textAllHeight/2);
for(ChartLable lable : lableList){
paintLabel.setColor(lable.getTextColor());
paintLabel.setTextSize(lable.getTextSize());
textW = FontUtil.getFontlength(paintLabel, lable.getText());
textH = FontUtil.getFontHeight(paintLabel);
textL = FontUtil.getFontLeading(paintLabel);
canvas.drawText(lable.getText(), centerPoint.x-textW/2, top + textL, paintLabel);
top += (textH+textSpace);
}
}
}
/**创建动画*/
@Override
protected ValueAnimator initAnim() {
if(progress>0) {
float angle = (float) progress / (float) total * 360;
ValueAnimator anim = ValueAnimator.ofObject(new AngleEvaluator(), 0f, angle);
anim.setInterpolator(new AccelerateDecelerateInterpolator());
return anim;
}
return null;
}
/**动画值变化之后计算数据*/
@Override
protected void evaluatorData(ValueAnimator animation) {
animPro = (float)animation.getAnimatedValue();
}
}
@@ -0,0 +1,22 @@
package com.openxu.cview.chart.anim;
import android.animation.TypeEvaluator;
/**
* autour : openXu
* date : 2017/7/28 9:12
* className : AngleEvaluator
* version : 1.0
* description : 角度计算器
*/
public class AngleEvaluator implements TypeEvaluator {
@Override
public Object evaluate(float fraction, Object startValue, Object endValue) {
float start = (float)startValue;
float end = (float)endValue;
return start+fraction*(end-start);
}
}
@@ -0,0 +1,383 @@
package com.openxu.cview.chart.barchart;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import com.openxu.cview.R;
import com.openxu.cview.chart.BaseChart;
import com.openxu.cview.chart.anim.AngleEvaluator;
import com.openxu.cview.chart.bean.BarBean;
import com.openxu.utils.DensityUtil;
import com.openxu.utils.FontUtil;
import com.openxu.utils.LogUtil;
import java.util.ArrayList;
import java.util.List;
import static com.openxu.cview.chart.BaseChart.TOUCH_EVENT_TYPE.EVENT_Y;
/**
* autour : openXu
* date : 2017/7/24 10:46
* className : BarHorizontalChart
* version : 1.0
* description : 横向(柱子水平) 柱状图,支持多柱
*/
public class BarHorizontalChart extends BaseChart {
private List<List<BarBean>> dataList;
private List<String> strList;
/**可以设置的属性*/
private int barNum = 2; //柱子数量
private int XMARK_NUM = 5; //X轴刻度数量
private int barWidth = DensityUtil.dip2px(getContext(), 15); //柱宽度
private int barSpace = DensityUtil.dip2px(getContext(), 1); //双柱间的间距
private int barItemSpace = DensityUtil.dip2px(getContext(), 25);//一组柱之间的间距
private int[] barColor = new int[]{Color.BLUE, Color.YELLOW}; //柱颜色
private int textSizeCoordinate = (int)getResources().getDimension(R.dimen.text_size_level_small); //坐标文字大小
private int textColorCoordinate = getResources().getColor(R.color.text_color_light_gray);
private int textSizeTag = (int)getResources().getDimension(R.dimen.text_size_level_small); //数值字体
private int textColorTag = getResources().getColor(R.color.text_color_light_gray);
private int textSpace = DensityUtil.dip2px(getContext(), 3); //默认的字与其他的间距
/**需要计算相关值*/
private int oneBarW; //单个宽度,需要计算
private int YMARK_MAX_WIDTH = DensityUtil.dip2px(getContext(), 30); //Y轴刻度最大值的宽度
private int XMARK_H, XMARK_ALL_H; //Y轴刻度间距值
private int topStartPointY; //从左侧开始绘制的X坐标
private int minTopPointY; //滑动到最右侧时的X坐标
private PointF zeroPoint = new PointF(); //柱状图图体圆点坐标
/*字体绘制相关*/
private int XMARK = 1; //X刻度间隔
private int XMARK_MAX = 1; //X轴刻度最大值(根据设置的数据自动赋值)
private int heightCoordinate;
private int leadCoordinate;
private int heightTag;
private int leadTag;
public BarHorizontalChart(Context context) {
this(context, null);
}
public BarHorizontalChart(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public BarHorizontalChart(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public void init(Context context, AttributeSet attrs, int defStyleAttr) {
touchEventType = EVENT_Y;
dataList = new ArrayList<>();
strList = new ArrayList<>();
}
/***********************************设置属性set方法**********************************/
public void setBarNum(int barNum) {
this.barNum = barNum;
}
public void setXMARK_NUM(int XMARK_NUM) {
this.XMARK_NUM = XMARK_NUM;
}
public void setBarWidth(int barWidth) {
this.barWidth = barWidth;
}
public void setBarSpace(int barSpace) {
this.barSpace = barSpace;
}
public void setBarItemSpace(int barItemSpace) {
this.barItemSpace = barItemSpace;
}
public void setBarColor(int[] barColor) {
this.barColor = barColor;
}
public void setTextSizeCoordinate(int textSizeCoordinate) {
this.textSizeCoordinate = textSizeCoordinate;
}
public void setTextColorCoordinate(int textColorCoordinate) {
this.textColorCoordinate = textColorCoordinate;
}
public void setTextSizeTag(int textSizeTag) {
this.textSizeTag = textSizeTag;
}
public void setTextColorTag(int textColorTag) {
this.textColorTag = textColorTag;
}
public void setTextSpace(int textSpace) {
this.textSpace = textSpace;
}
/***********************************设置属性set方法over**********************************/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
evaluatorByData();
postInvalidate();
}
/***************************************************/
/**滑动*/
@Override
protected void evaluatorFling(float fling) {
LogUtil.i(TAG, "fling = "+fling);
//已经滚动到顶部或者底部
if(topStartPointY + mMoveLen + fling <= minTopPointY){
mMoveLen = minTopPointY-topStartPointY;
moveOver = true;
if(null!=touchAnim && touchAnim.isRunning())
touchAnim.cancel();
}else if(topStartPointY + mMoveLen+ fling>=topStartPointY){
mMoveLen = 0;
moveOver = true;
if(null!=touchAnim && touchAnim.isRunning())
touchAnim.cancel();
}else{
moveOver = false;
mMoveLen += fling;
}
}
/***************************************************/
/**
* 设置数据
* @param dataList 柱状图数据
* @param strYList Y轴坐标数据
*/
public void setData(List<List<BarBean>> dataList, List<String> strYList){
this.strList.clear();
this.dataList.clear();
if(dataList!=null)
this.dataList.addAll(dataList);
if(strYList!=null)
this.strList.addAll(strYList);
if(rectChart!=null) {
evaluatorByData();
startDraw = false;
invalidate();
}
}
/**设置数据后,计算相关值*/
private void evaluatorByData(){
total = 0;
//三种字体计算
paintLabel.setTextSize(textSizeCoordinate);
heightCoordinate = (int) FontUtil.getFontHeight(paintLabel);
leadCoordinate = (int)FontUtil.getFontLeading(paintLabel);
paintLabel.setTextSize(textSizeTag);
heightTag = (int)FontUtil.getFontHeight(paintLabel);
leadTag = (int)FontUtil.getFontLeading(paintLabel);
//zeroPoint坐标
if(strList!=null && strList.size()>0){
YMARK_MAX_WIDTH = 0;
paintLabel.setTextSize(textSizeCoordinate);
for(String xStr : strList){
int tw = (int)FontUtil.getFontlength(paintLabel, xStr);
YMARK_MAX_WIDTH = tw>YMARK_MAX_WIDTH?tw:YMARK_MAX_WIDTH;
}
}
zeroPoint.x = rectChart.left + YMARK_MAX_WIDTH + textSpace;
zeroPoint.y = heightCoordinate + textSpace;
/*计算X刻度最大值*/
XMARK_MAX = 1;
for(List<BarBean> list : dataList){
for(BarBean bean : list){
total += bean.getNum();
XMARK_MAX = (int)bean.getNum()>XMARK_MAX?(int)bean.getNum():XMARK_MAX;
}
}
LogUtil.i(TAG, "真实XMARK_MAX="+XMARK_MAX);
if (XMARK_MAX<=5)
XMARK_MAX = 5;
XMARK = XMARK_MAX/XMARK_NUM + 1;
int MARK = (Integer.parseInt((XMARK+"").substring(0,1))+1);
if((XMARK+"").length()==1){
//XMARK = 1、2、5、10
XMARK = (XMARK==3||XMARK==4||XMARK==6||XMARK==7||XMARK==8||XMARK==9)?((XMARK==3||XMARK==4)?5:10):XMARK;
}else if((XMARK+"").length()==2){
XMARK = MARK*10;
}else if((XMARK+"").length()==3){
XMARK = MARK*100;
}else if((XMARK+"").length()==4){
XMARK = MARK*1000;
}else if((XMARK+"").length()==5){
XMARK = MARK*10000;
}else if((XMARK+"").length()==6){
XMARK = MARK*100000;
}
XMARK_MAX = XMARK*XMARK_NUM;
LogUtil.i(TAG, "计算XMARK_MAX="+XMARK_MAX+" XMARK="+XMARK);
//Y轴刻度间距值
XMARK_ALL_H = (int)(rectChart.right-zeroPoint.x);
XMARK_H = (int)((float)XMARK_ALL_H/XMARK_NUM);
//单份柱子(包含多个及其间距)宽度
oneBarW = (barWidth*barNum)+(barSpace*(barNum-1));
topStartPointY = (int)zeroPoint.y+barItemSpace/2; //从左侧开始绘制的X坐标
int allW = (oneBarW+barItemSpace)*dataList.size(); //每份柱子宽度+item间距(其中包含第一个item左边和最后一个item右边的半份间距)
int contentW = (int)(rectChart.bottom-zeroPoint.y);
touchEnable = allW>contentW;
if(touchEnable){
//超出总宽度了
minTopPointY = -allW+(int)(rectChart.bottom-topStartPointY-barItemSpace);
}
LogUtil.w(TAG, "柱状图表宽高:"+getMeasuredWidth()+"*"+getMeasuredHeight()+
" 图表范围"+rectChart+" 圆点坐标zeroPoint="+zeroPoint);
}
/**绘制图表基本框架*/
@Override
public void drawDefult(Canvas canvas) {
paint.setStyle(Paint.Style.FILL);
paint.setStrokeWidth(lineWidth);
paint.setColor(defColor);
canvas.drawLine(zeroPoint.x, zeroPoint.y, rectChart.right, zeroPoint.y, paint);
if(touchEnable){
//超出高度了
canvas.drawLine(zeroPoint.x, zeroPoint.y,zeroPoint.x , rectChart.bottom, paint);
}else{
canvas.drawLine(zeroPoint.x, zeroPoint.y,zeroPoint.x , zeroPoint.y+(oneBarW+barItemSpace)*dataList.size(), paint);
}
}
/**绘制debug辅助线*/
@Override
public void drawDebug(Canvas canvas) {
super.drawDebug(canvas);
}
/**绘制图表*/
@Override
public void drawChart(Canvas canvas) {
int topStart = topStartPointY + mMoveLen;
// LogUtil.i(TAG, "leftStart:"+leftStart+" zezeroPoint"+zeroPoint+" barItemSpace="+barItemSpace+" barWidth="+barWidth +" oneBarW="+oneBarW);
paint.setStyle(Paint.Style.FILL);
LogUtil.w(TAG, "");
for(int i = 0; i<dataList.size(); i++){
String str = strList.get(i);
List<BarBean> list = dataList.get(i);
//柱子开始绘制的Y坐标
int topY = topStart +(oneBarW+barItemSpace)*i;
//绘制X刻度
paintLabel.setTextSize(textSizeCoordinate);
paintLabel.setColor(textColorCoordinate);
float tw = FontUtil.getFontlength(paintLabel, str);
canvas.drawText(str, zeroPoint.x-textSpace-tw, topY + oneBarW/2-heightCoordinate/2+leadCoordinate,paintLabel);
//绘制辅助线
paint.setStrokeWidth(0.5f);
paint.setColor(defColor);
canvas.drawLine(zeroPoint.x, topY-15, rectChart.right, topY-15, paint);
paintLabel.setTextSize(textSizeTag);
paintLabel.setColor(textColorTag);
for(int j = 0 ;j <list.size(); j++){
BarBean bean = list.get(j);
//绘制辅助线
paint.setStrokeWidth(0.5f);
paint.setColor(defColor);
canvas.drawLine(zeroPoint.x, topY+barWidth/2, rectChart.right, topY+barWidth/2, paint);
//绘制柱子
paint.setColor(barColor[j]);
float right = (zeroPoint.x+XMARK_ALL_H*(bean.getNum()/XMARK_MAX)*animPro);
RectF br = new RectF(zeroPoint.x , topY , right, topY+barWidth);
canvas.drawRect(br, paint);
//绘制柱子占比
str = (int)bean.getNum()+"";
canvas.drawText(str, right+textSpace, topY +barWidth/2 -heightTag/2+ leadTag,paintLabel);
// LogUtil.d(TAG, "绘制bar:"+(br)+" "+getMeasuredWidth()+"*"+getMeasuredHeight());
topY += (barWidth+barSpace);
}
//绘制辅助线
paint.setStrokeWidth(0.5f);
paint.setColor(defColor);
canvas.drawLine(zeroPoint.x, topY-barSpace+15, rectChart.right, topY-barSpace+15, paint);
}
drawYmark(canvas);
}
private void drawYmark(Canvas canvas) {
//遮盖超出的柱状
paint.setStyle(Paint.Style.FILL);
paint.setColor(backColor);
canvas.drawRect(new RectF(0,0,rectChart.right, zeroPoint.y-lineWidth), paint);
canvas.drawRect(new RectF(0,rectChart.bottom,rectChart.right, getMeasuredHeight()), paint);
paintLabel.setTextSize(textSizeCoordinate);
paintLabel.setColor(textColorCoordinate);
for(int i = 0; i<=XMARK_MAX/XMARK; i++){
//绘制X刻度
String textX = (XMARK*i)+"";
float tw = FontUtil.getFontlength(paintLabel, textX);
// canvas.drawText(textX, getPaddingLeft()+(YMARK_MAX_WIDTH-tw), zeroPoint.y-(i*XMARK_H) - heightCoordinate/2+leadCoordinate,paintLabel);
canvas.drawText(textX, (int)(zeroPoint.x + (i*XMARK_H) - tw/2), zeroPoint.y-textSpace-heightCoordinate+leadCoordinate,paintLabel);
}
}
private float animPro; //动画计算的占比数量
/**创建动画*/
@Override
protected ValueAnimator initAnim() {
if(dataList.size()>0) {
ValueAnimator anim = ValueAnimator.ofObject(new AngleEvaluator(), 0f, 1f);
anim.setInterpolator(new AccelerateDecelerateInterpolator());
return anim;
}
return null;
}
/**动画值变化之后计算数据*/
@Override
protected void evaluatorData(ValueAnimator animation) {
animPro = (float)animation.getAnimatedValue();
}
}
@@ -0,0 +1,540 @@
package com.openxu.cview.chart.barchart;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import com.openxu.utils.DensityUtil;
import com.openxu.utils.FontUtil;
import com.openxu.utils.LogUtil;
import com.openxu.cview.chart.BaseChart;
import com.openxu.cview.chart.anim.AngleEvaluator;
import com.openxu.cview.chart.bean.BarBean;
import com.openxu.cview.R;
import java.util.ArrayList;
import java.util.List;
import static com.openxu.cview.chart.BaseChart.TOUCH_EVENT_TYPE.EVENT_X;
/**
* autour : openXu
* date : 2017/7/24 10:46
* className : BarVerticalChart
* version : 1.0
* description : 竖向(柱子竖直) 柱状图,支持多柱
*/
public class BarVerticalChart extends BaseChart {
private List<List<BarBean>> dataList;
private List<String> strList;
/**
* 可以设置的属性
*/
private boolean showLable = true; //是否显示下面的lable
private boolean showEnd = true; //当数据超出一屏宽度时,实现最后的数据
private int barNum = 2; //柱子数量
private int YMARK_NUM = 5; //Y轴刻度数量
private int barWidth = DensityUtil.dip2px(getContext(), 15); //柱宽度
private int barSpace = DensityUtil.dip2px(getContext(), 1); //双柱间的间距
private int barItemSpace = DensityUtil.dip2px(getContext(), 25);//一组柱之间的间距
private int[] barColor = new int[]{Color.BLUE, Color.YELLOW}; //柱颜色
private int rectW = DensityUtil.dip2px(getContext(), 10); //lable矩形宽高
private int rectH = DensityUtil.dip2px(getContext(), 10);
private int textSizeCoordinate = (int) getResources().getDimension(R.dimen.text_size_level_small); //坐标文字大小
private int textColorCoordinate = getResources().getColor(R.color.text_color_light_gray);
private int textSizeTag = (int) getResources().getDimension(R.dimen.text_size_level_small); //数值字体
private int textColorTag = getResources().getColor(R.color.text_color_light_gray);
private int textSizeLable = (int) getResources().getDimension(R.dimen.text_size_level_small); //lable字体
private int textColorLable = getResources().getColor(R.color.text_color_light_gray);
private int textSpace = DensityUtil.dip2px(getContext(), 3); //默认的字与其他的间距
private int textLableSpace = DensityUtil.dip2px(getContext(), 10); //默认的lable字与其他的间距
private int lableItemSpace = DensityUtil.dip2px(getContext(), 30); //默认的lable字与其他的间距
private int lableTopSpace = DensityUtil.dip2px(getContext(), 20); //默认的lable字与其他的间距
/**
* 需要计算相关值
*/
private int oneBarW; //单个宽度,需要计算
private int YMARK_MAX_WIDTH; //Y轴刻度最大值的宽度
private int YMARK_H, YMARK_ALL_H; //Y轴刻度间距值
private int leftStartPointX; //从左侧开始绘制的X坐标
private int minLeftPointX; //滑动到最右侧时的X坐标
private PointF zeroPoint = new PointF(); //柱状图图体圆点坐标
private RectF lableRect;
/*字体绘制相关*/
private int YMARK = 1; //Y轴刻度最大值(根据设置的数据自动赋值)
private int YMARK_MAX = 1; //Y轴刻度最大值(根据设置的数据自动赋值)
private int heightCoordinate;
private int leadCoordinate;
private int heightTag;
private int leadTag;
private int heightLable;
private int leadLable;
private float animPro; //动画计算的占比数量
public BarVerticalChart(Context context) {
this(context, null);
}
public BarVerticalChart(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public BarVerticalChart(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public void init(Context context, AttributeSet attrs, int defStyleAttr) {
touchEventType = EVENT_X;
dataList = new ArrayList<>();
strList = new ArrayList<>();
}
public void setShowEnd(boolean showEnd) {
this.showEnd = showEnd;
}
/***********************************设置属性set方法over**********************************/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
evaluatorByData();
postInvalidate();
}
/**
* 设置数据后,计算相关值
*/
private void evaluatorByData() {
total = 0;
//三种字体计算
paintLabel.setTextSize(textSizeCoordinate);
heightCoordinate = (int) FontUtil.getFontHeight(paintLabel);
leadCoordinate = (int) FontUtil.getFontLeading(paintLabel);
paintLabel.setTextSize(textSizeTag);
heightTag = (int) FontUtil.getFontHeight(paintLabel);
leadTag = (int) FontUtil.getFontLeading(paintLabel);
paintLabel.setTextSize(textSizeLable);
heightLable = (int) FontUtil.getFontHeight(paintLabel);
leadLable = (int) FontUtil.getFontLeading(paintLabel);
//下方lable范围计算
int lableH = 0;
if (showLable) {
lableH = (heightLable > rectH ? heightLable : rectH) + lableTopSpace;
lableRect = new RectF(rectChart.left, getMeasuredHeight() - getPaddingBottom() - lableH,
rectChart.right, rectChart.bottom);
}
//Y轴开始绘制的坐标 = 控件高度-padingB-lable高度-X轴刻度字高度-X刻度间距
zeroPoint.y = getMeasuredHeight() - getPaddingBottom() - lableH - heightCoordinate - textSpace;
LogUtil.w(TAG, "lableRect:" + lableRect + " lableH=" + lableH + " heightCoordinate=" + heightCoordinate + " textSpace=" + textSpace);
YMARK_MAX = 1;
/*计算Y刻度最大值*/
for (List<BarBean> list : dataList) {
for (BarBean bean : list) {
total += bean.getNum();
YMARK_MAX = (int) bean.getNum() > YMARK_MAX ? (int) bean.getNum() : YMARK_MAX;
}
}
LogUtil.i(TAG, "真实YMARK_MAX=" + YMARK_MAX);
if (YMARK_MAX <= 5)
YMARK_MAX = 5;
YMARK = YMARK_MAX / YMARK_NUM + 1;
int MARK = (Integer.parseInt((YMARK + "").substring(0, 1)) + 1);
if ((YMARK + "").length() == 1) {
//YMARK = 1、2、5、10
YMARK = (YMARK == 3 || YMARK == 4 || YMARK == 6 || YMARK == 7 || YMARK == 8 || YMARK == 9) ? ((YMARK == 3 || YMARK == 4) ? 5 : 10) : YMARK;
} else if ((YMARK + "").length() == 2) {
YMARK = MARK * 10;
} else if ((YMARK + "").length() == 3) {
YMARK = MARK * 100;
} else if ((YMARK + "").length() == 4) {
YMARK = MARK * 1000;
} else if ((YMARK + "").length() == 5) {
YMARK = MARK * 10000;
} else if ((YMARK + "").length() == 6) {
YMARK = MARK * 100000;
}
YMARK_MAX = YMARK * YMARK_NUM;
LogUtil.i(TAG, "计算YMARK_MAX=" + YMARK_MAX + " YMARK=" + YMARK);
//Y轴刻度间距值
YMARK_ALL_H = (int) (zeroPoint.y - getPaddingTop() - heightCoordinate);
YMARK_H = (int) ((float) YMARK_ALL_H / YMARK_NUM);
//单份柱子(包含多个及其间距)宽度
oneBarW = (barWidth * barNum) + (barSpace * (barNum - 1));
int allW = (oneBarW + barItemSpace) * dataList.size(); //每份柱子宽度+item间距(其中包含第一个item左边和最后一个item右边的半份间距)
paintLabel.setTextSize(textSizeCoordinate);
if(drawLine){
YMARK_MAX_WIDTH = (int) FontUtil.getFontlength(paintLabel, YMARK_MAX + "");
}else{
YMARK_MAX_WIDTH = 0;
}
int contentW = (int) (rectChart.right - rectChart.left - YMARK_MAX_WIDTH - textSpace);
touchEnable = allW > contentW;
zeroPoint.x = (int) rectChart.left + YMARK_MAX_WIDTH + textSpace;
leftStartPointX = (int) zeroPoint.x + barItemSpace / 2; //从左侧开始绘制的X坐标
if (touchEnable) {
//超出总宽度了
minLeftPointX = -allW + (int) rectChart.right;
mMoveLen = showEnd?(minLeftPointX - leftStartPointX):0;
} else {
minLeftPointX = 0;
mMoveLen = 0;
}
LogUtil.w(TAG, "柱状图表宽高:" + getMeasuredWidth() + "*" + getMeasuredHeight() +
" 图表范围" + rectChart + " 圆点坐标zeroPoint=" + zeroPoint);
LogUtil.w(TAG, "YMARK_MAX=" + YMARK_MAX + " YMARK=" + YMARK + " YMARK_H=" + YMARK_H + " YMARK_MAX_WIDTH=" + YMARK_MAX_WIDTH);
LogUtil.w(TAG, "minLeftPointX=" + minLeftPointX + " mMoveLen=" + mMoveLen + " leftStartPointX=" + leftStartPointX);
}
/**
* 绘制图表基本框架
*/
@Override
public void drawDefult(Canvas canvas) {
paint.setStyle(Paint.Style.FILL);
paint.setStrokeWidth(lineWidth);
paint.setColor(defColor);
//float startX, float startY, float stopX, float stopY
for (int i = 0; i <= YMARK_MAX / YMARK; i++) {
//绘制横向X轴刻度
float startX = zeroPoint.x;
float startY = zeroPoint.y - (i * YMARK_H);
float stopX = rectChart.right;
float stopY = startY;
if (drawLine) {
canvas.drawLine(startX, startY, stopX, stopY, paint);
}
// 绘制最下面的基线
if(drawBottomLine&&i==0){
canvas.drawLine(startX, startY, stopX, stopY, paint);
}
// LogUtil.w(TAG, "绘制直线start="+startX+"*"+startY+" stop="+stopX+"*"+stopY);
//绘制Y刻度
// paintLabel.setTextSize(textSizeCoordinate);
// String textY = (YMARK*i)+"";
// float tw = FontUtil.getFontlength(paintLabel, textY);
// canvas.drawText(textY, getPaddingLeft()+(YMARK_MAX_WIDTH-tw), startY - heightCoordinate/2+leadCoordinate,paintLabel);
}
//绘制左侧Y轴
if (drawLine) {
canvas.drawLine(zeroPoint.x + lineWidth / 2, zeroPoint.y, zeroPoint.x + lineWidth / 2, getPaddingTop(), paint);
}
if (showLable) {
//绘制lable
List<String> lableList = new ArrayList<>();
if (dataList.size() > 0) {
for (BarBean bean : dataList.get(0)) {
lableList.add(bean.getName());
}
}
paintLabel.setTextSize(textSizeLable);
paintLabel.setColor(textColorLable);
if (lableList != null && lableList.size() > 0) {
int lableAllW = 0;
for (int i = 0; i < lableList.size(); i++) {
lableAllW += (rectW + textLableSpace + FontUtil.getFontlength(paintLabel, lableList.get(i)));
}
lableAllW += (lableItemSpace * lableList.size() - 1);
int lableStart = 0;
if (lableAllW > ((int) (lableRect.right - lableRect.left))) {
//超过了,从left开始画
lableStart = (int) lableRect.left;
} else {
//没超过,话中间
lableStart = (int) (lableRect.left + ((lableRect.right - lableRect.left) / 2 - lableAllW / 2));
}
int left = lableStart;
for (int i = 0; i < lableList.size(); i++) {
String lable = lableList.get(i);
int tw = (int) FontUtil.getFontlength(paintLabel, lable);
paint.setColor(barColor[i]);
float rectTop = lableRect.bottom - rectH;
RectF rect = new RectF(left, rectTop, left + rectW, rectTop + rectH);
canvas.drawRect(rect, paint);
left += (rectW + textLableSpace);
rectTop = rectTop + rectH / 2 - heightLable / 2 + leadLable;
canvas.drawText(lable, left, rectTop, paintLabel);
left += (tw + lableItemSpace);
}
}
}
}
// 是否绘制最基础的线
public void setBaseLineAndText(boolean drawLine) {
this.drawLine = drawLine;
}
// 是否绘制最下面的基线
public void setBottomLine(boolean drawBottomLine) {
this.drawBottomLine = drawBottomLine;
}
/**
* 绘制debug辅助线
*/
@Override
public void drawDebug(Canvas canvas) {
super.drawDebug(canvas);
paint.setStyle(Paint.Style.STROKE);//设置空心
paint.setStrokeWidth(lineWidth);
paint.setColor(Color.BLACK);
canvas.drawRect(lableRect, paint);
}
/**
* 绘制图表
*/
@Override
public void drawChart(Canvas canvas) {
int leftStart = leftStartPointX + mMoveLen;
// LogUtil.i(TAG, "leftStart:"+leftStart+" zezeroPoint"+zeroPoint+" barItemSpace="+barItemSpace+" barWidth="+barWidth +" oneBarW="+oneBarW);
paint.setStyle(Paint.Style.FILL);
LogUtil.w(TAG, "");
for (int i = 0; i < dataList.size(); i++) {
String str = strList.get(i);
List<BarBean> list = dataList.get(i);
//柱子开始绘制的X坐标
int leftX = leftStart + (oneBarW + barItemSpace) * i;
//绘制X刻度
paintLabel.setTextSize(textSizeCoordinate);
paintLabel.setColor(textColorCoordinate);
float tw = FontUtil.getFontlength(paintLabel, str);
canvas.drawText(str, leftX + oneBarW / 2 - tw / 2, zeroPoint.y + textSpace + leadCoordinate, paintLabel);
// LogUtil.i(TAG, "绘制X刻度:leftX="+leftX +" "+(leftX + oneBarW/2-tw/2)+"*"+(zeroPoint.y+textSpace + leadCoordinate));
// LogUtil.i(TAG, "leftStartPointX="+leftStartPointX+" leftStart="+leftStart+ " oneBarW="+oneBarW +" barWidth="+barWidth+" barSpace="+barSpace);
paintLabel.setTextSize(textSizeTag);
paintLabel.setColor(textColorTag);
for (int j = 0; j < list.size(); j++) {
BarBean bean = list.get(j);
paint.setColor(barColor[j]);
float top = (zeroPoint.y - YMARK_ALL_H * (bean.getNum() / YMARK_MAX) * animPro);
RectF br = new RectF(leftX, top, leftX + barWidth, zeroPoint.y);
canvas.drawRect(br, paint);
str = (int) bean.getNum() + "";
tw = FontUtil.getFontlength(paintLabel, str);
canvas.drawText(str, leftX + barWidth / 2 - tw / 2, top - textSpace - heightTag + leadTag, paintLabel);
// LogUtil.d(TAG, "绘制bar:"+(br)+" "+getMeasuredWidth()+"*"+getMeasuredHeight());
leftX += (barWidth + barSpace);
}
}
if (drawLine) {
drawYmark(canvas);
}
}
private void drawYmark(Canvas canvas) {
//遮盖超出的柱状
paint.setStyle(Paint.Style.FILL);
paint.setColor(backColor);
canvas.drawRect(new RectF(0, 0, zeroPoint.x, lableRect.top), paint);
canvas.drawRect(new RectF(rectChart.right, 0, getMeasuredWidth(), lableRect.top), paint);
// LogUtil.d(TAG, "遮盖超出的柱状:"+new RectF(0,0,zeroPoint.x, lableRect.top));
// LogUtil.d(TAG, "遮盖超出的柱状:"+new RectF(rectChart.right,0,getMeasuredWidth(), lableRect.top));
paintLabel.setTextSize(textSizeCoordinate);
paintLabel.setColor(textColorCoordinate);
for (int i = 0; i <= YMARK_MAX / YMARK; i++) {
//绘制Y刻度
String textY = (YMARK * i) + "";
float tw = FontUtil.getFontlength(paintLabel, textY);
canvas.drawText(textY, getPaddingLeft() + (YMARK_MAX_WIDTH - tw), zeroPoint.y - (i * YMARK_H) - heightCoordinate / 2 + leadCoordinate, paintLabel);
}
}
/**
* 创建动画
*/
@Override
protected ValueAnimator initAnim() {
if (dataList.size() > 0) {
ValueAnimator anim = ValueAnimator.ofObject(new AngleEvaluator(), 0f, 1f);
anim.setInterpolator(new AccelerateDecelerateInterpolator());
return anim;
}
return null;
}
/**
* 动画值变化之后计算数据
*/
@Override
protected void evaluatorData(ValueAnimator animation) {
animPro = (float) animation.getAnimatedValue();
}
/***************************************************/
@Override
protected void evaluatorFling(float fling) {
LogUtil.i(TAG, "fling = " + fling);
if (leftStartPointX + mMoveLen + fling <= minLeftPointX) {
//最右侧
mMoveLen = minLeftPointX - leftStartPointX;
if (null != touchAnim && touchAnim.isRunning())
touchAnim.cancel();
} else if (leftStartPointX + mMoveLen + fling >= leftStartPointX) {
//最左侧
mMoveLen = 0;
if (null != touchAnim && touchAnim.isRunning())
touchAnim.cancel();
} else {
mMoveLen += fling;
}
}
/***********************************设置属性set方法**********************************/
public void setRectW(int rectW) {
this.rectW = rectW;
}
public void setRectH(int rectH) {
this.rectH = rectH;
}
public void setShowLable(boolean showLable) {
this.showLable = showLable;
}
public void setBarNum(int barNum) {
this.barNum = barNum;
}
public void setYMARK_NUM(int YMARK_NUM) {
this.YMARK_NUM = YMARK_NUM;
}
public void setBarWidth(int barWidth) {
this.barWidth = barWidth;
}
public void setBarSpace(int barSpace) {
this.barSpace = barSpace;
}
public void setBarItemSpace(int barItemSpace) {
this.barItemSpace = barItemSpace;
}
public void setBarColor(int[] barColor) {
this.barColor = barColor;
}
public void setTextSizeCoordinate(int textSizeCoordinate) {
this.textSizeCoordinate = textSizeCoordinate;
}
public void setTextColorCoordinate(int textColorCoordinate) {
this.textColorCoordinate = textColorCoordinate;
}
/***************************************************/
public void setTextSizeTag(int textSizeTag) {
this.textSizeTag = textSizeTag;
}
public void setTextColorTag(int textColorTag) {
this.textColorTag = textColorTag;
}
public void setTextSizeLable(int textSizeLable) {
this.textSizeLable = textSizeLable;
}
public void setTextColorLable(int textColorLable) {
this.textColorLable = textColorLable;
}
public void setTextSpace(int textSpace) {
this.textSpace = textSpace;
}
public void setTextLableSpace(int textLableSpace) {
this.textLableSpace = textLableSpace;
}
public void setLableItemSpace(int lableItemSpace) {
this.lableItemSpace = lableItemSpace;
}
public void setLableTopSpace(int lableTopSpace) {
this.lableTopSpace = lableTopSpace;
}
/**
* 设置数据
*
* @param dataList 柱状图数据
* @param strXList X轴坐标数据
*/
public void setData(List<List<BarBean>> dataList, List<String> strXList) {
LogUtil.w(TAG, "柱状图设置数据" + dataList);
this.strList.clear();
this.dataList.clear();
if (dataList != null)
this.dataList.addAll(dataList);
if (strXList != null)
this.strList.addAll(strXList);
if (rectChart!=null) {
evaluatorByData();
startDraw = false;
invalidate();
}
}
}
@@ -0,0 +1,69 @@
package com.openxu.cview.chart.bean;
import android.graphics.RectF;
import android.graphics.Region;
/**
* autour : openXu
* date : 2017/7/24 11:04
* className : PieChartBean
* version : 1.0
* description : 柱状图数据
*/
public class BarBean {
private float num;
private String name;
//bar绘制矩形
private RectF arcRect;
//触摸相关
private Region region; //扇形区域--用于判断手指触摸点是否在此范围
@Override
public String toString() {
return "BarBean{" +
"num=" + num +
", name='" + name + '\'' +
'}';
}
public BarBean() {
}
public BarBean(float num, String name) {
this.num = num;
this.name = name;
}
public float getNum() {
return num;
}
public void setNum(float num) {
this.num = num;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public RectF getArcRect() {
return arcRect;
}
public void setArcRect(RectF arcRect) {
this.arcRect = arcRect;
}
public Region getRegion() {
return region;
}
public void setRegion(Region region) {
this.region = region;
}
}
@@ -0,0 +1,55 @@
package com.openxu.cview.chart.bean;
/**
* autour : openXu
* date : 2017/10/11 14:08
* className : ChartLable
* version : 1.0
* description : 图表lable文字对象,包含文字大小和颜色
*/
public class ChartLable {
private String text;
private int textSize;
private int textColor;
public ChartLable(String text, int textSize, int textColor) {
this.text = text;
this.textSize = textSize;
this.textColor = textColor;
}
public ChartLable() {
}
@Override
public String toString() {
return "ChartLable{" +
"text='" + text + '\'' +
", textSize=" + textSize +
", textColor=" + textColor +
'}';
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public int getTextSize() {
return textSize;
}
public void setTextSize(int textSize) {
this.textSize = textSize;
}
public int getTextColor() {
return textColor;
}
public void setTextColor(int textColor) {
this.textColor = textColor;
}
}
@@ -0,0 +1,133 @@
package com.openxu.cview.chart.bean;
import android.graphics.PointF;
import android.graphics.RectF;
import android.graphics.Region;
import java.util.List;
/**
* autour : openXu
* date : 2017/7/24 11:04
* className : PieChartBean
* version : 1.0
* description : 饼状图数据
*/
public class PieChartBean {
private float num;
private String name;
//触摸相关
private Region region; //扇形区域--用于判断手指触摸点是否在此范围
private RectF rectLable; //矩形区域--用于判断手指触摸点是否在此范围
//扇形相关
private RectF arcRect;
private float startAngle;
private float sweepAngle;
//tag线段
private List<PointF> tagLinePoints;
private String tagStr;
private PointF tagTextPoint;
// //右侧lable
// private RectF colorRect;
// private PointF nameTextPoint;
// private PointF perTextPoint;
// private PointF dashPathPointStart;
// private PointF dashPathPointEnd;
@Override
public String toString() {
return "RoseChartBean{" +
"num=" + num +
", name='" + name + '\'' +
'}';
}
public PieChartBean(float num, String name) {
this.num = num;
this.name = name;
}
public String getTagStr() {
return tagStr;
}
public void setTagStr(String tagStr) {
this.tagStr = tagStr;
}
public PointF getTagTextPoint() {
return tagTextPoint;
}
public void setTagTextPoint(PointF tagTextPoint) {
this.tagTextPoint = tagTextPoint;
}
public List<PointF> getTagLinePoints() {
return tagLinePoints;
}
public void setTagLinePoints(List<PointF> tagLinePoints) {
this.tagLinePoints = tagLinePoints;
}
public RectF getArcRect() {
return arcRect;
}
public void setArcRect(RectF arcRect) {
this.arcRect = arcRect;
}
public float getStartAngle() {
return startAngle;
}
public void setStartAngle(float startAngle) {
this.startAngle = startAngle;
}
public float getSweepAngle() {
return sweepAngle;
}
public void setSweepAngle(float sweepAngle) {
this.sweepAngle = sweepAngle;
}
public RectF getRectLable() {
return rectLable;
}
public void setRectLable(RectF rectLable) {
this.rectLable = rectLable;
}
public Region getRegion() {
return region;
}
public void setRegion(Region region) {
this.region = region;
}
public float getNum() {
return num;
}
public void setNum(float num) {
this.num = num;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@@ -0,0 +1,67 @@
package com.openxu.cview.chart.dashboard;
/**
* autour : openXu
* date : 2018/4/20 15:14
* className : DashBoardItem
* version : 1.0
* description : 请添加类说明
*/
public class DashBoardItem {
private int color;
private String lable;
private float num;
private float angle;
public DashBoardItem() {
}
public DashBoardItem(int color, String lable, float num) {
this.color = color;
this.lable = lable;
this.num = num;
}
@Override
public String toString() {
return "DashBoardItem{" +
"color=" + color +
", lable='" + lable + '\'' +
", num=" + num +
", angle=" + angle +
'}';
}
public float getAngle() {
return angle;
}
public void setAngle(float angle) {
this.angle = angle;
}
public int getColor() {
return color;
}
public void setColor(int color) {
this.color = color;
}
public String getLable() {
return lable;
}
public void setLable(String lable) {
this.lable = lable;
}
public float getNum() {
return num;
}
public void setNum(float num) {
this.num = num;
}
}
@@ -0,0 +1,270 @@
package com.openxu.cview.chart.dashboard;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathEffect;
import android.graphics.PointF;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.animation.AccelerateDecelerateInterpolator;
import com.openxu.cview.R;
import com.openxu.cview.chart.BaseChart;
import com.openxu.cview.chart.anim.AngleEvaluator;
import com.openxu.utils.DensityUtil;
import com.openxu.utils.FontUtil;
import com.openxu.utils.LogUtil;
import java.util.ArrayList;
import java.util.List;
/**
* autour : openXu
* date : 2018/4/20 15:12
* className : DashBoardView
* version : 1.0
* description : 仪表
*/
public class DashBoardView extends BaseChart {
private List<DashBoardItem> dataList;
/**设置的属性*/
private int ringWidth = DensityUtil.dip2px(getContext(), 20);//圆环宽度
private int textSizeCoordinate = (int)getResources().getDimension(R.dimen.ts_barchart_x);
private int textSizeLable = (int)getResources().getDimension(R.dimen.ts_barchart_lable);
private int textColorCoordinate = getResources().getColor(R.color.text_color_light_gray);
private int textColorLable = getResources().getColor(R.color.text_color_def);
private int textXSpace = DensityUtil.dip2px(getContext(), 6);
private int textLableSpace = DensityUtil.dip2px(getContext(), 10);
//小圆圈半径
private int lableCircleSize = DensityUtil.dip2px(getContext(), 4);
//指针半径
private int centerPointSize = DensityUtil.dip2px(getContext(), 5);
/**常量*/
private final float startAngle = 180;
private final float endAngle = 360;
/**计算*/
private PointF arcCenter;
private float arcRaidus; //饼状图半径
private float tagMaxW;
public DashBoardView(Context context) {
super(context, null);
}
public DashBoardView(Context context, AttributeSet attrs) {
super(context, attrs, 0);
}
public DashBoardView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public void init(Context context, AttributeSet attrs, int defStyleAttr) {
dataList = new ArrayList<>();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(widthSize, heightSize);
calculatData();
}
/***********************************设置属性set方法**********************************/
public void setData(List<DashBoardItem> dataList) {
this.dataList.clear();
if(dataList!=null)
this.dataList.addAll(dataList);
calculatData();
}
private float progress;
private float progressAnim;
public void setPro(float progress){
this.progress = progress;
invalidate();
}
/***********************************设置属性set方法over**********************************/
public void calculatData() {
total = 0;
float maxLableLength = 0; //lable最长的长度
paintLabel.setTextSize(textSizeLable);
for(DashBoardItem bean : dataList){
total += bean.getNum();
float l = FontUtil.getFontlength(paintLabel, bean.getLable());
maxLableLength = maxLableLength>l?maxLableLength:l;
}
//计算每个部分的角度
float allAngle = endAngle - startAngle;
for(DashBoardItem bean : dataList){
bean.setAngle(allAngle * (bean.getNum()/total*1.0f));
}
//lable所占用的宽度
maxLableLength = maxLableLength + textLableSpace + lableCircleSize * 2;
int widthSize = getMeasuredWidth();
int heightSize = getMeasuredHeight();
paintLabel.setTextSize(textSizeCoordinate);
float xLableSize = FontUtil.getFontHeight(paint);
arcCenter = new PointF(
getPaddingLeft()+(widthSize - getPaddingLeft() - maxLableLength -getPaddingRight())/2,
heightSize - xLableSize - textXSpace- getPaddingBottom()
);
float raidusX = arcCenter.x - getPaddingLeft();
float raidusY = arcCenter.y - getPaddingTop();
arcRaidus = raidusX<raidusY?raidusX:raidusY;
rectChart = new RectF(arcCenter.x - arcRaidus,arcCenter.y - arcRaidus,
arcCenter.x + arcRaidus,arcCenter.y + arcRaidus);
if(centerPoint!=null && centerPoint.x>0){
startDraw = false;
invalidate();
}
}
/**绘制图表基本框架*/
@Override
public void drawDefult(Canvas canvas) {
/* paint.setStyle(Paint.Style.FILL);
paint.setColor(defColor);
canvas.drawCircle(centerPoint.x,centerPoint.y,chartRaidus, paint);
if(ringWidth>0){ //绘制空心圆圈
paint.setColor(backColor);
canvas.drawCircle(centerPoint.x,centerPoint.y,chartRaidus-ringWidth, paint);
}*/
}
/**绘制debug辅助线*/
@Override
public void drawDebug(Canvas canvas) {
super.drawDebug(canvas);
paint.setColor(Color.RED);
canvas.drawRect(rectChart, paint);
}
@Override
public void drawChart(Canvas canvas) {
// paint.setStrokeWidth(ringWidth);
if(null==dataList || dataList.size()<=0)
return;
paint.setStyle(Paint.Style.FILL);//设置空心
float angle = startAngle;
float lableTop = getPaddingTop();
paintLabel.setTextSize(textSizeLable);
paintLabel.setColor(textColorLable);
float lableLead = FontUtil.getFontLeading(paintLabel);
float lableHeight = FontUtil.getFontHeight(paintLabel);
float lableCrc = (lableHeight - lableCircleSize*2)/2;
// canvas.drawArc(rectChart, 180, 180, false, paint);
for(DashBoardItem bean : dataList){
paint.setColor(bean.getColor());
LogUtil.w(TAG, "绘制扇形"+bean);
canvas.drawArc(rectChart, angle, bean.getAngle(), true, paint);
angle += bean.getAngle();
//绘制lable
canvas.drawCircle(rectChart.right+lableCircleSize, lableTop+lableCrc+lableCircleSize, lableCircleSize, paint);
canvas.drawText(bean.getLable(), rectChart.right+lableCircleSize*2+textLableSpace, lableTop + lableLead, paintLabel);
lableTop += lableHeight+DensityUtil.dip2px(getContext(),5);
}
//覆盖
paint.setColor(Color.WHITE);
canvas.drawCircle(arcCenter.x, arcCenter.y, arcRaidus - ringWidth, paint);
//绘制x坐标
paintLabel.setTextSize(textSizeCoordinate);
paintLabel.setColor(textColorCoordinate);
float xLead = FontUtil.getFontLeading(paintLabel);
float xLength = FontUtil.getFontlength(paintLabel, "0");
canvas.drawText("0", rectChart.left+(ringWidth-xLength)/2,
arcCenter.y + textXSpace+xLead, paintLabel);
xLength = FontUtil.getFontlength(paintLabel, "0分钟");
canvas.drawText("0分钟",
rectChart.right-(Math.abs((ringWidth-xLength)/2))-(xLength<ringWidth?xLength:ringWidth),
arcCenter.y + textXSpace+xLead, paintLabel);
//绘制虚线
paintEffect.setStyle(Paint.Style.STROKE);
paintEffect.setStrokeWidth(lineWidth);
paintEffect.setColor(defColor);
PathEffect effects = new DashPathEffect(new float[]{15,8,15,8},0);
paintEffect.setPathEffect(effects);
Path path = new Path();
path.moveTo(rectChart.left, arcCenter.y);
path.lineTo(rectChart.right, arcCenter.y);
canvas.drawPath(path, paintEffect);
path = new Path();
path.moveTo(arcCenter.x, rectChart.top+ringWidth);
path.lineTo(arcCenter.x, arcCenter.y);
canvas.drawPath(path, paintEffect);
//绘制指针
paint.setColor(Color.BLACK);
canvas.drawCircle(arcCenter.x, arcCenter.y, centerPointSize, paint);
/*画一个实心三角形*/
float proAndle = (endAngle - startAngle) * (progressAnim/total*1.0f);
//以正东面为0度起点计算指定角度所对应的圆周上的点的坐标:
float centerArcX1 = arcCenter.x + (float) (centerPointSize* Math.cos(Math.toRadians(startAngle+proAndle - 90)));
float centerArcY1 = arcCenter.y + (float) (centerPointSize* Math.sin(Math.toRadians(startAngle+proAndle - 90)));
float endx2 = arcCenter.x + (float) (arcRaidus* Math.cos(Math.toRadians(startAngle+proAndle)));
float endy2 = arcCenter.y + (float) (arcRaidus* Math.sin(Math.toRadians(startAngle+proAndle)));
float centerArcX2 = arcCenter.x + (float) (centerPointSize* Math.cos(Math.toRadians(startAngle+proAndle + 90)));
float centerArcY2 = arcCenter.y + (float) (centerPointSize* Math.sin(Math.toRadians(startAngle+proAndle + 90)));
Path path2=new Path();
path2.moveTo(centerArcX1,centerArcY1);
path2.lineTo(endx2,endy2);
path2.lineTo(centerArcX2,centerArcY2);
path2.close();
canvas.drawPath(path2, paint);
}
/**创建动画*/
@Override
protected ValueAnimator initAnim() {
anim = ValueAnimator.ofObject(new AngleEvaluator(), 0f, 1.0f);
anim.setInterpolator(new AccelerateDecelerateInterpolator());
return anim;
}
/**动画值变化之后计算数据*/
@Override
protected void evaluatorData(ValueAnimator animation) {
float percentage = (float) animation.getAnimatedValue();
evaluatorData(percentage);
}
/**计算各种绘制坐标*/
private void evaluatorData(float animPre){
progressAnim = animPre*progress;
}
}
@@ -0,0 +1,395 @@
package com.openxu.cview.chart.piechart;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.RectF;
import android.graphics.Region;
import android.graphics.Typeface;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import com.openxu.cview.chart.BaseChart;
import com.openxu.cview.chart.anim.AngleEvaluator;
import com.openxu.cview.chart.bean.ChartLable;
import com.openxu.cview.chart.bean.PieChartBean;
import com.openxu.utils.FontUtil;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import static com.openxu.cview.chart.piechart.PieChartLayout.TAG_MODUL.MODUL_CHART;
/**
* autour : openXu
* date : 2017/7/24 10:46
* className : ProgressPieChart
* version : 1.0
* description : 占比饼状图表
*/
public class PieChart extends BaseChart {
private List<PieChartBean> dataList;
private List<ChartLable> lableList; //需要绘制的lable集合
/**设置的属性*/
private boolean showZeroPart = false; //如果某部分占比为0, 是否显示
private int centerLableSpace;
//圆环宽度,如果值>0,则为空心圆环,内环为白色,可以在内环中绘制字
private int ringWidth;
private int lineLenth;
private int outSpace;
private int textSpace;
private PieChartLayout.TAG_TYPE tagType; //TAG展示类型
private PieChartLayout.TAG_MODUL tagModul; //TAG展示位置
private int tagTextSize; //tag文字大小
private int tagTextColor;
/**计算*/
private int chartSize; //饼状图大小
private int chartRaidus; //饼状图半径
private float tagMaxW;
private Paint paintSelected;
//RGB颜色数组
private int arrColorRgb[][];
public PieChart(Context context) {
super(context, null);
}
public PieChart(Context context, AttributeSet attrs) {
super(context, attrs, 0);
}
public PieChart(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public void init(Context context, AttributeSet attrs, int defStyleAttr) {
dataList = new ArrayList<>();
startAngle = -90; //开始的角度
paintSelected = new Paint();
paintSelected.setColor(Color.LTGRAY);
paintSelected.setStyle(Paint.Style.STROKE);//设置空心
paintSelected.setStrokeWidth(lineWidth*5);
paintSelected.setAntiAlias(true);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = View.MeasureSpec.getMode(widthMeasureSpec);
int widthSize = View.MeasureSpec.getSize(widthMeasureSpec);
int heightMode = View.MeasureSpec.getMode(heightMeasureSpec);
int heightSize = View.MeasureSpec.getSize(heightMeasureSpec);
initSome();
setMeasuredDimension(widthSize, heightSize);
}
private void initSome(){
int widthSize = getMeasuredWidth();
int heightSize = getMeasuredHeight();
chartSize = widthSize>heightSize?heightSize:widthSize;
//外圈半径=size/2-最大部分lble线长度-边距
paintLabel.setTextSize(tagTextSize);
tagMaxW = 0;
if(MODUL_CHART == tagModul){
switch (tagType){
case TYPE_NUM:
tagMaxW = FontUtil.getFontlength(paintLabel, "0.0%");
break;
case TYPE_PERCENT:
tagMaxW = FontUtil.getFontlength(paintLabel, "000");
break;
}
}
chartRaidus = chartSize/2-lineLenth-(int)tagMaxW-textSpace-outSpace;
centerPoint = new PointF(widthSize/2, heightSize/2);
if(widthSize<heightSize){
rectChart = new RectF(0,(heightSize-widthSize)/2,widthSize, (heightSize+widthSize)/2);
}else{
rectChart = new RectF((widthSize-heightSize)/2, 0, (widthSize+heightSize)/2, heightSize);
}
}
/***********************************设置属性set方法**********************************/
public void setArrColorRgb(int[][] arrColorRgb) {
this.arrColorRgb = arrColorRgb;
}
public void setTagTextSize(int tagTextSize) {
this.tagTextSize = tagTextSize;
initSome();
}
public void setTagTextColor(int tagTextColor) {
this.tagTextColor = tagTextColor;
}
public void setTagType(PieChartLayout.TAG_TYPE tagType) {
this.tagType = tagType;
initSome();
}
public void setTagModul(PieChartLayout.TAG_MODUL tagModul) {
this.tagModul = tagModul;
initSome();
}
public void setShowZeroPart(boolean showZeroPart) {
this.showZeroPart = showZeroPart;
}
public void setCenterLableSpace(int centerLableSpace) {
this.centerLableSpace = centerLableSpace;
}
public void setRingWidth(int ringWidth) {
this.ringWidth = ringWidth;
}
public void setLineLenth(int lineLenth) {
this.lineLenth = lineLenth;
}
public void setOutSpace(int outSpace) {
this.outSpace = outSpace;
}
public void setTextSpace(int textSpace) {
this.textSpace = textSpace;
}
/**
* 设置数据
*/
public void setData(List<PieChartBean> dataList, List<ChartLable> lableList){
total = 0;
this.lableList = lableList;
this.dataList.clear();
if(dataList!=null)
this.dataList.addAll(dataList);
for(PieChartBean bean : this.dataList){
total += bean.getNum();
}
if(centerPoint!=null && centerPoint.x>0){
startDraw = false;
invalidate();
}
}
/***********************************设置属性set方法over**********************************/
/**绘制图表基本框架*/
@Override
public void drawDefult(Canvas canvas) {
paint.setStyle(Paint.Style.FILL);
paint.setColor(defColor);
canvas.drawCircle(centerPoint.x,centerPoint.y,chartRaidus, paint);
if(ringWidth>0){ //绘制空心圆圈
paint.setColor(backColor);
canvas.drawCircle(centerPoint.x,centerPoint.y,chartRaidus-ringWidth, paint);
}
}
/**绘制debug辅助线*/
@Override
public void drawDebug(Canvas canvas) {
super.drawDebug(canvas);
canvas.drawCircle(centerPoint.x, centerPoint.y, chartRaidus, paint);
canvas.drawCircle(centerPoint.x, centerPoint.y, chartRaidus+lineLenth, paint);
canvas.drawCircle(centerPoint.x, centerPoint.y, chartRaidus+lineLenth+textSpace, paint);
canvas.drawCircle(centerPoint.x, centerPoint.y, chartRaidus+lineLenth+textSpace+(int)tagMaxW, paint);
canvas.drawCircle(centerPoint.x, centerPoint.y, chartRaidus+lineLenth+textSpace+(int)tagMaxW+outSpace, paint);
}
private int selectedIndex = -1; //被选中的索引
/**绘制图表*/
@Override
public void drawChart(Canvas canvas) {
int index = -1;
for(int i = 0; i < dataList.size(); i++){
PieChartBean bean = dataList.get(i);
if(bean.getNum() == 0){
if(showZeroPart)
index++;
continue;
}
index++;
paint.setARGB(255, arrColorRgb[index%arrColorRgb.length][0], arrColorRgb[index%arrColorRgb.length][1], arrColorRgb[index%arrColorRgb.length][2]);
paintLabel.setARGB(255, arrColorRgb[index%arrColorRgb.length][0], arrColorRgb[index%arrColorRgb.length][1], arrColorRgb[index%arrColorRgb.length][2]);
if(selectedIndex == i){
paint.setTypeface(Typeface.DEFAULT_BOLD);
paintLabel.setTypeface(Typeface.DEFAULT_BOLD);
}else{
paint.setTypeface(Typeface.DEFAULT);
paintLabel.setTypeface(Typeface.DEFAULT);
}
/**1、绘制扇形*/
paint.setStyle(Paint.Style.FILL);//设置实心
canvas.drawArc(bean.getArcRect(), bean.getStartAngle(), bean.getSweepAngle(), true, paint);
if(selectedIndex == i){
//被选中的,绘制边界
paintSelected.setStyle(Paint.Style.STROKE);//设置空心
canvas.drawArc(bean.getArcRect(), bean.getStartAngle(), bean.getSweepAngle(), true, paintSelected);
}
if(MODUL_CHART == tagModul){
/**2、绘制直线*/
List<PointF> tagLinePoints = bean.getTagLinePoints();
if (tagLinePoints != null && tagLinePoints.size() > 0) {
for (int p = 1; p < tagLinePoints.size(); p++) {
canvas.drawLine(tagLinePoints.get(p - 1).x, tagLinePoints.get(p - 1).y,
tagLinePoints.get(p).x, tagLinePoints.get(p).y, paint);
}
}
/**3、绘制指示标签*/
paintLabel.setTextSize(tagTextSize);
if (tagTextColor != 0) {
paintLabel.setColor(tagTextColor);
}
canvas.drawText(bean.getTagStr() + "", bean.getTagTextPoint().x, bean.getTagTextPoint().y, paintLabel);
}
}
//绘制中心内圆
if(ringWidth>0){
paint.setColor(backColor);
paint.setStyle(Paint.Style.FILL);//设置实心
canvas.drawCircle(centerPoint.x, centerPoint.y, chartRaidus-ringWidth, paint);
}
/**4 绘制中间文字*/
if(ringWidth>0 && lableList!=null &&lableList.size()>0){
float textAllHeight = 0;
float textW, textH, textL;
for(ChartLable lable : lableList){
paintLabel.setTextSize(lable.getTextSize());
textH = FontUtil.getFontHeight(paintLabel);
textAllHeight += (textH+centerLableSpace);
}
textAllHeight -= centerLableSpace;
int top = (int)(centerPoint.y-textAllHeight/2);
for(ChartLable lable : lableList){
paintLabel.setColor(lable.getTextColor());
paintLabel.setTextSize(lable.getTextSize());
textW = FontUtil.getFontlength(paintLabel, lable.getText());
textH = FontUtil.getFontHeight(paintLabel);
textL = FontUtil.getFontLeading(paintLabel);
canvas.drawText(lable.getText(), centerPoint.x-textW/2, top + textL, paintLabel);
top += (textH+centerLableSpace);
}
}
}
/**创建动画*/
@Override
protected ValueAnimator initAnim() {
anim = ValueAnimator.ofObject(new AngleEvaluator(), 0f, 1.0f);
anim.setInterpolator(new AccelerateDecelerateInterpolator());
return anim;
}
/**动画值变化之后计算数据*/
@Override
protected void evaluatorData(ValueAnimator animation) {
float percentage = (float) animation.getAnimatedValue();
evaluatorData(percentage);
}
/**计算各种绘制坐标*/
private void evaluatorData(float animPre){
paintLabel.setTextSize(tagTextSize);
float tagTextLead = FontUtil.getFontLeading(paintLabel);
float tagTextHeight = FontUtil.getFontHeight(paintLabel);
float oneStartAngle = startAngle;
for(int i = 0; i < dataList.size(); i++) {
PieChartBean bean = dataList.get(i);
/* if(bean.getNum() == 0 && !showZeroPart){
continue;
}*/
/**1、绘制扇形*/
float arcLeft = centerPoint.x - chartRaidus; //扇形半径
float arcTop = centerPoint.y - chartRaidus;
float arcRight = centerPoint.x + chartRaidus;
float arcBottom = centerPoint.y + chartRaidus;
// float percentage = 360.0f / total * bean.getNum();
float percentage = (bean.getNum()==0?0:(360.0f / total * bean.getNum())*animPre);
bean.setArcRect(new RectF(arcLeft, arcTop, arcRight, arcBottom));
bean.setStartAngle(oneStartAngle);
bean.setSweepAngle(percentage);
/**2、计算扇形区域*/
arcLeft = centerPoint.x - chartSize;
arcTop = centerPoint.y - chartSize;
arcRight = centerPoint.x + chartSize;
arcBottom = centerPoint.y + chartSize;
Path allPath = new Path();
allPath.moveTo(centerPoint.x, centerPoint.y);//添加原始点
float ovalX = centerPoint.x + (float) (chartRaidus * Math.cos(Math.toRadians(oneStartAngle)));
float ovalY = centerPoint.y + (float) (chartRaidus * Math.sin(Math.toRadians(oneStartAngle)));
allPath.lineTo(ovalX, ovalY);
RectF touchOval = new RectF(arcLeft, arcTop, arcRight, arcBottom);
allPath.addArc(touchOval, oneStartAngle, percentage);
allPath.lineTo(centerPoint.x, centerPoint.y);
allPath.close();
RectF r = new RectF();
allPath.computeBounds(r, true);
Region region = new Region();
region.setPath(allPath, new Region((int) r.left, (int) r.top, (int) r.right, (int) r.bottom));
bean.setRegion(region);
if(MODUL_CHART == tagModul) {
/**3、绘制直线*/
//确定直线的起始和结束的点的位置
float startX = centerPoint.x + (float) (chartRaidus * Math.cos(Math.toRadians(oneStartAngle + percentage / 2)));
float startY = centerPoint.y + (float) (chartRaidus * Math.sin(Math.toRadians(oneStartAngle + percentage / 2)));
float endX = centerPoint.x + (float) ((chartRaidus + lineLenth - 20) * Math.cos(Math.toRadians(oneStartAngle + percentage / 2)));
float endY = centerPoint.y + (float) ((chartRaidus + lineLenth - 20) * Math.sin(Math.toRadians(oneStartAngle + percentage / 2)));
boolean isRight = true;
float lineAngle = oneStartAngle + percentage / 2;
if (lineAngle > 90 && lineAngle < 270) {
isRight = false;
}
// LogUtil.i(TAG, "直线坐标:start=("+startX+","+startY +") end=("+endX+","+endY+")"+" lineAngle="+lineAngle+" isRight="+isRight);
List<PointF> tagLinePoints = new ArrayList<>();
tagLinePoints.add(new PointF(startX, startY));
tagLinePoints.add(new PointF(endX, endY));
float textX = isRight ? (endX + 20) : (endX - 20);
tagLinePoints.add(new PointF(textX, endY));
bean.setTagLinePoints(tagLinePoints);
/**3、绘制指示标签*/
String tagText = "";
paintLabel.setTextSize(tagTextSize);
if (tagType == PieChartLayout.TAG_TYPE.TYPE_NUM) {
tagText = bean.getNum() + "";
} else if (tagType == PieChartLayout.TAG_TYPE.TYPE_PERCENT) {
DecimalFormat decimalFormat = new DecimalFormat("0.0%");
tagText = (total==0?"/":decimalFormat.format(((float) bean.getNum() / (float) total)));
}
float textW = FontUtil.getFontlength(paintLabel, tagText);
textX = isRight ? textX + textSpace : (textX - textW - textSpace);
float textY = endY - tagTextHeight / 2 + tagTextLead;
bean.setTagStr(tagText);
bean.setTagTextPoint(new PointF(textX, textY));
}
/*开始角度累加*/
oneStartAngle += percentage;
}
}
}
@@ -0,0 +1,285 @@
package com.openxu.cview.chart.piechart;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import com.openxu.cview.chart.BaseChart;
import com.openxu.cview.chart.bean.PieChartBean;
import com.openxu.utils.DensityUtil;
import com.openxu.utils.FontUtil;
import com.openxu.utils.LogUtil;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import static com.openxu.cview.chart.BaseChart.TOUCH_EVENT_TYPE.EVENT_Y;
import static com.openxu.cview.chart.piechart.PieChartLayout.TAG_MODUL.MODUL_LABLE;
/**
* autour : openXu
* date : 2017/7/24 10:46
* className : PieChartLableView
* version : 1.0
* description : 占比饼状图表
*/
public class PieChartLableView extends BaseChart {
private List<PieChartBean> dataList;
private boolean showZeroPart = false; //如果某部分占比为0, 是否显示
protected RectF rectLable; //图表矩形
private int minTopPointY; //滑动到最上面时的y坐标
private int top; //绘制的y坐标
private int oneLableHeight; //右侧标签单个高度,需要计算
/**设置的属性*/
private int rectW;
private int rectH;
private int rectRaidus; //矩形圆角
private int rectSpace; //右侧标签上下间距
private int leftSpace;
private int textSize = 90; //文字大小
private int textColor;
private int arrColorRgb[][];
private PieChartLayout.TAG_TYPE tagType; //TAG展示类型
private PieChartLayout.TAG_MODUL tagModul; //TAG展示位置
public PieChartLableView(Context context) {
super(context, null);
}
public PieChartLableView(Context context, AttributeSet attrs) {
super(context, attrs, 0);
}
public PieChartLableView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public void init(Context context, AttributeSet attrs, int defStyleAttr) {
touchEventType = EVENT_Y;
dataList = new ArrayList<>();
setClickable(true);
}
/***********************************设置属性set方法**********************************/
public void setArrColorRgb(int[][] arrColorRgb) {
this.arrColorRgb = arrColorRgb;
}
public void setTextSize(int textSize) {
this.textSize = textSize;
}
public void setTextColor(int textColor) {
this.textColor = textColor;
}
public void setShowZeroPart(boolean showZeroPart) {
this.showZeroPart = showZeroPart;
}
public void setRectW(int rectW) {
this.rectW = rectW;
}
public void setRectH(int rectH) {
this.rectH = rectH;
}
public void setRectRaidus(int rectRaidus) {
this.rectRaidus = rectRaidus;
}
public void setTagType(PieChartLayout.TAG_TYPE tagType) {
this.tagType = tagType;
}
public void setTagModul(PieChartLayout.TAG_MODUL tagModul) {
this.tagModul = tagModul;
}
public void setRectSpace(int rectSpace) {
this.rectSpace = rectSpace;
}
public void setLeftSpace(int leftSpace) {
this.leftSpace = leftSpace;
}
/***********************************设置属性set方法over**********************************/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = View.MeasureSpec.getMode(widthMeasureSpec);
int widthSize = View.MeasureSpec.getSize(widthMeasureSpec);
int heightMode = View.MeasureSpec.getMode(heightMeasureSpec);
int heightSize = View.MeasureSpec.getSize(heightMeasureSpec);
centerPoint = new PointF(widthSize/2, heightSize/2);
rectLable = new RectF(0,0,widthSize, heightSize);
setMeasuredDimension(widthSize, heightSize);
evaluatorLable();
}
@Override
public void dispatchTouchEvent1(MotionEvent event) {
int move = (int)(event.getY() - lastTouchPoint.y);
if(top + mMoveLen <= minTopPointY){
if(move<0){
LogUtil.e(TAG, "已经到最下面了,还往上滑,不行");
getParent().requestDisallowInterceptTouchEvent(false);
}else{
getParent().requestDisallowInterceptTouchEvent(true);
}
}else if(top + mMoveLen>=rectSpace){
if(move>0){
LogUtil.w(TAG, "已经划到头了,还往下滑,不行");
getParent().requestDisallowInterceptTouchEvent(false);
}else{
getParent().requestDisallowInterceptTouchEvent(true);
}
}else{
//请求所有父控件及祖宗控件不要拦截事件
getParent().requestDisallowInterceptTouchEvent(true);
LogUtil.i(TAG, "正常请求事件");
}
}
@Override
protected void evaluatorFling(float fling) {
if(top + mMoveLen + fling <= minTopPointY){
mMoveLen = minTopPointY-rectSpace;
}else if(top + mMoveLen+ fling>=rectSpace){
mMoveLen = 0;
} else{
mMoveLen += fling;
}
}
/**
* 设置数据
*/
public void setData(List<PieChartBean> dataList){
this.dataList.clear();
if(dataList!=null)
this.dataList.addAll(dataList);
evaluatorLable();
startDraw = false;
invalidate();
}
/**计算右侧标签相关坐标值*/
private void evaluatorLable(){
paintLabel.setTextSize(textSize);
oneLableHeight = (int) FontUtil.getFontHeight(paintLabel);
//字和矩形中高度的较大值
oneLableHeight = oneLableHeight>rectH?oneLableHeight:rectH;
int allHeight = 0 ;
//总高度
for(PieChartBean bean : dataList){
if(bean.getNum() == 0 && !showZeroPart){
continue;
}
allHeight += oneLableHeight+rectSpace;
}
if(allHeight>0)
allHeight -= rectSpace;
// int allHeight = (oneLableHeight+rectSpace)*dataList.size() - rectSpace;
int contentH = getMeasuredHeight() - rectSpace*2;
touchEnable = allHeight>contentH;
if(touchEnable){
//超出总高度了
top = rectSpace;
minTopPointY = -allHeight - rectSpace + getMeasuredHeight();
if(debug)
LogUtil.i(TAG, "边界"+minTopPointY+" "+getMeasuredHeight());
}else{
top = (getMeasuredHeight()-allHeight)/2;
}
}
/**绘制图表基本框架*/
@Override
public void drawDefult(Canvas canvas) {
}
/**绘制debug辅助线*/
@Override
public void drawDebug(Canvas canvas) {
super.drawDebug(canvas);
}
/**绘制图表*/
@Override
public void drawChart(Canvas canvas) {
int topY = top + mMoveLen;
paintLabel.setTextSize(textSize);
int lableH =(int) FontUtil.getFontHeight(paintLabel);
int lableL =(int) FontUtil.getFontLeading(paintLabel);
paint.setStyle(Paint.Style.FILL);
int index = -1;
for(int i = 0; i < dataList.size(); i++){
PieChartBean bean = dataList.get(i);
if(bean.getNum() == 0 && !showZeroPart){
continue;
}
index++;
paint.setARGB(255, arrColorRgb[index%arrColorRgb.length][0], arrColorRgb[index%arrColorRgb.length][1], arrColorRgb[index%arrColorRgb.length][2]);
paintLabel.setARGB(255, arrColorRgb[index%arrColorRgb.length][0], arrColorRgb[index%arrColorRgb.length][1], arrColorRgb[index%arrColorRgb.length][2]);
int rectTop = topY+(oneLableHeight-rectH)/2;
RectF rect = new RectF(leftSpace, rectTop, leftSpace+rectW, rectTop+rectH);
canvas.drawRoundRect(rect, rectRaidus, rectRaidus, paint);
if(textColor!=0) {
paintLabel.setColor(textColor);
}
rectTop = topY+(oneLableHeight-lableH)/2;
canvas.drawText(bean.getName(), leftSpace+rectW+leftSpace , rectTop+lableL , paintLabel);
float textW = FontUtil.getFontlength(paintLabel, bean.getName());
/**5、绘制指示标签*/
if(MODUL_LABLE == tagModul){
String tagText = "";
if (tagType == PieChartLayout.TAG_TYPE.TYPE_NUM) {
tagText = bean.getNum() + "";
} else if (tagType == PieChartLayout.TAG_TYPE.TYPE_PERCENT) {
DecimalFormat decimalFormat = new DecimalFormat("0.0%");
tagText = decimalFormat.format(((float) bean.getNum() / (float) total));
}
float tagW = FontUtil.getFontlength(paintLabel, tagText);
canvas.drawText(tagText, getMeasuredWidth()-tagW- DensityUtil.dip2px(getContext(),3), rectTop+lableL , paintLabel);
/* DashPathEffect dashPathEffect = new DashPathEffect(new float[]{8,10,8,10}, 0);
paint.setColor(Color.RED);
// paint.setPathEffect(dashPathEffect);
paint.setStrokeWidth(4);
Path path = new Path();
path.moveTo(leftSpace+rectW+leftSpace+textW, rectTop);
path.lineTo(getMeasuredWidth()-tagW- DensityUtil.dip2px(getContext(),3), rectTop);
LogUtil.i(TAG, "绘制虚线"+path);
canvas.drawPath(path,paint);*/
}
topY += oneLableHeight + rectSpace;
}
}
/**创建动画*/
@Override
protected ValueAnimator initAnim() {
return null;
}
/**动画值变化之后计算数据*/
@Override
protected void evaluatorData(ValueAnimator animation) {
}
}
@@ -0,0 +1,293 @@
package com.openxu.cview.chart.piechart;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;
import com.openxu.cview.R;
import com.openxu.cview.chart.bean.ChartLable;
import com.openxu.cview.chart.bean.PieChartBean;
import com.openxu.utils.DensityUtil;
import com.openxu.utils.LogUtil;
import com.openxu.utils.SharedData;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
/**
* autour : openXu
* date : 2017/7/24 10:46
* className : PieChartLayout
* version : 1.0
* description : 占比饼状图表
*/
public class PieChartLayout extends LinearLayout {
private String TAG = "PieChartLayout";
protected boolean isLoading = true;
protected boolean debug = SharedData.getInstance().getSp().getBoolean(SharedData.KEY_DEBUG, false);
protected List<PieChartBean> dataList;
protected List<ChartLable> lableList;
private PieChart chartView;
/**饼状图设置属性*/
private boolean showZeroPart = false; //如果某部分占比为0, 是否显示
private int centerLableSpace = DensityUtil.dip2px(getContext(), 1); //中间文字行距
//圆环宽度,如果值>0,则为空心圆环,内环为白色,可以在内环中绘制字
private int ringWidth = DensityUtil.dip2px(getContext(), 20); //圆环宽度
private int lineLenth = DensityUtil.dip2px(getContext(), 20); //指示线长度
private int outSpace = DensityUtil.dip2px(getContext(), 5);
private int textSpace = DensityUtil.dip2px(getContext(), 3); //tag指示文字与线的距离
private int tagTextSize = (int)getResources().getDimension(R.dimen.text_size_level_small); //饼状图占比指示文字大小
private int tagTextColor = getResources().getColor(R.color.text_color_light_gray); //文字颜色,如果为0,则根据扇形颜色一样;
private TAG_MODUL tagModul = TAG_MODUL.MODUL_LABLE; //TAG展示位置
private TAG_TYPE tagType = TAG_TYPE.TYPE_NUM; //TAG展示类型
private PieChartLableView lableView;
/**右侧lable设置属性*/
private int rectW = DensityUtil.dip2px(getContext(), 10); //lable矩形宽高
private int rectH = DensityUtil.dip2px(getContext(), 10);
private int rectRaidus = 0; //矩形圆角
private int rectSpace = DensityUtil.dip2px(getContext(), 8); //右侧标签上下间距
private int leftSpace = DensityUtil.dip2px(getContext(), 5); //右侧标签左右间距
private int lableTextSize = (int)getResources().getDimension(R.dimen.text_size_level_small); //饼状图占比指示文字大小
private int lableTextColor = getResources().getColor(R.color.text_color_light_gray);
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean result = super.dispatchTouchEvent(ev);
LogUtil.w(TAG, "dispatchTouchEvent分发事件 "+result);
return result;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean result = super.onInterceptTouchEvent(ev);
LogUtil.e(TAG, "onInterceptTouchEvent拦截事件 "+result);
return result;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
boolean result = super.onTouchEvent(ev);
LogUtil.i(TAG, "onTouchEvent处理事件 "+result);
return result;
}
//RGB颜色数组
private int arrColorRgb[][] = {
{113, 137, 230}, // UIColorFromRGB(0xD95F5B),
{217, 95, 91}, // UIColorFromRGB(0x7189E6),
{90, 185, 199}, // UIColorFromRGB(0x5AB9C7),
{170, 150, 213}, // UIColorFromRGB(0xB096D5),
{107, 186, 151}, // UIColorFromRGB(0x6BBA97),
{91, 164, 231}, // UIColorFromRGB(0x5BA4E7),
{220, 170, 97},// UIColorFromRGB(0xDCAA61),
{125, 171, 88},// UIColorFromRGB(0x7DAB58),
{233, 200, 88},// UIColorFromRGB(0xE9C858),
{213, 150, 196},// UIColorFromRGB(0xd596c4)
{220, 127, 104},// UIColorFromRGB(0xDC7F68),
// {86, 138, 220}, // UIColorFromRGB(0x568ADC),
// {112, 173, 71}, //UIColorFromRGB(0x70AD47)
// {219, 69, 40}, // UIColorFromRGB(0xDB4528)
// {255, 193, 2}, // UIColorFromRGB(0xFFC102)
// {242, 141, 2}, // UIColorFromRGB(0xF28D02)
// {41, 182, 180}, // UIColorFromRGB(0x29B6B4)
// {187, 107, 201}, // UIColorFromRGB(0xbb6bc9)
// {124, 117, 214}, // UIColorFromRGB(0x7c75d6)
};
/**tag类型*/
public enum TAG_TYPE{
TYPE_NUM, //数量
TYPE_PERCENT, //百分比
}
public enum TAG_MODUL{
MODEUL_NULL, //不展示
MODUL_CHART, //在扇形图上显示tag
MODUL_LABLE, //在lable后面显示tag
}
public PieChartLayout(Context context) {
this(context, null);
}
public PieChartLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public PieChartLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init() {
this.dataList = new ArrayList<>();
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
for(int i = 0; i<getChildCount(); i++){
View child = getChildAt(i);
if(child instanceof PieChart){
chartView = (PieChart)child;
}else if(child instanceof PieChartLableView){
lableView = (PieChartLableView)child;
}
}
LogUtil.i(TAG, "init"+ chartView+ lableView);
setConfig();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
private void setConfig(){
if(chartView!=null) {
chartView.setLoading(isLoading);
chartView.setArrColorRgb(arrColorRgb);
chartView.setTagType(tagType);
chartView.setTagModul(tagModul);
chartView.setTagTextColor(tagTextColor);
chartView.setTagTextSize(tagTextSize);
chartView.setShowZeroPart(showZeroPart);
chartView.setCenterLableSpace(centerLableSpace);
chartView.setRingWidth(ringWidth);
chartView.setLineLenth(lineLenth);
chartView.setOutSpace(outSpace);
chartView.setTextSpace(textSpace);
chartView.setData(dataList, lableList);
}
if(lableView!=null) {
lableView.setLoading(isLoading);
lableView.setTagType(tagType);
lableView.setTagModul(tagModul);
lableView.setShowZeroPart(showZeroPart);
lableView.setArrColorRgb(arrColorRgb);
lableView.setTextColor(lableTextColor);
lableView.setTextSize(lableTextSize);
lableView.setRectW(rectW);
lableView.setRectH(rectH);
lableView.setRectRaidus(rectRaidus);
lableView.setRectSpace(rectSpace);
lableView.setLeftSpace(leftSpace);
lableView.setData(dataList);
}
}
public int getTotal() {
return chartView.getTotal();
}
public void setLoading(boolean loading) {
this.isLoading = loading;
setConfig();
}
public void setShowZeroPart(boolean showZeroPart) {
this.showZeroPart = showZeroPart;
}
public void setCenterLableSpace(int centerLableSpace) {
this.centerLableSpace = centerLableSpace;
}
public void setTagType(TAG_TYPE tagType) {
this.tagType = tagType;
}
public void setTagModul(TAG_MODUL tagModul) {
this.tagModul = tagModul;
}
public void setRingWidth(int ringWidth) {
this.ringWidth = ringWidth;
}
public void setLineLenth(int lineLenth) {
this.lineLenth = lineLenth;
}
public void setOutSpace(int outSpace) {
this.outSpace = outSpace;
}
public void setTextSpace(int textSpace) {
this.textSpace = textSpace;
}
public void setRectW(int rectW) {
this.rectW = rectW;
}
public void setRectH(int rectH) {
this.rectH = rectH;
}
public void setRectRaidus(int rectRaidus) {
this.rectRaidus = rectRaidus;
}
public void setRectSpace(int rectSpace) {
this.rectSpace = rectSpace;
}
public void setLeftSpace(int leftSpace) {
this.leftSpace = leftSpace;
}
public void setTagTextSize(int tagTextSize) {
this.tagTextSize = tagTextSize;
}
public void setTagTextColor(int tagTextColor) {
this.tagTextColor = tagTextColor;
}
public void setLableTextSize(int lableTextSize) {
this.lableTextSize = lableTextSize;
}
public void setLableTextColor(int lableTextColor) {
this.lableTextColor = lableTextColor;
}
public void setArrColorRgb(int[][] arrColorRgb) {
this.arrColorRgb = arrColorRgb;
}
/**
* 设置数据
*/
public void setChartData(Class clazz, String per, String name, List<? extends Object> dataList, List<ChartLable> lableList){
this.lableList = lableList;
this.dataList.clear();
if(dataList!=null){
try{
Field filedPer = clazz.getDeclaredField(per);
Field filedName = clazz.getDeclaredField(name);
filedPer.setAccessible(true);
filedName.setAccessible(true);
for(Object obj : dataList){
String perStr = filedPer.get(obj).toString();
PieChartBean bean = new PieChartBean(Float.parseFloat(perStr), (String)filedName.get(obj));
this.dataList.add(bean);
}
}catch (Exception e){
e.printStackTrace();
}
}
setConfig();
}
}
@@ -0,0 +1,638 @@
package com.openxu.cview.chart.rosechart;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathEffect;
import android.graphics.PointF;
import android.graphics.RectF;
import android.graphics.Region;
import android.graphics.Typeface;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import com.openxu.cview.R;
import com.openxu.cview.chart.anim.AngleEvaluator;
import com.openxu.utils.DensityUtil;
import com.openxu.utils.FontUtil;
import com.openxu.utils.LogUtil;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
/**
* autour : openXu
* date : 2017/7/24 10:46
* className : NightingaleRoseChart
* version : 1.0
* description : 南丁格尔玫瑰图
*/
public class NightingaleRoseChart extends View {
private String TAG = "NightingaleRoseChart";
private int ScrWidth,ScrHeight;
private int backColor = Color.WHITE;
private PointF centerPoint; //chart中心点坐标
private int startAngle = -90; //开始的角度
private int chartRaidusInner = DensityUtil.dip2px(getContext(), 15); //内圈半径
private int chartRaidusOuter; //最大部分扇形区域半径
private int outSpace = DensityUtil.dip2px(getContext(), 30); //图表与边界距离
private int lineLenth = DensityUtil.dip2px(getContext(), 8);
private int lineWidth = 1; //线宽度
private RectF rectChart, rectLable;
private int rightLableTop;
private int oneLableHeight; //右侧标签单个高度
private int rightRectItemW = DensityUtil.dip2px(getContext(), 16);
private int rightRectItemH = DensityUtil.dip2px(getContext(), 8);
private int rightRectSpace = DensityUtil.dip2px(getContext(), 8); //右侧标签上下间距
private int textSpace = DensityUtil.dip2px(getContext(), 5);
private int lableTextSize = (int)getResources().getDimension(R.dimen.text_size_level_mid); //右侧标注字体大小
private int tagTextSize = (int)getResources().getDimension(R.dimen.text_size_level_small);
private String nullStr = "暂无数据";
private List<RoseChartBean> dataList;
private float max;
private Paint paintArc , paintSelected;
private Paint paintLabel;
private Paint mLinePaint, mlableLinePaint;
private int selectedIndex = -1; //被选中的索引
/**正在加载*/
boolean isLoading = true;
/**是否在图表上显示指示lable*/
boolean showChartLable = false;
/**是否在图表上显示指示num*/
boolean showChartNum = true;
/**点击显示数量*/
boolean showNumTouched = false;
/**右侧显示数量*/
boolean showRightNum = false;
//RGB颜色数组
//#D95F5B红 #7189E6蓝 #5AB9C7蓝1 #B096D5紫 #6BBA97绿1 #DCAA61黄 #7DAB58绿2 #DC7F68橙
private final int arrColorRgb[][] = {
{113, 137, 230}, // UIColorFromRGB(0xD95F5B),
{217, 95, 91}, // UIColorFromRGB(0x7189E6),
{90, 185, 199}, // UIColorFromRGB(0x5AB9C7),
{170, 150, 213}, // UIColorFromRGB(0xB096D5),
{107, 186, 151}, // UIColorFromRGB(0x6BBA97),
{91, 164, 231}, // UIColorFromRGB(0x5BA4E7),
{220, 170, 97},// UIColorFromRGB(0xDCAA61),
{125, 171, 88},// UIColorFromRGB(0x7DAB58),
{233, 200, 88},// UIColorFromRGB(0xE9C858),
{213, 150, 196},// UIColorFromRGB(0xd596c4)
{220, 127, 104}// UIColorFromRGB(0xDC7F68),
};
public void setLoading(boolean loading) {
isLoading = loading;
}
public void setShowNumTouched(boolean showNumTouched) {
this.showNumTouched = showNumTouched;
}
public void setShowRightNum(boolean showRightNum) {
this.showRightNum = showRightNum;
}
public void setChartRaidusInner(int chartRaidusInner) {
this.chartRaidusInner = DensityUtil.dip2px(getContext(), chartRaidusInner); //内圈半径
}
public void setChartRaidusOuter(int chartRaidusOuter) {
this.chartRaidusOuter = DensityUtil.dip2px(getContext(), chartRaidusOuter); //图表与边界距离
}
public NightingaleRoseChart(Context context) {
this(context, null);
}
public NightingaleRoseChart(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public NightingaleRoseChart(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context, attrs, defStyle);
}
public void init(Context context, AttributeSet attrs, int defStyleAttr) {
dataList = new ArrayList<>();
DisplayMetrics dm = getResources().getDisplayMetrics();
ScrHeight = dm.heightPixels;
ScrWidth = dm.widthPixels;
//画笔初始化
paintArc = new Paint();
paintArc.setAntiAlias(true);
paintLabel = new Paint();
paintLabel.setAntiAlias(true);
paintSelected = new Paint();
paintSelected.setColor(Color.LTGRAY);
paintSelected.setStyle(Paint.Style.STROKE);//设置空心
paintSelected.setStrokeWidth(lineWidth*5);
paintSelected.setAntiAlias(true);
mLinePaint = new Paint();
mLinePaint.setStyle(Paint.Style.FILL);
mLinePaint.setStrokeWidth(lineWidth);
mLinePaint.setAntiAlias(true);
mlableLinePaint = new Paint();
mlableLinePaint.setStyle(Paint.Style.STROKE);
mlableLinePaint.setColor(Color.DKGRAY);
mlableLinePaint.setStrokeWidth(3);
// PathEffect是用来控制绘制轮廓(线条)的方式
// 代码中的float数组,必须是偶数长度,且>=2,指定了多少长度的实线之后再画多少长度的空白
// .如本代码中,绘制长度5的实线,再绘制长度5的空白,再绘制长度5的实线,再绘制长度5的空白,依次重复.1是偏移量,可以不用理会.
PathEffect effects = new DashPathEffect(new float[]{5,5,5,5},1);
mlableLinePaint.setPathEffect(effects);
}
public void setData(Class clazz, String per, String name, List<Object> dataList){
this.dataList.clear();
max = 0;
LogUtil.i(TAG, "玫瑰图设置数据"+dataList);
if(dataList!=null){
try{
Field filedPer = clazz.getDeclaredField(per);
Field filedName = clazz.getDeclaredField(name);
filedPer.setAccessible(true);
filedName.setAccessible(true);
for(Object obj : dataList){
String perStr = filedPer.get(obj).toString();
this.dataList.add(new RoseChartBean(Float.parseFloat(perStr), (String)filedName.get(obj) ));
}
}catch (Exception e){
e.printStackTrace();
}
for(RoseChartBean bean : this.dataList){
max = max>bean.getPer()?max:bean.getPer();
}
if(getMeasuredHeight()>0){
evaluatorLable();
}
}
Log.i(TAG, "玫瑰图设置数据dataList"+this.dataList);
startDraw = false;
invalidate();
}
boolean startDraw = false;
public void setData(List<RoseChartBean> dataList){
this.dataList.clear();
max = 0;
if(dataList!=null){
this.dataList.addAll(dataList);
for(RoseChartBean bean : dataList){
max = max>bean.getPer()?max:bean.getPer();
}
evaluatorLable();
}
startDraw = false;
invalidate();
}
/**计算右侧标签相关坐标值*/
private void evaluatorLable(){
paintLabel.setTextSize(lableTextSize);
oneLableHeight = (int) FontUtil.getFontHeight(paintLabel);
//字和矩形中高度的较大值
oneLableHeight = oneLableHeight>rightRectItemH?oneLableHeight:rightRectItemH;
int allHeight = (oneLableHeight+rightRectSpace)*dataList.size() +rightRectSpace;
LogUtil.e(TAG, "测量高度"+getMeasuredHeight()+" allHeight="+allHeight);
if(allHeight>getMeasuredHeight()){
//超出总高度了
oneLableHeight = getMeasuredHeight() / ((dataList==null || dataList.size()<=0)?1:dataList.size());
rightLableTop = -1;
}else{
rightLableTop = (getMeasuredHeight()-allHeight)/2;
}
}
public void setShowChartLable(boolean showChartLable) {
this.showChartLable = showChartLable;
}
public void setShowChartNum(boolean showChartNum) {
this.showChartNum = showChartNum;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int height;
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else {
height = ScrWidth/3 * 2; //3分之2的位置画图,3分1的位置画标注
}
int chartWidth = width/3*2;
int chartSize = chartWidth>height?height:chartWidth;
//外圈半径=size/2-最大部分lable线长度-边距
chartRaidusOuter = chartSize/2-lineLenth-outSpace;
centerPoint = new PointF(chartWidth/2, height/2);
rectChart = new RectF(0,0,chartWidth, height);
rectLable = new RectF(chartWidth,0,width, height);
setMeasuredDimension(width, height);
if(dataList.size()>0){
evaluatorLable();
}
LogUtil.i(TAG, "图表总宽高="+width+"*"+height);
LogUtil.i(TAG, "chart宽高="+chartWidth+"*"+height);
LogUtil.i(TAG, "chart直径="+chartSize);
LogUtil.i(TAG, "lineLenth="+lineLenth+" outSpace="+outSpace);
LogUtil.i(TAG, "chartRaidusOuter="+chartRaidusOuter);
LogUtil.i(TAG, "centerPoint="+centerPoint.x+"*"+ centerPoint.y);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
PointF point = new PointF(event.getX(),event.getY());
sureSelectedIndex(point);
break;
case MotionEvent.ACTION_MOVE:
// LogUtil.w(TAG, "长按后的移动时间");
point = new PointF(event.getX(),event.getY());
sureSelectedIndex(point);
break;
case MotionEvent.ACTION_UP:
// selectedIndex = -1;
invalidate();
break;
}
return super.onTouchEvent(event);
}
private boolean sureSelectedIndex(PointF point){
for(int i = 0; i < dataList.size(); i++) {
RoseChartBean bean = dataList.get(i);
if(bean.getRegion()!=null && bean.getRegion().contains((int)point.x, (int)point.y)){
// LogUtil.e(TAG, "在第"+i+"个扇形区域");
selectedIndex = i;
invalidate();
return true;
}
if(bean.getRectLable()!=null && bean.getRectLable().contains((int)point.x, (int)point.y)){
// LogUtil.e(TAG, "在第"+i+"个lable区域");
selectedIndex = i;
invalidate();
return true;
}
}
return false;
}
public void onDraw(Canvas canvas){
//画布背景
canvas.drawColor(backColor);
if(isLoading){
paintLabel.setTextSize(lableTextSize);
float NullTextLead = FontUtil.getFontLeading(paintLabel);
float NullTextHeight = FontUtil.getFontHeight(paintLabel);
float textY = centerPoint.y-NullTextHeight/2+NullTextLead;
paintLabel.setColor(getContext().getResources().getColor(R.color.text_color_def));
canvas.drawText("loading...", centerPoint.x- FontUtil.getFontlength(paintLabel, "loading...")/2, textY, paintLabel);
return;
}
if(dataList==null || dataList.size()<=0) {
// paintArc.setStyle(Paint.Style.STROKE);//设置空心
// canvas.drawCircle(centerPoint.x, centerPoint.y, chartRaidusOuter, paintArc);
paintLabel.setTextSize(lableTextSize);
float NullTextLead = FontUtil.getFontLeading(paintLabel);
float NullTextHeight = FontUtil.getFontHeight(paintLabel);
float textY = centerPoint.y-NullTextHeight/2+NullTextLead;
canvas.drawText(nullStr, centerPoint.x- FontUtil.getFontlength(paintLabel, nullStr)/2, textY, paintLabel);
return;
}
// drawDebug(canvas);
if(!startDraw){
startDraw = true;
startAnimation();
}else{
drawChart(canvas);
}
}
private void drawDebug(Canvas canvas){
paintArc.setStyle(Paint.Style.STROKE);//设置空心
//绘制边界--chart区域
paintArc.setColor(Color.YELLOW);
canvas.drawRect(rectChart, paintArc);
//绘制边界--标签区域
paintArc.setColor(Color.RED);
canvas.drawRect(rectLable, paintArc);
//绘制边界--chart圆边界
paintArc.setColor(Color.BLUE);
canvas.drawCircle(centerPoint.x, centerPoint.y, chartRaidusOuter+outSpace+lineLenth, paintArc);
//绘制边界--chart lable边缘
paintArc.setColor(Color.GRAY);
//绘制边界--chart 扇形边缘
canvas.drawCircle(centerPoint.x, centerPoint.y, chartRaidusOuter+lineLenth, paintArc);
paintArc.setColor(Color.LTGRAY);
canvas.drawCircle(centerPoint.x, centerPoint.y, chartRaidusOuter, paintArc);
}
public int STYLE_1 = 1;
public int STYLE_2 = 2;
private int STYLE = STYLE_2;
public void setAnimStyle(int style){
STYLE = style;
}
private long duration = 1000;
public void setAnimDuration(long duration){
this.duration = duration;
}
ValueAnimator anim;
private void startAnimation() {
if(anim!=null){
anim.cancel();
}
Log.w(TAG, "开始动画");
//将百分比转换为扇形半径长度
float percentage = 360.0f / dataList.size();
anim = ValueAnimator.ofObject(new AngleEvaluator(), 0f, percentage);
anim.setInterpolator(new AccelerateDecelerateInterpolator());
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float percentage = (float) animation.getAnimatedValue();
evaluatorData(percentage);
invalidate();
}
});
anim.setDuration(duration);
anim.start();
}
/**
* 计算各种绘制坐标
* @param percentage 当前扇形度数
*/
private void evaluatorData(float percentage){
paintLabel.setTextSize(tagTextSize);
float chartTextLead = FontUtil.getFontLeading(paintLabel);
float chartTextHeight = FontUtil.getFontHeight(paintLabel);
paintLabel.setTextSize(lableTextSize);
float lableTextLead = FontUtil.getFontLeading(paintLabel);
float lableTextHeight = FontUtil.getFontHeight(paintLabel);
float radius; //每部分扇形半径
for(int i = 0; i < dataList.size(); i++){
RoseChartBean bean = dataList.get(i);
/**1、绘制扇形*/
//将百分比转换为扇区的半径
radius = chartRaidusInner+(chartRaidusOuter-chartRaidusInner)* (bean.getPer()*1.0f / max*1.0f);
float arcLeft = centerPoint.x - radius;
float arcTop = centerPoint.y - radius ;
float arcRight = centerPoint.x + radius ;
float arcBottom = centerPoint.y + radius ;
float oneStartAngle = startAngle + i * percentage;
if(STYLE == STYLE_1){
oneStartAngle = startAngle + i * percentage;
}else{
//将百分比转换为扇形半径长度
float partPercentage = 360.0f / dataList.size();
oneStartAngle = startAngle + i * partPercentage;
}
bean.setArcRect(new RectF(arcLeft ,arcTop, arcRight, arcBottom));
bean.setStartAngle(oneStartAngle);
bean.setSweepAngle(percentage);
/**计算扇形区域*/
float Allradius = chartRaidusOuter+ lineLenth + outSpace;
arcLeft = centerPoint.x - Allradius;
arcTop = centerPoint.y - Allradius;
arcRight = centerPoint.x + Allradius;
arcBottom = centerPoint.y + Allradius;
Path allPath = new Path();
allPath.moveTo(centerPoint.x, centerPoint.y);//添加原始点
float ovalX = centerPoint.x+(float) (radius* Math.cos(Math.toRadians(oneStartAngle)));
float ovalY = centerPoint.y+(float) (radius* Math.sin(Math.toRadians(oneStartAngle)));
allPath.lineTo(ovalX, ovalY);
RectF touchOval = new RectF(arcLeft, arcTop, arcRight, arcBottom);
allPath.addArc(touchOval, oneStartAngle, percentage);
allPath.lineTo(centerPoint.x, centerPoint.y);
allPath.close();
RectF r = new RectF();
allPath.computeBounds(r, true);
Region region = new Region();
region.setPath(allPath, new Region((int)r.left, (int) r.top, (int) r.right,(int)r.bottom));
bean.setRegion(region);
/**2、绘制直线*/
//确定直线的起始和结束的点的位置
float startX = centerPoint.x + (float) (radius * Math.cos(Math.toRadians(oneStartAngle + percentage / 2)));
float startY = centerPoint.y + (float) (radius * Math.sin(Math.toRadians(oneStartAngle + percentage / 2)));
float endX = centerPoint.x + (float) ((chartRaidusOuter + lineLenth) * Math.cos(Math.toRadians(oneStartAngle + percentage / 2)));
float endY = centerPoint.y + (float) ((chartRaidusOuter + lineLenth) * Math.sin(Math.toRadians(oneStartAngle + percentage / 2)));
boolean isRight = true;
float lineAngle = startAngle + i * percentage + percentage / 2;
if (lineAngle > 90 && lineAngle < 270) {
isRight = false;
}
// LogUtil.i(TAG, "直线坐标:start=("+startX+","+startY +") end=("+endX+","+endY+")"+" lineAngle="+lineAngle+" isRight="+isRight);
List<PointF> tagLinePoints = new ArrayList<>();
tagLinePoints.add(new PointF(startX, startY));
tagLinePoints.add(new PointF(endX, endY));
float textX = isRight ? (endX + 20) : (endX - 20);
tagLinePoints.add(new PointF(textX, endY));
bean.setTagLinePoints(tagLinePoints);
/**3、绘制指示标签*/
paintLabel.setTextSize(tagTextSize);
String lableText = "";
if(showChartLable){
lableText = bean.getName();
}else if(showChartNum){
lableText = bean.getPer()+"";
}
if(showNumTouched){
lableText = bean.getPer()+"";
}
//标签字体长度
float textW = FontUtil.getFontlength(paintLabel, lableText);
//字体绘制X坐标
textX = isRight ? textX : (textX - textW);
float textY = endY - chartTextHeight / 2 + chartTextLead;
bean.setTagTextPoint(new PointF(textX, textY));
/**4、绘制右侧item矩形*/
float rectL, rectT, rectR, centerY, DashPathL;
if(rightLableTop<0){
//超出总高度了
bean.setRectLable(new RectF(rectLable.left, i * oneLableHeight, rectLable.right, (i+1) * oneLableHeight));
rectL = rectLable.left+textSpace;
rectT = i * oneLableHeight + (oneLableHeight-rightRectItemH)/2;
rectR = rectL + rightRectItemW;
centerY = i * oneLableHeight + oneLableHeight/2.0f;
RectF colorRect = new RectF(rectL, rectT, rectR, rectT+ rightRectItemH);
bean.setColorRect(colorRect);
}else{
rectT = rightLableTop +rightRectSpace+ i * (oneLableHeight+rightRectSpace);
bean.setRectLable(new RectF(rectLable.left, rectT, rectLable.right, rectT+oneLableHeight));
centerY = rectT + oneLableHeight/2.0f;
rectL = rectLable.left+textSpace;
rectT = rectT + (oneLableHeight-rightRectItemH)/2;
rectR = rectL + rightRectItemW;
RectF colorRect = new RectF(rectL, rectT, rectR, rectT+ rightRectItemH);
bean.setColorRect(colorRect);
}
/**5、绘制指示标签*/
paintLabel.setTextSize(lableTextSize);
//标签字体长度
textW = FontUtil.getFontlength(paintLabel, bean.getName());
//字体绘制X坐标
rectL = rectR+textSpace;
textY = centerY -lableTextHeight/2 +lableTextLead;
bean.setNameTextPoint(new PointF(rectL, textY));
DashPathL = rectL + textW + textSpace;
/**6.绘制占比*/
textW = FontUtil.getFontlength(paintLabel, bean.getPer()+"");
rectL = rectLable.right - textW - textSpace;
bean.setPerTextPoint(new PointF(rectL, textY));
/**7、绘制虚线*/
if(DashPathL<(rectL-textSpace)){
bean.setDashPathPointStart(new PointF(DashPathL, centerY));
bean.setDashPathPointEnd(new PointF(rectL-textSpace, centerY));
}
}
}
private void drawChart(Canvas canvas){
paintArc.setStyle(Paint.Style.FILL);//设置实心
for(int i = 0; i < dataList.size(); i++){
paintArc.setARGB(255, arrColorRgb[i%arrColorRgb.length][0], arrColorRgb[i%arrColorRgb.length][1], arrColorRgb[i%arrColorRgb.length][2]);
mLinePaint.setARGB(255, arrColorRgb[i%arrColorRgb.length][0],
arrColorRgb[i%arrColorRgb.length][1], arrColorRgb[i%arrColorRgb.length][2]);
paintLabel.setARGB(255, arrColorRgb[i%arrColorRgb.length][0],
arrColorRgb[i%arrColorRgb.length][1], arrColorRgb[i%arrColorRgb.length][2]);
if(selectedIndex == i){
paintLabel.setTypeface(Typeface.DEFAULT_BOLD);
mLinePaint.setTypeface(Typeface.DEFAULT_BOLD);
}else{
paintLabel.setTypeface(Typeface.DEFAULT);
mLinePaint.setTypeface(Typeface.DEFAULT);
}
paintLabel.setTextSize(tagTextSize);
RoseChartBean bean = dataList.get(i);
/**1、绘制扇形*/
canvas.drawArc(bean.getArcRect(), bean.getStartAngle(), bean.getSweepAngle(), true, paintArc);
// canvas.drawPath(allPath,mLinePaint);
if(selectedIndex == i){
//被选中的,绘制边界
paintSelected.setStyle(Paint.Style.STROKE);//设置空心
canvas.drawArc(bean.getArcRect(), bean.getStartAngle(), bean.getSweepAngle(), true, paintSelected);
}
if(showChartLable || showChartNum){
/**2、绘制直线*/
List<PointF> tagLinePoints = bean.getTagLinePoints();
if (tagLinePoints != null && tagLinePoints.size() > 0) {
for (int p = 1; p < tagLinePoints.size(); p++) {
canvas.drawLine(tagLinePoints.get(p - 1).x, tagLinePoints.get(p - 1).y,
tagLinePoints.get(p).x, tagLinePoints.get(p).y, mLinePaint);
}
}
if(showChartLable){
/**3、绘制指示标签*/
canvas.drawText(bean.getName(), bean.getTagTextPoint().x, bean.getTagTextPoint().y, paintLabel);
}else if(showChartNum){
/**3、绘制指示标签*/
canvas.drawText(bean.getPer()+"", bean.getTagTextPoint().x, bean.getTagTextPoint().y, paintLabel);
}
}
if(showNumTouched && selectedIndex == i) {
/**2、绘制直线*/
List<PointF> tagLinePoints = bean.getTagLinePoints();
if (tagLinePoints != null && tagLinePoints.size() > 0) {
for (int p = 1; p < tagLinePoints.size(); p++) {
canvas.drawLine(tagLinePoints.get(p - 1).x, tagLinePoints.get(p - 1).y,
tagLinePoints.get(p).x, tagLinePoints.get(p).y, mLinePaint);
}
}
/**3、绘制数量*/
canvas.drawText(bean.getPer()+"", bean.getTagTextPoint().x, bean.getTagTextPoint().y, paintLabel);
LogUtil.i(TAG, "绘制bean="+bean.getName()+" "+bean.getPer());
}
/**4、绘制右侧item矩形*/
canvas.drawRoundRect(bean.getColorRect(), 8, 8, paintArc);//第二个参数是x半径,第三个参数是y半径
// LogUtil.i(TAG, "绘制lable矩形"+bean.getColorRect());
if(selectedIndex == i){
canvas.drawRoundRect(bean.getColorRect(), 8, 8, paintSelected);
}
/**5、绘制指示标签*/
paintLabel.setTextSize(lableTextSize);
paintLabel.setColor(getContext().getResources().getColor(R.color.text_color_def));
canvas.drawText(bean.getName(), bean.getNameTextPoint().x, bean.getNameTextPoint().y , paintLabel);
if(showRightNum){
/**6.绘制占比*/
canvas.drawText(bean.getPer()+"", bean.getPerTextPoint().x, bean.getPerTextPoint().y , paintLabel);
/**7、绘制虚线*/
if(bean.getDashPathPointStart()!=null){
Path path = new Path();
path.moveTo(bean.getDashPathPointStart().x,bean.getDashPathPointStart().y);
path.lineTo( bean.getDashPathPointEnd().x,bean.getDashPathPointEnd().y);
canvas.drawPath(path, mlableLinePaint);
}
}
}
//绘制中心内圆
paintArc.setColor(backColor);
canvas.drawCircle(centerPoint.x, centerPoint.y, chartRaidusInner, paintArc);
}
}
@@ -0,0 +1,162 @@
package com.openxu.cview.chart.rosechart;
import android.graphics.PointF;
import android.graphics.RectF;
import android.graphics.Region;
import java.util.List;
/**
* autour : openXu
* date : 2017/7/24 11:04
* className : RoseChartBean
* version : 1.0
* description : 南丁格尔玫瑰图数据
*/
public class RoseChartBean {
private float per;
private String name;
//触摸相关
private Region region; //扇形区域--用于判断手指触摸点是否在此范围
private RectF rectLable; //矩形区域--用于判断手指触摸点是否在此范围
//扇形相关
private RectF arcRect;
private float startAngle;
private float sweepAngle;
//tag线段
private List<PointF> tagLinePoints;
private PointF tagTextPoint;
//右侧lable
private RectF colorRect;
private PointF nameTextPoint;
private PointF perTextPoint;
private PointF dashPathPointStart;
private PointF dashPathPointEnd;
@Override
public String toString() {
return "RoseChartBean{" +
"per=" + per +
", name='" + name + '\'' +
'}';
}
public RoseChartBean(float per, String name) {
this.per = per;
this.name = name;
}
public PointF getDashPathPointStart() {
return dashPathPointStart;
}
public void setDashPathPointStart(PointF dashPathPointStart) {
this.dashPathPointStart = dashPathPointStart;
}
public PointF getDashPathPointEnd() {
return dashPathPointEnd;
}
public void setDashPathPointEnd(PointF dashPathPointEnd) {
this.dashPathPointEnd = dashPathPointEnd;
}
public PointF getNameTextPoint() {
return nameTextPoint;
}
public void setNameTextPoint(PointF nameTextPoint) {
this.nameTextPoint = nameTextPoint;
}
public PointF getPerTextPoint() {
return perTextPoint;
}
public void setPerTextPoint(PointF perTextPoint) {
this.perTextPoint = perTextPoint;
}
public RectF getColorRect() {
return colorRect;
}
public void setColorRect(RectF colorRect) {
this.colorRect = colorRect;
}
public PointF getTagTextPoint() {
return tagTextPoint;
}
public void setTagTextPoint(PointF tagTextPoint) {
this.tagTextPoint = tagTextPoint;
}
public List<PointF> getTagLinePoints() {
return tagLinePoints;
}
public void setTagLinePoints(List<PointF> tagLinePoints) {
this.tagLinePoints = tagLinePoints;
}
public RectF getArcRect() {
return arcRect;
}
public void setArcRect(RectF arcRect) {
this.arcRect = arcRect;
}
public float getStartAngle() {
return startAngle;
}
public void setStartAngle(float startAngle) {
this.startAngle = startAngle;
}
public float getSweepAngle() {
return sweepAngle;
}
public void setSweepAngle(float sweepAngle) {
this.sweepAngle = sweepAngle;
}
public RectF getRectLable() {
return rectLable;
}
public void setRectLable(RectF rectLable) {
this.rectLable = rectLable;
}
public Region getRegion() {
return region;
}
public void setRegion(Region region) {
this.region = region;
}
public float getPer() {
return per;
}
public void setPer(float per) {
this.per = per;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@@ -0,0 +1,8 @@
package com.openxu.cview;
/**
* Created by Admin on 2018/6/7.
*/
public class sss {
}
@@ -0,0 +1,312 @@
package com.openxu.cview.xmstock;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import com.openxu.cview.R;
import com.openxu.cview.chart.anim.AngleEvaluator;
import com.openxu.cview.xmstock.bean.DataPoint;
import com.openxu.cview.xmstock.bean.FenbuInfo;
import com.openxu.cview.xmstock.bean.FenbuNum;
import com.openxu.utils.DensityUtil;
import com.openxu.utils.FontUtil;
import com.openxu.utils.LogUtil;
import java.util.ArrayList;
import java.util.List;
/**
* autour : xiami
* date : 2018/3/13 14:26
* className : BarChart
* version : 1.0
* description : 股票涨跌分布图
*/
public class BarChart extends BaseChart {
//设置数据
private FenbuInfo fenbuData;
//计算后的数据
private List<DataPoint> itemCenterPointList;
/**可以设置的属性*/
//设置颜色(绿、黄、红)
private int[] barColor = new int[]{
Color.parseColor("#278d3a"),
Color.parseColor("#ec9343"),
Color.parseColor("#d03231")};
//设置柱子宽度
private int barSize = DensityUtil.dip2px(getContext(), 20);
//设置文字大小
private int textSize_X = (int)getResources().getDimension(R.dimen.ts_barchart_x); //X坐标文字大小
private int textSize_Y = (int)getResources().getDimension(R.dimen.ts_barchart_y); //Y坐标文字大小
private int lableTextSize = (int)getResources().getDimension(R.dimen.ts_barchart_lable);
//设置字体距离
private int textSpaceX = DensityUtil.dip2px(getContext(), 5);
private int textSpaceY = DensityUtil.dip2px(getContext(), 6);
private int textSpaceLable = DensityUtil.dip2px(getContext(), 8);
private int textSpaceLableNum = DensityUtil.dip2px(getContext(), 12); //涨停文字与数字之间的间隙(涨停 58家)
//设置坐标文字颜色
private int textColorLable = getResources().getColor(R.color.tc_chart_lable);
private int textColorX = getResources().getColor(R.color.tc_chart_xy);
private float ONE_BAR_WEDTH; //单个柱状占的宽度(包含space)
private int YMARK_MAX = Integer.MIN_VALUE; //Y轴刻度最大值
public BarChart(Context context) {
this(context, null);
}
public BarChart(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public BarChart(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public void init(Context context, AttributeSet attrs, int defStyleAttr) {
touchEnable = true;
}
/***********************************设置属性set方法**********************************/
public void setBarColor(int[] barColor) {
this.barColor = barColor;
}
public void setBarSize(int barSize) {
this.barSize = barSize;
}
/***********************************设置属性set方法over**********************************/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = View.MeasureSpec.getMode(widthMeasureSpec);
int widthSize = View.MeasureSpec.getSize(widthMeasureSpec);
int heightMode = View.MeasureSpec.getMode(heightMeasureSpec);
int heightSize = View.MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(widthSize, heightSize);
evaluatorByData();
invalidate();
}
/***************************************************/
/***************************************************/
/**设置数据*/
public void setData(FenbuInfo fenbuData){
this.fenbuData = fenbuData;
if(null==fenbuData)
return;
if(getMeasuredWidth()>0) {
evaluatorByData();
startDraw = false;
invalidate();
}
}
/**设置数据后,计算相关值*/
private void evaluatorByData(){
if(null==fenbuData)
return;
/**①、计算表体矩形区域*/
paintLabel.setTextSize(lableTextSize);
float lableHeight = FontUtil.getFontHeight(paintLabel);
paintLabel.setTextSize(textSize_Y);
float yMarkHeight = FontUtil.getFontHeight(paintLabel);
paintLabel.setTextSize(textSize_X);
float xLableHeight = FontUtil.getFontHeight(paintLabel);
//图表主体矩形
rectChart = new RectF(getPaddingLeft(),
getPaddingTop() + yMarkHeight + textSpaceY,
getMeasuredWidth()-getPaddingRight(),
getMeasuredHeight() - getPaddingBottom() - lableHeight*2 - textSpaceLable*3
- xLableHeight - textSpaceX);
LogUtil.w(TAG, "表体矩形区域"+rectChart);
/**②、计算item的中点坐标(X轴)*/
List<List<String>> fenbu_line = fenbuData.getFenbu_line();
ONE_BAR_WEDTH = (rectChart.right - rectChart.left)/fenbu_line.size();
itemCenterPointList = new ArrayList<>();
for(int i = 0; i < fenbu_line.size() ; i++){
List<String> group = fenbu_line.get(i);
String valueX = group.get(0);
int valueY = Integer.parseInt(group.get(1));
if(YMARK_MAX<valueY) //计算Y刻度最大值
YMARK_MAX = valueY;
PointF point = new PointF(rectChart.left+ONE_BAR_WEDTH*i+ONE_BAR_WEDTH/2, rectChart.bottom);
LogUtil.w(TAG, valueX+" "+valueY+" point="+point);
itemCenterPointList.add(new DataPoint(valueX, valueY, point));
}
LogUtil.i(TAG, "Y轴YMARK_MAX="+YMARK_MAX +" ONE_BAR_WEDTH="+ONE_BAR_WEDTH);
}
private boolean onFocus = false; //是否正处于触摸焦点状态
private int focusIndex;
@Override
protected void onTouchMoved(PointF point) {
if(null==fenbuData)
return;
onFocus = (null != point);
List<List<String>> fenbu_line = fenbuData.getFenbu_line();
if(null != point && null!=fenbuData && fenbu_line.size()>0) {
//获取焦点对应的数据的索引
focusIndex = (int) ((point.x - rectChart.left) *
fenbu_line.size() / (rectChart.right - rectChart.left));
focusIndex = Math.max(0, Math.min(focusIndex, fenbu_line.size() - 1));
}
invalidate();
}
/**绘制图表基本框架*/
@Override
public void drawDefult(Canvas canvas) {
if(null==fenbuData)
return;
paint.setStyle(Paint.Style.FILL);
paint.setStrokeWidth(lineWidth);
paint.setColor(defColor);
canvas.drawLine(rectChart.left, rectChart.bottom,
rectChart.right, rectChart.bottom, paint);
//绘制下面的文字
FenbuNum numData = fenbuData.getFenbu_num();
paintLabel.setTextSize(lableTextSize);
float doubleTextLength = FontUtil.getFontlength(paintLabel, "上涨");
float downTextLength = FontUtil.getFontlength(paintLabel, numData.getDown()+"家");
float downStopTextLength = FontUtil.getFontlength(paintLabel, numData.getDown_stop()+"家");
float equalTextLength = FontUtil.getFontlength(paintLabel, numData.getEqual()+"家");
float stopTextLength = FontUtil.getFontlength(paintLabel, numData.getStop()+"家");
float centerStart =rectChart.left + (rectChart.right-rectChart.left -
(doubleTextLength+textSpaceLableNum+(downTextLength>downStopTextLength?downTextLength:downStopTextLength)))/2;
float rightStart = rectChart.right-doubleTextLength-textSpaceLableNum-
(equalTextLength>stopTextLength?equalTextLength:stopTextLength);
float lableHeight = FontUtil.getFontHeight(paintLabel);
float lableLead = FontUtil.getFontHeight(paintLabel);
//第一行
float top = getMeasuredHeight() - getPaddingBottom() - (lableHeight+textSpaceLable)*2;
paintLabel.setColor(textColorLable);
canvas.drawText("上涨", rectChart.left,top + lableLead, paintLabel);
paintLabel.setColor(barColor[2]);
canvas.drawText(numData.getUp()+"家",
rectChart.left+doubleTextLength+textSpaceLableNum,top + lableLead, paintLabel);
paintLabel.setColor(textColorLable);
canvas.drawText("下跌", centerStart,top + lableLead, paintLabel);
paintLabel.setColor(barColor[0]);
canvas.drawText(numData.getDown()+"家",
centerStart+doubleTextLength+textSpaceLableNum,top + lableLead, paintLabel);
paintLabel.setColor(textColorLable);
canvas.drawText("平家", rightStart,top + lableLead, paintLabel);
paintLabel.setColor(barColor[1]);
canvas.drawText(numData.getEqual()+"家",
rightStart+doubleTextLength+textSpaceLableNum,top + lableLead, paintLabel);
//第二行
top = getMeasuredHeight() - getPaddingBottom() - (lableHeight+textSpaceLable);
paintLabel.setColor(textColorLable);
canvas.drawText("涨停", rectChart.left,top + lableLead, paintLabel);
paintLabel.setColor(barColor[2]);
canvas.drawText(numData.getUp_stop()+"家",
rectChart.left+doubleTextLength+textSpaceLableNum,top + lableLead, paintLabel);
paintLabel.setColor(textColorLable);
canvas.drawText("跌停", centerStart,top + lableLead, paintLabel);
paintLabel.setColor(barColor[0]);
canvas.drawText(numData.getDown_stop()+"家",
centerStart+doubleTextLength+textSpaceLableNum,top + lableLead, paintLabel);
paintLabel.setColor(textColorLable);
canvas.drawText("停牌", rightStart,top + lableLead, paintLabel);
paintLabel.setColor(Color.BLACK);
canvas.drawText(numData.getStop()+"家",
rightStart+doubleTextLength+textSpaceLableNum,top + lableLead, paintLabel);
}
/**绘制debug辅助线*/
@Override
public void drawDebug(Canvas canvas) {
super.drawDebug(canvas);
}
/**绘制图表*/
@Override
public void drawChart(Canvas canvas) {
if(null==fenbuData)
return;
paintLabel.setTextSize(textSize_X);
float xLableLead = FontUtil.getFontLeading(paintLabel);
paintLabel.setTextSize(textSize_Y);
float yLableHeight = FontUtil.getFontHeight(paintLabel);
float yLableLead = FontUtil.getFontLeading(paintLabel);
int barColorIndex = 0;
for(int i = 0 ;i<itemCenterPointList.size(); i++){
DataPoint dataPoint = itemCenterPointList.get(i);
if(onFocus && i == focusIndex){
//触摸焦点加粗
Typeface font = Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD);//粗体
paintLabel.setTypeface(font);
}
//①、绘制X轴文字
paintLabel.setTextSize(textSize_X);
paintLabel.setColor(textColorX);
float xLableLength = FontUtil.getFontlength(paintLabel, dataPoint.getValueX());
canvas.drawText(dataPoint.getValueX(), dataPoint.getPoint().x-xLableLength/2,
dataPoint.getPoint().y + textSpaceX + xLableLead, paintLabel);
//②、绘制柱子
barColorIndex = (i==(itemCenterPointList.size()-1)/2)?1:(barColorIndex == 1 || barColorIndex == 2)?2:0;
paint.setColor(barColor[barColorIndex]);
int left = (int)(dataPoint.getPoint().x-barSize/2);
int top = (int)((rectChart.top+
(rectChart.bottom-rectChart.top)-
(rectChart.bottom-rectChart.top)/YMARK_MAX*dataPoint.getValueY()*animPro
));
canvas.drawRect(new Rect(left, top,left+barSize, (int)rectChart.bottom), paint);
//③、绘制柱子上的数字
paintLabel.setTextSize(textSize_Y);
paintLabel.setColor(barColor[barColorIndex]);
xLableLength = FontUtil.getFontlength(paintLabel, (int)dataPoint.getValueY()+"");
canvas.drawText((int) dataPoint.getValueY() + "", dataPoint.getPoint().x - xLableLength / 2,
top - textSpaceY - yLableHeight + yLableLead, paintLabel);
Typeface font = Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL);//常规
paintLabel.setTypeface(font);
}
}
private float animPro; //动画计算的占比数量
/**创建动画*/
@Override
protected ValueAnimator initAnim() {
if(null!=fenbuData) {
ValueAnimator anim = ValueAnimator.ofObject(new AngleEvaluator(), 0f, 1f);
anim.setInterpolator(new AccelerateDecelerateInterpolator());
return anim;
}
return null;
}
/**动画值变化之后计算数据*/
@Override
protected void evaluatorData(ValueAnimator animation) {
animPro = (float)animation.getAnimatedValue();
}
}
@@ -0,0 +1,288 @@
package com.openxu.cview.xmstock;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.RectF;
import android.os.Handler;
import android.os.Message;
import android.os.Vibrator;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import com.openxu.cview.R;
import com.openxu.cview.xmstock.bean.Constacts;
import com.openxu.utils.DensityUtil;
import com.openxu.utils.FontUtil;
import com.openxu.utils.LogUtil;
import com.openxu.utils.SharedData;
import static android.content.Context.VIBRATOR_SERVICE;
/**
* autour : xiami
* date : 2018/3/11 9:13
* className : BaseChart
* version : 1.0
* description : 图表基类
*
*
*
*/
public abstract class BaseChart extends View {
protected String TAG;
protected int ScrWidth,ScrHeight; //屏幕宽高
protected RectF rectChart; //图表矩形
protected PointF centerPoint; //chart中心点坐标
protected Paint paint;
protected Paint paintEffect;
protected Paint paintLabel;
protected boolean debug = SharedData.getInstance().getSp().getBoolean(SharedData.KEY_DEBUG, false);
/**可设置属性*/
protected int backColor = Color.WHITE;
//辅助线宽度
protected int lineWidth = DensityUtil.dip2px(getContext(), 0.7f);
//正在加载
protected boolean isLoading = true;
protected String loadingStr = "loading...";
protected int defColor = Color.rgb(220, 220, 220); //底色
protected boolean touchEnable = true; //是否能获取事件
protected boolean onFocus = false; //是否正处于触摸焦点状态
protected int focusPreTime = 500; //获取焦点长按时间
/**动画相关统一属性,也可以设置,需要写set方法*/
protected long animDuration = 1000;
protected ValueAnimator anim;
protected boolean startDraw = false;
public void setLoading(boolean loading) {
isLoading = loading;
invalidate();
}
public void setLineWidth(int lineWidth) {
this.lineWidth = lineWidth;
}
protected Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if(msg.what == 100) {
onFocus = true;
onTouchMoved(lastTouchPoint);
Vibrator vibrator = (Vibrator) getContext().getSystemService(VIBRATOR_SERVICE);
vibrator.vibrate(50);
}
}
};
public BaseChart(Context context) {
this(context, null);
}
public BaseChart(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public BaseChart(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
init(context, attrs, defStyle);
}
public void setBackColor(int backColor) {
this.backColor = backColor;
}
public void init() {
TAG = getClass().getSimpleName();
DisplayMetrics dm = getResources().getDisplayMetrics();
ScrHeight = dm.heightPixels;
ScrWidth = dm.widthPixels;
paint = new Paint();
paint.setAntiAlias(true);
paintEffect = new Paint();
paintEffect.setAntiAlias(true);
paintEffect.setStyle(Paint.Style.FILL);
paintEffect.setStrokeWidth(lineWidth);
paintEffect.setColor(Color.RED);
paintLabel = new Paint();
paintLabel.setAntiAlias(true);
}
public abstract void init(Context context, AttributeSet attrs, int defStyleAttr);
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
centerPoint = new PointF(getMeasuredWidth()/2, getMeasuredHeight()/2);
rectChart = new RectF(getPaddingLeft(),getPaddingTop(),getMeasuredWidth()-getPaddingRight(),
getMeasuredHeight()-getPaddingBottom());
}
protected PointF downPoint = new PointF();
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
downPoint.x = event.getX();
downPoint.y = event.getY();
if(touchEnable) {
getParent().requestDisallowInterceptTouchEvent(true);
handler.sendEmptyMessageDelayed(100, focusPreTime);
}
break;
case MotionEvent.ACTION_MOVE:
if(!onFocus){
if(Math.abs(event.getY()-downPoint.y)>20 &&
Math.abs(event.getY()-downPoint.y)> Math.abs(event.getX() - downPoint.x)) {
getParent().requestDisallowInterceptTouchEvent(false);
handler.removeMessages(100);
onFocus = false;
} else {
getParent().requestDisallowInterceptTouchEvent(true);
}
}else
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
handler.removeMessages(100);
onFocus = false;
LogUtil.e(TAG, "事件结束");
break;
}
return super.dispatchTouchEvent(event);
}
protected PointF lastTouchPoint;
@Override
public boolean onTouchEvent(MotionEvent event) {
if(!touchEnable)
return false;
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
lastTouchPoint = new PointF(event.getX(), event.getY());
if(onFocus)
onTouchMoved(lastTouchPoint);
return true;
case MotionEvent.ACTION_MOVE:
int move = (int)(event.getX() - lastTouchPoint.x);
LogUtil.i(TAG, "MotionEvent.ACTION_MOVE"+move);
lastTouchPoint.x = (int)event.getX();
lastTouchPoint.y = (int)event.getY();
if(onFocus)
onTouchMoved(lastTouchPoint);
return true;
case MotionEvent.ACTION_UP:
lastTouchPoint.x = 0;
lastTouchPoint.y = 0;
onTouchMoved(null);
return true;
}
return true;
}
protected void onTouchMoved(PointF point){
}
public void onDraw(Canvas canvas){
try {
//画布背景
// canvas.drawColor(backColor);
if (debug) {
drawDebug(canvas);
}
drawDefult(canvas);
if (isLoading) {
drawLoading(canvas);
return;
}
if (!startDraw) {
startDraw = true;
startAnimation(canvas);
} else {
drawChart(canvas);
}
}catch (Exception e){
e.printStackTrace();
}
}
public void drawLoading(Canvas canvas) {
paintLabel.setTextSize(35);
float NullTextLead = FontUtil.getFontLeading(paintLabel);
float NullTextHeight = FontUtil.getFontHeight(paintLabel);
float textY = centerPoint.y-NullTextHeight/2+NullTextLead;
paintLabel.setColor(getContext().getResources().getColor(R.color.tc_chart_xy));
canvas.drawText(loadingStr, centerPoint.x- FontUtil.getFontlength(paintLabel, loadingStr)/2, textY, paintLabel);
}
/**绘制图表基本框架*/
public abstract void drawDefult(Canvas canvas);
/**绘制debug辅助线*/
public void drawDebug(Canvas canvas){
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(lineWidth);
//绘制边界--chart区域
paint.setColor(Color.BLUE);
RectF r = new RectF(0,0,getMeasuredWidth(), getMeasuredHeight());
canvas.drawRect(r, paint);
paint.setColor(Color.RED);
canvas.drawRect(rectChart, paint);
}
/**绘制图表*/
public abstract void drawChart(Canvas canvas);
/**创建动画*/
protected abstract ValueAnimator initAnim();
/**动画值变化之后计算数据*/
protected abstract void evaluatorData(ValueAnimator animation);
public void setAnimDuration(long duration){
this.animDuration = duration;
}
protected void startAnimation(Canvas canvas) {
if(anim!=null){
anim.cancel();
}
anim = initAnim();
if(anim==null){
drawChart(canvas);
}else{
anim.setInterpolator(new AccelerateDecelerateInterpolator());
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
evaluatorData(animation);
invalidate();
}
});
anim.setDuration(animDuration);
anim.start();
}
}
}
@@ -0,0 +1,537 @@
package com.openxu.cview.xmstock;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathEffect;
import android.graphics.PointF;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import com.openxu.cview.R;
import com.openxu.cview.chart.anim.AngleEvaluator;
import com.openxu.cview.xmstock.bean.DataPoint;
import com.openxu.cview.xmstock.bean.FocusInfo;
import com.openxu.utils.DensityUtil;
import com.openxu.utils.FontUtil;
import com.openxu.utils.LogUtil;
import com.openxu.utils.NumberFormatUtil;
import java.util.ArrayList;
import java.util.List;
/**
* autour : xiami
* date : 2018/3/13 14:26
* className : LinesChart
* version : 1.0
* description : 折线图表
*/
public class LinesChart extends BaseChart {
//设置数据
private List<List<String>> dataList;
private String[] lableXList = new String[]{"09:30", "10:30", "11:30/13:00", "14:00", "15:00"};
private int dataNumCount = 60 * 4; //60*4 一分钟一个,四小时
//计算后的数据
private List<DataPoint> lableXPointList;
private List<ArrayList<DataPoint>> linePointList;
/**可以设置的属性*/
//设置Y轴刻度数量
private int YMARK_NUM = 5;
private YMARK_TYPE yMarkType = YMARK_TYPE.INTEGER;
//设置线条颜色
private int[] lineColor = new int[]{Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW};
//设置曲线粗细
private int lineSize = DensityUtil.dip2px(getContext(), 1.5f);
//曲线数量
private int LINE_NUM = 0;
//设置坐标文字大小
private int textSize = (int)getResources().getDimension(R.dimen.ts_chart_xy);
//设置坐标文字颜色
private int textColor = getResources().getColor(R.color.tc_chart_xy);
//设置X坐标字体与横轴的距离
private int textSpaceX = DensityUtil.dip2px(getContext(), 5);
//设置Y坐标字体与横轴的距离
private int textSpaceY = DensityUtil.dip2px(getContext(), 3);
//设置动画类型
private AnimType animType = AnimType.SLOW_DRAW;
//设置焦点线颜色 及 粗细
private int focusLineColor = getResources().getColor(R.color.tc_chart_focus_line);
private int focusLineSize = DensityUtil.dip2px(getContext(), 0.8f);
public enum YMARK_TYPE{
PERCENTAGE, /*百分比*/
INTEGER
}
public enum AnimType{
LEFT_TO_RIGHT, //动画从左往右
BOTTOM_TO_TOP, //动画从下网上上升
SLOW_DRAW //动画缓慢绘制
}
/**需要计算相关值*/
private float lableLead, lableHeight;
/*字体绘制相关*/
private float YMARK = 1; //Y轴刻度间隔
private float YMARK_MAX = Float.MIN_VALUE; //Y轴刻度最大值
private float YMARK_MIN = Float.MAX_VALUE; //Y轴刻度最小值
public LinesChart(Context context) {
this(context, null);
}
public LinesChart(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public LinesChart(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public void init(Context context, AttributeSet attrs, int defStyleAttr) {
dataList = new ArrayList<>();
touchEnable = true;
}
public void setYMARK_NUM(int YMARK_NUM) {
this.YMARK_NUM = YMARK_NUM;
}
public void setyMarkType(YMARK_TYPE yMarkType) {
this.yMarkType = yMarkType;
}
/***********************************设置属性set方法**********************************/
/**设置数据的数量,默认是60*4*/
public void setDataNumCount(int dataNumCount) {
this.dataNumCount = dataNumCount;
}
public void setLineColor(int[] lineColor) {
this.lineColor = lineColor;
}
public void setAnimType(AnimType animType) {
this.animType = animType;
}
public void setLINE_NUM(int LINE_NUM) {
this.LINE_NUM = LINE_NUM;
}
/***********************************设置属性set方法over**********************************/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = View.MeasureSpec.getMode(widthMeasureSpec);
int widthSize = View.MeasureSpec.getSize(widthMeasureSpec);
int heightMode = View.MeasureSpec.getMode(heightMeasureSpec);
int heightSize = View.MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(widthSize, heightSize);
evaluatorByData();
invalidate();
}
/***************************************************/
/***************************************************/
/**
* 设置数据
* @param dataList 折线数据
* @param lableXList X轴显示的刻度,默认是9:30-15:00(传null)
* 如果需要显示其他的数据,需要重新设置
*/
public void setData(List<List<String>> dataList, String[] lableXList){
if(null==dataList)
return;
this.dataList.clear();
this.dataList.addAll(dataList);
if(null!=lableXList && lableXList.length>0)
this.lableXList = lableXList;
if(getMeasuredWidth()>0) {
evaluatorByData();
startDraw = false;
invalidate();
}
}
/**设置数据后,计算相关值*/
private void evaluatorByData(){
if(dataList.size()<=0)
return;
dataNumCount = dataList.size();
LogUtil.w(TAG, "总共"+dataNumCount+"条数据");
/**①、计算字体相关以及图表原点坐标*/
paintLabel.setTextSize(textSize);
lableHeight = FontUtil.getFontHeight(paintLabel);
lableLead = FontUtil.getFontLeading(paintLabel);
//图表主体矩形
rectChart = new RectF(getPaddingLeft(),getPaddingTop() + lableHeight + textSpaceY,
getMeasuredWidth()-getPaddingRight(),
getMeasuredHeight()-getPaddingBottom() - lableHeight - textSpaceX);
/**②、计算X标签绘制坐标*/
float lableXSpace = 0;
for(String lableX : lableXList){
lableXSpace += FontUtil.getFontlength(paintLabel, lableX);
}
lableXSpace = (rectChart.right - rectChart.left - lableXSpace)/(lableXList.length -1);
lableXPointList = new ArrayList<>();
if(lableXSpace>0){
float left = rectChart.left;
for(int i = 0; i<lableXList.length; i++){
String lableX = lableXList[i];
lableXPointList.add(new DataPoint(lableX, 0, new PointF(
left,rectChart.bottom + textSpaceX + lableLead)));
left += (FontUtil.getFontlength(paintLabel, lableXList[i])+lableXSpace);
}
}else{
//如果X轴标签字体过长的情况需要特殊处理
float oneWidth = (rectChart.right - rectChart.left)/lableXList.length;
for(int i = 0; i<lableXList.length; i++){
String lableX = lableXList[i];
float xLen = FontUtil.getFontlength(paintLabel, lableX);
lableXPointList.add(new DataPoint(lableX, 0, new PointF(
rectChart.left+i*oneWidth+(oneWidth-xLen)/2,rectChart.bottom + textSpaceX + lableLead)));
}
}
/**③、计算Y刻度最大值和最小值以及幅度*/
int lineNum = LINE_NUM==0?(dataList.get(0).size()-1):LINE_NUM;
YMARK_MAX = Float.MIN_VALUE; //Y轴刻度最大值
YMARK_MIN = Float.MAX_VALUE; //Y轴刻度最小值
for(List<String> list : dataList){
for(int i = 1; i< lineNum+1 ; i++){
String str = list.get(i);
try {
if(str.contains("%")){
str = str.substring(0,str.indexOf("%"));//百分数不能直接强转
YMARK = Float.parseFloat(str) /100.0f;
}else{
YMARK = Float.parseFloat(str);
}
// LogUtil.d(TAG, str+" Y轴 被识别为Float = "+YMARK);
if(YMARK>YMARK_MAX)
YMARK_MAX = YMARK;
if(YMARK<YMARK_MIN)
YMARK_MIN = YMARK;
}catch (Exception e){
}
}
}
LogUtil.w(TAG, "Y轴真实YMARK_MIN="+YMARK_MIN+" YMARK_MAX="+YMARK_MAX);
if(YMARK_MAX>0)
YMARK_MAX *= 1.1f;
else
YMARK_MAX /= 1.1f;
if(YMARK_MIN>0)
YMARK_MIN /= 1.1f;
else
YMARK_MIN *= 1.1f;
if(YMARK_MIN>0)
YMARK_MIN = 0;
YMARK = (YMARK_MAX-YMARK_MIN)/(YMARK_NUM - 1);
if(yMarkType == YMARK_TYPE.INTEGER){
YMARK = (int)YMARK + 1;
YMARK_MIN = (int)YMARK_MIN;
YMARK_MAX = YMARK_MIN+YMARK*(YMARK_NUM-1);
}else if(yMarkType == YMARK_TYPE.PERCENTAGE){
}
LogUtil.i(TAG, "Y轴YMARK_MIN="+YMARK_MIN+" YMARK_MAX="+YMARK_MAX+" YMARK="+YMARK);
/**④、计算点的坐标,如果有动画的情况下,边绘制边计算会耗费性能,所以先计算*/
//创建集合,用于存放每条线上每个点的坐标数据
List<String> group = dataList.get(0);
linePointList = new ArrayList<>();
for(int i = 0; i<group.size()-1; i++)
linePointList.add(new ArrayList<DataPoint>());
float oneSpace = (rectChart.right - rectChart.left) / (dataNumCount-1);
List<DataPoint> lastDataGroup = null; //最后一组数据
for(int i = 0; i < dataList.size(); i++){
//一组数据,包含每条线的一个item数据
group = dataList.get(i);
for(int j = 1; j < group.size(); j++){
try {
float valueY;
if(group.get(j).contains("%"))
valueY = Float.parseFloat(group.get(j).substring(0,group.get(j).indexOf("%"))) /100.0f;
else
valueY = Float.parseFloat(group.get(j));
PointF point = new PointF();
//只有需要绘制的线才计算坐标
if(j<lineNum+1){
point.x = rectChart.left + i * oneSpace;
//根据最高价和最低价,计算当前数据在图表上Y轴的坐标
point.y = rectChart.bottom -
(rectChart.bottom-rectChart.top)/(YMARK_MAX - YMARK_MIN) * (valueY-YMARK_MIN);
}
linePointList.get(j-1).add(new DataPoint(group.get(0), valueY, point));
// LogUtil.w(TAG, "绘制"+j+"曲线"+(int)point.x+", "+(int)point.y);
/**默认最后一组数据为显示的lable数据*/
if(null!=onFocusChangeListener && i==dataList.size()-1){
if(null==focusInfo) {
focusInfo = new FocusInfo();
lastDataGroup = new ArrayList<>();
focusInfo.setPoint(point);
focusInfo.setFocusData(lastDataGroup);
}
lastDataGroup.add(new DataPoint(group.get(0), valueY, point));
}
}catch (Exception e){
e.printStackTrace();
}
}
}
if(null!=onFocusChangeListener)
onFocusChangeListener.onfocus(focusInfo);
}
private FocusInfo focusInfo;
@Override
protected void onTouchMoved(PointF point) {
if(null==dataList)
return;
onFocus = (null != point);
if(null != point && null!=dataList && dataList.size()>0) {
//获取焦点对应的数据的索引
int index = (int) ((point.x - rectChart.left) * dataNumCount / (rectChart.right - rectChart.left));
//避免滑出
List<DataPoint> dataPoints = linePointList.get(0);
if(point.x > dataPoints.get(dataPoints.size()-1).getPoint().x)
point.x = dataPoints.get(dataPoints.size()-1).getPoint().x;
if(point.x < dataPoints.get(0).getPoint().x)
point.x = dataPoints.get(0).getPoint().x;
index = Math.max(0, Math.min(index, dataPoints.size() - 1));
focusInfo = new FocusInfo();
focusInfo.setPoint(point);
List<DataPoint> objs = new ArrayList<>();
for(List<DataPoint> points : linePointList){
objs.add(points.get(index));
}
focusInfo.setFocusData(objs);
if(null!=onFocusChangeListener)
onFocusChangeListener.onfocus(focusInfo);
}
invalidate();
}
private OnFocusChangeListener onFocusChangeListener;
public void setOnFocusChangeListener(OnFocusChangeListener onFocusChangeListener) {
this.onFocusChangeListener = onFocusChangeListener;
}
public interface OnFocusChangeListener{
public void onfocus(FocusInfo focusInfo);
}
/**绘制图表基本框架*/
@Override
public void drawDefult(Canvas canvas) {
if(null==linePointList || linePointList.size()<=0)
return;
paint.setStyle(Paint.Style.FILL);
paint.setStrokeWidth(lineWidth);
paint.setColor(defColor);
drawGrid(canvas);
drawXLable(canvas);
drawYLable(canvas);
}
/**绘制debug辅助线*/
@Override
public void drawDebug(Canvas canvas) {
super.drawDebug(canvas);
}
/**绘制图表*/
@Override
public void drawChart(Canvas canvas) {
if(null==linePointList || linePointList.size()<=0)
return;
drawDataPath(canvas);
drawFocus(canvas);
}
/**绘制X轴方向辅助网格*/
private void drawGrid(Canvas canvas){
float yMarkSpace = (rectChart.bottom - rectChart.top)/(YMARK_NUM-1);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(lineWidth);
paint.setColor(defColor);
paintEffect.setStyle(Paint.Style.STROKE);
paintEffect.setStrokeWidth(lineWidth);
paintEffect.setColor(defColor);
canvas.drawLine(rectChart.left, rectChart.top, rectChart.left, rectChart.bottom, paint);
canvas.drawLine(rectChart.right, rectChart.top, rectChart.right, rectChart.bottom, paint);
for (int i = 0; i < YMARK_NUM; i++) {
if(i==0||i ==YMARK_NUM-1) { //实线
canvas.drawLine(rectChart.left, rectChart.bottom-yMarkSpace*i,
rectChart.right, rectChart.bottom-yMarkSpace*i, paint);
} else { //虚线
Path path = new Path();
path.moveTo(rectChart.left, rectChart.bottom-yMarkSpace*i);
path.lineTo(rectChart.right,rectChart.bottom-yMarkSpace*i);
PathEffect effects = new DashPathEffect(new float[]{15,8,15,8},0);
paintEffect.setPathEffect(effects);
canvas.drawPath(path, paintEffect);
}
}
}
/**绘制X轴刻度*/
private void drawXLable(Canvas canvas){
paintLabel.setTextSize(textSize);
paintLabel.setColor(textColor);
for(DataPoint lable : lableXPointList){
canvas.drawText(lable.getValueX(), lable.getPoint().x, lable.getPoint().y, paintLabel);
}
}
/**绘制Y轴刻度*/
private void drawYLable(Canvas canvas){
float yMarkSpace = (rectChart.bottom - rectChart.top)/(YMARK_NUM-1);
paintLabel.setTextSize(textSize);
paintLabel.setColor(textColor);
for (int i = 0; i < YMARK_NUM; i++) {
if(yMarkType == YMARK_TYPE.INTEGER) {
canvas.drawText((int) (YMARK_MIN + i * YMARK) + "", rectChart.left,
rectChart.bottom - yMarkSpace * i - lableHeight - textSpaceY + lableLead, paintLabel);
}else if(yMarkType == YMARK_TYPE.PERCENTAGE){
canvas.drawText(NumberFormatUtil.formattedDecimalToPercentage((YMARK_MIN + i * YMARK), 2), rectChart.left,
rectChart.bottom - yMarkSpace * i - lableHeight - textSpaceY + lableLead, paintLabel);
}
}
}
/**绘制曲线*/
private void drawDataPath(Canvas canvas) {
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setStrokeJoin(Paint.Join.ROUND);
paint.setStrokeWidth(lineSize);
long startTime =System.currentTimeMillis();
int lineNum = LINE_NUM==0?(dataList.get(0).size()-1):LINE_NUM;
Log.w(TAG, "总共:"+lineNum+ " 条线,每条线"+linePointList.get(0).size()+"个数据");
for(int j = 0; j < lineNum; j++){
// for(int j = 0; j < linePointList.size(); j++){
List<DataPoint> lineList = linePointList.get(j);
paint.setColor(lineColor[j]);
//一条一条的绘制
Path path = new Path();
PointF lastPoint = null;
for(int i = 0 ; i<lineList.size(); i++){
DataPoint dataPoint = lineList.get(i);
if(i == 0){
if(animType == AnimType.LEFT_TO_RIGHT){
path.moveTo(rectChart.left+(dataPoint.getPoint().x-rectChart.left)*animPro, dataPoint.getPoint().y);
}else if(animType == AnimType.BOTTOM_TO_TOP){
path.moveTo(dataPoint.getPoint().x, rectChart.bottom-(rectChart.bottom- dataPoint.getPoint().y)* animPro);
}else{
path.moveTo(dataPoint.getPoint().x, dataPoint.getPoint().y);
}
}else{
//quadTo:二阶贝塞尔曲线连接前后两点,这样使得曲线更加平滑
if(animType == AnimType.LEFT_TO_RIGHT){
path.quadTo(rectChart.left+(lastPoint.x-rectChart.left)*animPro, lastPoint.y,
rectChart.left+(dataPoint.getPoint().x-rectChart.left)*animPro , dataPoint.getPoint().y);
}else if(animType == AnimType.BOTTOM_TO_TOP){
path.quadTo(lastPoint.x, rectChart.bottom-(rectChart.bottom-lastPoint.y)* animPro,
dataPoint.getPoint().x, rectChart.bottom-(rectChart.bottom- dataPoint.getPoint().y)* animPro);
}else if(animType == AnimType.SLOW_DRAW){
if(i>lineList.size()*animPro)
break;
path.quadTo(lastPoint.x, lastPoint.y, dataPoint.getPoint().x, dataPoint.getPoint().y);
}else{
path.quadTo(lastPoint.x, lastPoint.y, dataPoint.getPoint().x, dataPoint.getPoint().y);
}
}
lastPoint = dataPoint.getPoint();
}
canvas.drawPath(path, paint);
}
Log.w(TAG, "绘制一次需要:"+(System.currentTimeMillis() - startTime)+ " ms");
}
/**绘制焦点*/
private void drawFocus(Canvas canvas){
if(!onFocus || null==focusInfo)
return;
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.FILL);
paint.setStrokeWidth(focusLineSize);
paint.setColor(focusLineColor);
//竖直线
canvas.drawLine(focusInfo.getPoint().x, rectChart.bottom, focusInfo.getPoint().x, rectChart.top, paint);
/*
paintLabel.setTextSize(focusTextSize);
paintLabel.setColor(focusTextColor);
float focusTextLead = FontUtil.getFontLeading(paintLabel);
float focusTextHeight = FontUtil.getFontHeight(paintLabel);
try {
for (Object obj : focusInfo.getObjs()) {
DataPoint dataPoint = (DataPoint)obj;
//水平参考线
canvas.drawLine(rectChart.left, dataPoint.getPoint().y, rectChart.right, dataPoint.getPoint().y, paint);
//中心点
canvas.drawCircle(dataPoint.getPoint().x, dataPoint.getPoint().y, 2.0f, paint);
//绘制刻度值
String str = "("+ dataPoint.getValueX()+",";
if(yMarkType == YMARK_TYPE.INTEGER) {
str += dataPoint.getValueY()+")";
}else if(yMarkType == YMARK_TYPE.PERCENTAGE){
str += formattedDecimalToPercentage(dataPoint.getValueY())+")";
}
float textLength = FontUtil.getFontlength(paintLabel,str);
if((rectChart.right- dataPoint.getPoint().x)>textLength){
canvas.drawText(str, dataPoint.getPoint().x + 10,
dataPoint.getPoint().y-focusTextHeight+focusTextLead - 10, paintLabel);
}else{
canvas.drawText(str, dataPoint.getPoint().x - 10 - textLength,
dataPoint.getPoint().y-focusTextHeight+focusTextLead - 10, paintLabel);
}
}
}catch (Exception e){
e.printStackTrace();
}
*/
}
private float animPro; //动画计算的占比数量
/**创建动画*/
@Override
protected ValueAnimator initAnim() {
if(dataList.size()>0) {
ValueAnimator anim = ValueAnimator.ofObject(new AngleEvaluator(), 0f, 1f);
anim.setInterpolator(new AccelerateDecelerateInterpolator());
return anim;
}
return null;
}
/**动画值变化之后计算数据*/
@Override
protected void evaluatorData(ValueAnimator animation) {
animPro = (float)animation.getAnimatedValue();
}
}
@@ -0,0 +1,223 @@
package com.openxu.cview.xmstock;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
import com.openxu.cview.R;
import com.openxu.cview.chart.BaseChart;
import com.openxu.cview.xmstock.bean.DataPoint;
import com.openxu.utils.DensityUtil;
import com.openxu.utils.FontUtil;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* autour : xiami
* date : 2018/3/13 14:26
* className : UpDownChart
* version : 1.0
* description : 股票涨跌图
*/
public class LinesLableChart extends BaseChart {
//设置数据
private String timeStr; //时间
private String[] lableArr; //标签
private int columnNum = 1; //展示的列数
//设置线条颜色
private int[] lineColor = new int[]{Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW};
//设置坐标文字大小
private int timeTextSize = (int)getResources().getDimension(R.dimen.ts_chart_time);
private int lableTextSize = (int)getResources().getDimension(R.dimen.ts_chart_lable);
//设置lable文字颜色
private int textColorLable = getResources().getColor(R.color.tc_chart_lable);
private int textColorTime = getResources().getColor(R.color.tc_chart_time);
//设置字体与小圆点的距离
private int textSpace = DensityUtil.dip2px(getContext(), 3); //圆圈 space 上证指数
private int itemSpace = DensityUtil.dip2px(getContext(), 12); //圆圈 上证指数 space 圆圈 上证指数
private int rowsSpace = DensityUtil.dip2px(getContext(), 12); //行距
private int radius = DensityUtil.dip2px(getContext(), 3); //小圆圈半径
/**计算*/
private float lableTextHeight,lableTextLead;
private float timeTextHeight,timeTextLead;
//数据分组
private List<List<DataPoint>> linePointList;
private Map<String, Float> columnMaxLenMap;
public LinesLableChart(Context context) {
this(context, null);
}
public LinesLableChart(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public LinesLableChart(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public void init(Context context, AttributeSet attrs, int defStyleAttr) {
}
/***********************************设置属性set方法**********************************/
public void setColumnNum(int columnNum) {
this.columnNum = columnNum;
}
public void setLineColor(int[] lineColor) {
this.lineColor = lineColor;
}
/***********************************设置属性set方法over**********************************/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
paintLabel.setTextSize(timeTextSize);
timeTextHeight = FontUtil.getFontHeight(paintLabel);
timeTextLead = FontUtil.getFontLeading(paintLabel);
paintLabel.setTextSize(lableTextSize);
lableTextHeight = FontUtil.getFontHeight(paintLabel);
lableTextLead = FontUtil.getFontLeading(paintLabel);
int rowsNum;
if(null==lableArr || lableArr.length<=0){
rowsNum = TextUtils.isEmpty(timeStr)?0:1;
}else
rowsNum = lableArr.length<=columnNum?1:(lableArr.length/columnNum+(lableArr.length%columnNum>0?1:0));
int height = rowsNum==0?
(int)timeTextHeight:
(int)(lableTextHeight>timeTextHeight?lableTextHeight:timeTextHeight)*rowsNum + rowsSpace*(rowsNum-1);
int widthSize = View.MeasureSpec.getSize(widthMeasureSpec);
// LogUtil.w(TAG, "时间文字高度"+timeTextHeight+" lable文字高度"+lableTextHeight);
// LogUtil.w(TAG, "lable总数"+lableArr.length+" 列数"+columnNum+" 行数"+rowsNum);
// LogUtil.w(TAG, "高度"+height);
setMeasuredDimension(widthSize, height+getPaddingTop()+getPaddingBottom());
evaluatorByData();
}
/***************************************************/
/**设置数据*/
public void setData(String[] lableArr, String timeStr){
this.lableArr = lableArr;
this.timeStr = timeStr;
if(getMeasuredWidth()>0) {
evaluatorByData();
startDraw = false;
requestLayout();
}
}
private void evaluatorByData() {
if(null==lableArr || lableArr.length<=0)
return;
//①、分行
linePointList = new ArrayList<>();
List<DataPoint> rowData = null;
int column = 0;
int rowNum = 1;
for(int i = 0; i<lableArr.length; i++){
// LogUtil.i(TAG, lableArr[i]+ " 第"+i+"个lable分组,到第"+rowNum+"行 "+column+"列");
if(column == 0)
rowData = new ArrayList<>();
rowData.add(new DataPoint(lableArr[i], 0, null));
if(column == columnNum-1) {
rowNum ++;
column = 0;
linePointList.add(rowData);
}else
column++;
}
columnMaxLenMap = new HashMap<>();
for(int i = 0; i<columnNum;i++){
columnMaxLenMap.put("c"+i, 0.0f);
}
//②、获取每列的最长字的长度
paintLabel.setTextSize(lableTextSize);
for(int i = 0 ; i< linePointList.size(); i++){ //遍历行
rowData = linePointList.get(i);
for(int j = 0; j< rowData.size(); j++){ //遍历列
float textLength = FontUtil.getFontlength(paintLabel, rowData.get(j).getValueX());
columnMaxLenMap.put("c"+j, textLength>columnMaxLenMap.get("c"+j)?textLength:columnMaxLenMap.get("c"+j));
// LogUtil.w(TAG, "第"+j+"列:"+rowData.get(j).getValueX()+" 文字长度"+textLength);
}
}
}
@Override
public void drawDefult(Canvas canvas) {
}
@Override
public void drawDebug(Canvas canvas) {
super.drawDebug(canvas);
}
/**绘制图表*/
@Override
public void drawChart(Canvas canvas) {
if(null==lableArr || lableArr.length<=0)
return;
float rowHeight = lableTextHeight>timeTextHeight?lableTextHeight:timeTextHeight;
paintLabel.setColor(textColorTime);
//绘制时间
paintLabel.setTextSize(timeTextSize);
canvas.drawText(timeStr, getPaddingRight(),
getPaddingTop() + (rowHeight- timeTextHeight)/2 + timeTextLead, paintLabel);
paintLabel.setTextSize(lableTextSize);
paintLabel.setColor(textColorLable);
float rowTextMaxLen = 0;
for(Float f : columnMaxLenMap.values()){
rowTextMaxLen += f;
}
float left = getMeasuredWidth()-getPaddingRight()-rowTextMaxLen
-(radius*2+textSpace)*columnNum-itemSpace*(columnNum-1);
paint.setStyle(Paint.Style.FILL);
for(int i = 0 ; i< linePointList.size(); i++){ //遍历行
float startX = left;
List<DataPoint> rowData = linePointList.get(i);
for(int j = 0; j< rowData.size(); j++){ //遍历列
//点
paint.setColor(lineColor[i*columnNum+j]);
canvas.drawCircle(startX+radius, getPaddingTop() +
i*(rowHeight+rowsSpace)+rowHeight/2, radius, paint);
startX += (radius*2+textSpace);
//文字
canvas.drawText(rowData.get(j).getValueX(), startX,getPaddingTop() +
i*(rowHeight+rowsSpace)+(rowHeight-lableTextHeight)/2+lableTextLead, paintLabel);
startX += (columnMaxLenMap.get("c"+j)+itemSpace);
}
}
}
@Override
protected ValueAnimator initAnim() {
return null;
}
@Override
protected void evaluatorData(ValueAnimator animation) {
}
}
@@ -0,0 +1,102 @@
package com.openxu.cview.xmstock.bean;
import java.util.List;
/**
* autour : xiami
* date : 2018/3/14 10:54
* className : ChartData
* version : 1.0
* description : 股票数据解析封装
*/
public class ChartData {
private FenbuInfo fenbu; //分布
private TrendInfo trend; //涨跌趋势
private CompareInfo compare;
private ZhabanInfo zhaban;
private List<ZhabanStock> zhaban_stock_list;
private ZhangfuInfo zhangfu; //涨幅
private WeightInfo weight; //权重
private List<ZhangtingStock> zhangting_stock_list;
private List<ZhangtingStock> yesterday_zhangting_stock;
public FenbuInfo getFenbu() {
return fenbu;
}
public void setFenbu(FenbuInfo fenbu) {
this.fenbu = fenbu;
}
public TrendInfo getTrend() {
return trend;
}
public void setTrend(TrendInfo trend) {
this.trend = trend;
}
public CompareInfo getCompare() {
return compare;
}
public void setCompare(CompareInfo compare) {
this.compare = compare;
}
public ZhabanInfo getZhaban() {
return zhaban;
}
public void setZhaban(ZhabanInfo zhaban) {
this.zhaban = zhaban;
}
public List<ZhabanStock> getZhaban_stock_list() {
return zhaban_stock_list;
}
public void setZhaban_stock_list(List<ZhabanStock> zhaban_stock_list) {
this.zhaban_stock_list = zhaban_stock_list;
}
public ZhangfuInfo getZhangfu() {
return zhangfu;
}
public void setZhangfu(ZhangfuInfo zhangfu) {
this.zhangfu = zhangfu;
}
public WeightInfo getWeight() {
return weight;
}
public void setWeight(WeightInfo weight) {
this.weight = weight;
}
public List<ZhangtingStock> getZhangting_stock_list() {
return zhangting_stock_list;
}
public void setZhangting_stock_list(List<ZhangtingStock> zhangting_stock_list) {
this.zhangting_stock_list = zhangting_stock_list;
}
public List<ZhangtingStock> getYesterday_zhangting_stock() {
return yesterday_zhangting_stock;
}
public void setYesterday_zhangting_stock(List<ZhangtingStock> yesterday_zhangting_stock) {
this.yesterday_zhangting_stock = yesterday_zhangting_stock;
}
}
@@ -0,0 +1,30 @@
package com.openxu.cview.xmstock.bean;
/**
* autour : xiami
* date : 2018/3/13 14:59
* className : CompareInfo
* version : 1.0
* description : 涨跌对比
*/
public class CompareInfo {
private CompareLine compare_line;
private String compare_tag;
public CompareLine getCompare_line() {
return compare_line;
}
public void setCompare_line(CompareLine compare_line) {
this.compare_line = compare_line;
}
public String getCompare_tag() {
return compare_tag;
}
public void setCompare_tag(String compare_tag) {
this.compare_tag = compare_tag;
}
}
@@ -0,0 +1,59 @@
package com.openxu.cview.xmstock.bean;
import java.util.List;
/**
* autour : xiami
* date : 2018/3/13 14:59
* className : CompareLine
* version : 1.0
* description : 涨跌对比
*/
public class CompareLine {
private List<List<String>> stock_updown_all; //全部
private List<List<String>> stock_updown_sz; //深市
private List<List<String>> stock_updown_sh; //深沪
private List<List<String>> stock_updown_small; //中小
private List<List<String>> stock_updown_gem; //创业
public List<List<String>> getStock_updown_all() {
return stock_updown_all;
}
public void setStock_updown_all(List<List<String>> stock_updown_all) {
this.stock_updown_all = stock_updown_all;
}
public List<List<String>> getStock_updown_sz() {
return stock_updown_sz;
}
public void setStock_updown_sz(List<List<String>> stock_updown_sz) {
this.stock_updown_sz = stock_updown_sz;
}
public List<List<String>> getStock_updown_sh() {
return stock_updown_sh;
}
public void setStock_updown_sh(List<List<String>> stock_updown_sh) {
this.stock_updown_sh = stock_updown_sh;
}
public List<List<String>> getStock_updown_small() {
return stock_updown_small;
}
public void setStock_updown_small(List<List<String>> stock_updown_small) {
this.stock_updown_small = stock_updown_small;
}
public List<List<String>> getStock_updown_gem() {
return stock_updown_gem;
}
public void setStock_updown_gem(List<List<String>> stock_updown_gem) {
this.stock_updown_gem = stock_updown_gem;
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,47 @@
package com.openxu.cview.xmstock.bean;
import android.graphics.PointF;
/**
* autour : xiami
* date : 2018/3/13 16:44
* className : DataPoint
* version : 1.0
* description : 图表连线上单个点
*/
public class DataPoint {
private String valueX; //x轴 时间
private float valueY; //对应y轴上的值
private PointF point; //绘制的点的坐标
public DataPoint() {
}
public DataPoint(String valueX, float valueY, PointF point) {
this.valueX = valueX;
this.valueY = valueY;
this.point = point;
}
public String getValueX() {
return valueX;
}
public void setValueX(String valueX) {
this.valueX = valueX;
}
public float getValueY() {
return valueY;
}
public void setValueY(float valueY) {
this.valueY = valueY;
}
public PointF getPoint() {
return point;
}
public void setPoint(PointF point) {
this.point = point;
}
}
@@ -0,0 +1,41 @@
package com.openxu.cview.xmstock.bean;
import java.util.List;
/**
* autour : xiami
* date : 2018/3/13 14:59
* className : FenbuInfo
* version : 1.0
* description : 涨跌分布
*/
public class FenbuInfo {
private List<List<String>> fenbu_line;
private FenbuNum fenbu_num;
private String fenbu_tag;
public List<List<String>> getFenbu_line() {
return fenbu_line;
}
public void setFenbu_line(List<List<String>> fenbu_line) {
this.fenbu_line = fenbu_line;
}
public FenbuNum getFenbu_num() {
return fenbu_num;
}
public void setFenbu_num(FenbuNum fenbu_num) {
this.fenbu_num = fenbu_num;
}
public String getFenbu_tag() {
return fenbu_tag;
}
public void setFenbu_tag(String fenbu_tag) {
this.fenbu_tag = fenbu_tag;
}
}
@@ -0,0 +1,104 @@
package com.openxu.cview.xmstock.bean;
/**
* autour : xiami
* date : 2018/3/13 14:59
* className : FenbuInfo
* version : 1.0
* description : 涨跌分布
*/
public class FenbuNum {
private String time;
private String up;
private String up_stop;
private String down;
private String down_stop;
private String equal;
private String stop;
private String up_no_one;
private String up_one;
/*
"time": "14:51",
"up": 962,
"up_stop": 23,
"down": 2268,
"down_stop": 2,
"equal": 361,
"stop": 137,
"up_no_one": 0,
"up_one": 3
*/
public String getTime() {
return time;
}
public void setTime(String time) {
this.time = time;
}
public String getUp() {
return up;
}
public void setUp(String up) {
this.up = up;
}
public String getUp_stop() {
return up_stop;
}
public void setUp_stop(String up_stop) {
this.up_stop = up_stop;
}
public String getDown() {
return down;
}
public void setDown(String down) {
this.down = down;
}
public String getDown_stop() {
return down_stop;
}
public void setDown_stop(String down_stop) {
this.down_stop = down_stop;
}
public String getEqual() {
return equal;
}
public void setEqual(String equal) {
this.equal = equal;
}
public String getStop() {
return stop;
}
public void setStop(String stop) {
this.stop = stop;
}
public String getUp_no_one() {
return up_no_one;
}
public void setUp_no_one(String up_no_one) {
this.up_no_one = up_no_one;
}
public String getUp_one() {
return up_one;
}
public void setUp_one(String up_one) {
this.up_one = up_one;
}
}
@@ -0,0 +1,35 @@
package com.openxu.cview.xmstock.bean;
import android.graphics.PointF;
import java.util.List;
/**
* autour : xiami
* date : 2018/3/14 10:23
* className : FocusInfo
* version : 1.0
* description : 触摸焦点封装
*/
public class FocusInfo {
private PointF point;
private List<DataPoint> focusData;
public PointF getPoint() {
return point;
}
public void setPoint(PointF point) {
this.point = point;
}
public List<DataPoint> getFocusData() {
return focusData;
}
public void setFocusData(List<DataPoint> focusData) {
this.focusData = focusData;
}
}
@@ -0,0 +1,41 @@
package com.openxu.cview.xmstock.bean;
import java.util.List;
/**
* autour : xiami
* date : 2018/3/13 14:59
* className : TrendInfo
* version : 1.0
* description : 涨跌趋势
*/
public class TrendInfo {
private List<List<String>> day;
private List<List<String>> month;
private String trend_tag;
public List<List<String>> getDay() {
return day;
}
public void setDay(List<List<String>> day) {
this.day = day;
}
public List<List<String>> getMonth() {
return month;
}
public void setMonth(List<List<String>> month) {
this.month = month;
}
public String getTrend_tag() {
return trend_tag;
}
public void setTrend_tag(String trend_tag) {
this.trend_tag = trend_tag;
}
}
@@ -0,0 +1,32 @@
package com.openxu.cview.xmstock.bean;
import java.util.List;
/**
* autour : xiami
* date : 2018/3/13 14:59
* className : WeightInfo
* version : 1.0
* description : 权重
*/
public class WeightInfo {
private List<List<String>> weight_line;
private String weight_tag;
public List<List<String>> getWeight_line() {
return weight_line;
}
public void setWeight_line(List<List<String>> weight_line) {
this.weight_line = weight_line;
}
public String getWeight_tag() {
return weight_tag;
}
public void setWeight_tag(String weight_tag) {
this.weight_tag = weight_tag;
}
}
@@ -0,0 +1,32 @@
package com.openxu.cview.xmstock.bean;
import java.util.List;
/**
* autour : xiami
* date : 2018/3/13 14:59
* className : ZhabanInfo
* version : 1.0
* description : 炸板
*/
public class ZhabanInfo {
private List<List<String>> zhaban_line;
private String zhaban_tag;
public List<List<String>> getZhaban_line() {
return zhaban_line;
}
public void setZhaban_line(List<List<String>> zhaban_line) {
this.zhaban_line = zhaban_line;
}
public String getZhaban_tag() {
return zhaban_tag;
}
public void setZhaban_tag(String zhaban_tag) {
this.zhaban_tag = zhaban_tag;
}
}
@@ -0,0 +1,84 @@
package com.openxu.cview.xmstock.bean;
/**
* autour : xiami
* date : 2018/3/13 14:59
* className : ZhabanStock
* version : 1.0
* description : 炸板股票
*/
public class ZhabanStock {
private String symbol;
private String name;
private String code;
private String trade;
private String hsl;
private String amount;
private String updownrate;
private String zhenfu;
public String getSymbol() {
return symbol;
}
public void setSymbol(String symbol) {
this.symbol = symbol;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getTrade() {
return trade;
}
public void setTrade(String trade) {
this.trade = trade;
}
public String getHsl() {
return hsl;
}
public void setHsl(String hsl) {
this.hsl = hsl;
}
public String getAmount() {
return amount;
}
public void setAmount(String amount) {
this.amount = amount;
}
public String getUpdownrate() {
return updownrate;
}
public void setUpdownrate(String updownrate) {
this.updownrate = updownrate;
}
public String getZhenfu() {
return zhenfu;
}
public void setZhenfu(String zhenfu) {
this.zhenfu = zhenfu;
}
}
@@ -0,0 +1,32 @@
package com.openxu.cview.xmstock.bean;
import java.util.List;
/**
* autour : xiami
* date : 2018/3/13 14:59
* className : ZhangfuInfo
* version : 1.0
* description : 涨幅
*/
public class ZhangfuInfo {
private List<List<String>> zhangfu_line;
private String zhangfu_tag;
public List<List<String>> getZhangfu_line() {
return zhangfu_line;
}
public void setZhangfu_line(List<List<String>> zhangfu_line) {
this.zhangfu_line = zhangfu_line;
}
public String getZhangfu_tag() {
return zhangfu_tag;
}
public void setZhangfu_tag(String zhangfu_tag) {
this.zhangfu_tag = zhangfu_tag;
}
}
@@ -0,0 +1,71 @@
package com.openxu.cview.xmstock.bean;
/**
* autour : xiami
* date : 2018/3/13 14:59
* className : ZhangtingStock
* version : 1.0
* description : 涨停股票信息
*/
public class ZhangtingStock {
//"symbol": "sh603083",
// "name": "剑桥科技",
// "code": "603083",
// "trade": "43.320",
// "updownrate": "-8.95%",
// "hsl": "6.05%"
private String symbol;
private String name;
private String code;
private String trade;
private String hsl;
private String updownrate;
public String getSymbol() {
return symbol;
}
public void setSymbol(String symbol) {
this.symbol = symbol;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getTrade() {
return trade;
}
public void setTrade(String trade) {
this.trade = trade;
}
public String getHsl() {
return hsl;
}
public void setHsl(String hsl) {
this.hsl = hsl;
}
public String getUpdownrate() {
return updownrate;
}
public void setUpdownrate(String updownrate) {
this.updownrate = updownrate;
}
}
@@ -0,0 +1,282 @@
package com.openxu.cview.xmstock20191205;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathEffect;
import android.graphics.PointF;
import android.graphics.RadialGradient;
import android.graphics.RectF;
import android.graphics.Shader;
import android.graphics.SweepGradient;
import android.util.AttributeSet;
import android.util.Log;
import android.view.animation.AccelerateDecelerateInterpolator;
import com.openxu.cview.R;
import com.openxu.cview.chart.anim.AngleEvaluator;
import com.openxu.cview.xmstock.BaseChart;
import com.openxu.utils.DensityUtil;
import com.openxu.utils.FontUtil;
import com.openxu.utils.LogUtil;
/**
* 仪表盘图表
*/
public class DashboardView extends BaseChart {
private int total; //总数量
private int progress; //占比数量
private float animPro; //动画计算的占比数量
//开始角度 (正常情况正下方为90度,这里的30是指正下方向左偏30度,也就是120度)
private int startAngle = 30;
private int dashesSize = DensityUtil.dip2px(getContext(), 20); //虚线宽度
private int solidSize = DensityUtil.dip2px(getContext(), 9); //实线宽度
private int rSpace1 = DensityUtil.dip2px(getContext(), 25); //中心白圈与 实线间隙
private int rSpace2 = DensityUtil.dip2px(getContext(), 4); //虚线 实线间隙
//指针颜色和长度(长度应该比rSpace1小)
private int pointerColor = Color.parseColor("#d55942");
private int pointerRaidus = DensityUtil.dip2px(getContext(), 20);
private int pointerSize = DensityUtil.dip2px(getContext(), 10);
/**文字大小颜色*/
//中间文字 (5000亿)
private int centerTextSize = DensityUtil.sp2px(getContext(), 18);
private int centerTextColor = Color.parseColor("#d55942");
//仪表名称 (量能)
private String nameText = "能量";
private int nameTextSize = DensityUtil.sp2px(getContext(), 20);
private int nameTextColor = Color.parseColor("#dc3929");
//仪表刻度 (0 一万亿)
private int lableTextSize = DensityUtil.sp2px(getContext(), 18);
private int lableTextColor = Color.parseColor("#e2978a");
private String lableLeft = "0";
private String lableRight = "一万亿";
//0 一万亿 文字与扇形间距
private int lableSpace = DensityUtil.dip2px(getContext(), 4);
//渐变色
int[] proColors = new int[]{
Color.parseColor("#e7ac6a"), //初始颜色
Color.parseColor("#e47f3f"), //中间颜色
Color.parseColor("#df422c")}; //最终颜色
float[] proPositions = new float[]{0.3f, .5f, .7f};
SweepGradient proShader;
//中间灰色边缘渐变色
RadialGradient sideShader;
int[] sideColors = new int[]{
Color.parseColor("#b2c2dd"), //灰色
Color.parseColor("#00b2c2dd")}; //全透明灰色
//虚线间隔
PathEffect effects = new DashPathEffect(new float[]{9,11,9,11},0);
/**计算*/
private int centerRaidus; //中间白色圆圈半径
private int chartRaidus; //半径
protected RectF rectDashes; //虚线扇形矩形框(位于扇形中心)
protected RectF rectSolid; //实线扇形矩形框(位于扇形中心)
private PointF startPointLeft, startPointRight; //0 一万亿 lable绘制点
private PointF startPointName; //量能 绘制点
public DashboardView(Context context) {
super(context, null);
}
public DashboardView(Context context, AttributeSet attrs) {
super(context, attrs, 0);
}
public DashboardView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public void init(Context context, AttributeSet attrs, int defStyleAttr) {
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(widthMeasureSpec);
LogUtil.i(TAG, "建议宽高:"+widthSize + " * "+heightSize);
//根据中间文字大小计算白色圆圈半径
paintLabel.setTextSize(centerTextSize);
int textWidth = (int)FontUtil.getFontlength(paintLabel, "100000");
// int textHeight = (int)FontUtil.getFontHeight(paintLabel);
centerRaidus = textWidth/2 + DensityUtil.dip2px(getContext(), 5);
//最大半径
chartRaidus = centerRaidus + rSpace1 + solidSize + rSpace2 + dashesSize;
paintLabel.setTextSize(lableTextSize);
LogUtil.d(TAG, "centerRaidus:"+centerRaidus);
LogUtil.d(TAG, "chartRaidus:"+chartRaidus);
//计算控件宽高
int raidus = chartRaidus;
int sectionRaidus = (int)(Math.cos(Math.toRadians(startAngle)) * raidus);//下切面高度 Math.toRadians将角度转换成弧度值
widthSize = getPaddingLeft() + getPaddingRight() + chartRaidus*2;
heightSize = getPaddingTop()+getPaddingBottom() + chartRaidus + sectionRaidus + lableSpace + (int)FontUtil.getFontHeight(paintLabel);
setMeasuredDimension(MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY));
LogUtil.i(TAG, "宽高:"+getMeasuredWidth() + " * "+getMeasuredHeight());
centerPoint = new PointF(getPaddingLeft()+chartRaidus, getPaddingTop()+chartRaidus);
rectChart = new RectF(getPaddingLeft(),getPaddingTop(),
getMeasuredWidth()-getPaddingRight(), getPaddingTop()+chartRaidus*2);
//计算实线扇形矩形区域-----------------
raidus = centerRaidus+rSpace1+solidSize/2;
rectSolid = new RectF(centerPoint.x-raidus,centerPoint.y-raidus,
centerPoint.x+raidus, centerPoint.y+raidus);
//量能 绘制点
startPointName = new PointF(centerPoint.x, rectSolid.bottom);
//虚线部分--------------------------
raidus = centerRaidus+rSpace1+solidSize +rSpace2+dashesSize/2;
rectDashes = new RectF(centerPoint.x-raidus,centerPoint.y-raidus,
centerPoint.x+raidus, centerPoint.y+raidus);
//lable绘制开始和结束点 ----------------
raidus = centerRaidus+rSpace1+solidSize +rSpace2+dashesSize;
sectionRaidus = (int)(Math.cos(Math.toRadians(startAngle)) * raidus);
startPointLeft = new PointF((int)(centerPoint.x-Math.sin(Math.toRadians(startAngle))*raidus), centerPoint.y+sectionRaidus);
startPointRight = new PointF((int)(centerPoint.x+Math.sin(Math.toRadians(startAngle))*raidus), centerPoint.y+sectionRaidus);
//渐变色
proShader = new SweepGradient(centerPoint.x, centerPoint.y,proColors,proPositions);
sideShader = new RadialGradient(centerPoint.x, centerPoint.y, centerRaidus+rSpace1, sideColors,
new float[]{0.2f,.75f}, Shader.TileMode.CLAMP);
LogUtil.d(TAG, "centerPoint:"+centerPoint);
LogUtil.d(TAG, "rectChart:"+rectChart);
invalidate();
}
public void setData(int total, int progress){
if(progress>total)
progress = total;
this.total = total;
this.progress = progress;
startDraw = false;
invalidate();
}
@Override
public void drawDebug(Canvas canvas) {
super.drawDebug(canvas);
paint.setStyle(Paint.Style.STROKE);
paint.setColor(Color.GREEN);
canvas.drawRect(rectChart.left, rectChart.top, rectChart.right, rectChart.bottom, paint);
paint.setColor(Color.BLACK);
canvas.drawRect(rectDashes.left, rectDashes.top, rectDashes.right, rectDashes.bottom, paint);
paint.setColor(Color.GRAY);
canvas.drawRect(rectSolid.left, rectSolid.top, rectSolid.right, rectSolid.bottom, paint);
}
/**绘制图表基本框架*/
@Override
public void drawDefult(Canvas canvas) {
//量能
paintLabel.setTextSize(nameTextSize);
paintLabel.setColor(nameTextColor);
canvas.drawText(nameText, startPointName.x - FontUtil.getFontlength(paintLabel, nameText)/2,
startPointName.y-FontUtil.getFontHeight(paintLabel)/3*2+FontUtil.getFontLeading(paintLabel), paintLabel);
//0 一万亿
paintLabel.setTextSize(lableTextSize);
paintLabel.setColor(lableTextColor);
canvas.drawText(lableLeft, startPointLeft.x - FontUtil.getFontlength(paintLabel, lableLeft)/2,
startPointLeft.y+lableSpace+FontUtil.getFontLeading(paintLabel), paintLabel);
canvas.drawText(lableRight, startPointRight.x - FontUtil.getFontlength(paintLabel, lableRight)/2,
startPointRight.y+lableSpace+FontUtil.getFontLeading(paintLabel), paintLabel);
//实线扇形
//由于渐变色shader是从正东方开始绘制,导致颜色排版错误,这里将画布旋转90度,正下方为0度
canvas.rotate(90, centerPoint.x, centerPoint.y);
paint.setShader(proShader);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(solidSize);
canvas.drawArc(rectSolid, startAngle, 360.0f - startAngle*2, false, paint);
paint.setShader(null);
//绘制虚线
paintEffect.setShader(null);
paintEffect.setStyle(Paint.Style.STROKE);
paintEffect.setPathEffect(effects);
paintEffect.setColor(defColor);
paintEffect.setStrokeWidth(dashesSize);
canvas.drawArc(rectDashes, startAngle, 360.0f - startAngle*2, false, paintEffect);
//绘制中间灰色边缘
paint.setShader(sideShader);
// paint.setColor(Color.RED);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(rSpace1);
canvas.drawCircle(centerPoint.x, centerPoint.y, centerRaidus+rSpace1/2, paint);
paint.setShader(null);
//中间白色圆圈
paint.setStyle(Paint.Style.FILL);
paint.setColor(Color.WHITE);
canvas.drawCircle(centerPoint.x, centerPoint.y, centerRaidus, paint);
//反转90度还原
canvas.rotate(-90, centerPoint.x, centerPoint.y);
}
/**绘制图表*/
@Override
public void drawChart(Canvas canvas) {
if(progress>0) {
//数量文字
paintLabel.setTextSize(centerTextSize);
paintLabel.setColor(centerTextColor);
String text = ((int)animPro)+"";
canvas.drawText(text, centerPoint.x - FontUtil.getFontlength(paintLabel, text)/2,
centerPoint.y-FontUtil.getFontHeight(paintLabel)+FontUtil.getFontLeading(paintLabel), paintLabel);
text = "亿";
canvas.drawText(text, centerPoint.x - FontUtil.getFontlength(paintLabel, text)/2,
centerPoint.y+DensityUtil.dip2px(getContext(), 2)+FontUtil.getFontLeading(paintLabel), paintLabel);
//绘制彩色虚线
//再次寻转90度
canvas.rotate(90, centerPoint.x, centerPoint.y);
float proAngle = (360.0f - startAngle*2) * (animPro/total*1.0f);
paintEffect.setStyle(Paint.Style.STROKE);
paintEffect.setPathEffect(effects);
paintEffect.setShader(proShader);
paintEffect.setStrokeWidth(dashesSize);
//消除由于虚线间隔导致视觉上仪表进度不够的情况,默认多绘制2.5度
canvas.drawArc(rectDashes, startAngle, proAngle<(360.0f - startAngle*2-5)? proAngle+2.5f:proAngle, false, paintEffect);
paintEffect.setShader(null);
//绘制指针
/*画一个实心三角形*/
//以正东面为0度起点计算指定角度所对应的圆周上的点的坐标:
double asin = Math.toDegrees(Math.asin(pointerSize/2f/centerRaidus));
float centerArcX1 = centerPoint.x + (float) (centerRaidus* Math.cos(Math.toRadians(startAngle-asin+proAngle)));
float centerArcY1 = centerPoint.y + (float) (centerRaidus* Math.sin(Math.toRadians(startAngle-asin+proAngle)));
// LogUtil.i(TAG, "角度 "+(startAngle-asin+proAngle)+" 弧度:"+Math.toRadians(startAngle-asin+proAngle));
// LogUtil.i(TAG, "cos "+Math.cos(Math.toRadians(startAngle-asin+proAngle)));
// LogUtil.i(TAG, "sin "+Math.sin(Math.toRadians(startAngle-asin+proAngle)));
float endx2 = centerPoint.x + (float) ((pointerRaidus+centerRaidus)* Math.cos(Math.toRadians(startAngle+proAngle)));
float endy2 = centerPoint.y + (float) ((pointerRaidus+centerRaidus)* Math.sin(Math.toRadians(startAngle+proAngle)));
float centerArcX2 = centerPoint.x + (float) (centerRaidus* Math.cos(Math.toRadians(startAngle+asin+proAngle)));
float centerArcY2 = centerPoint.y + (float) (centerRaidus* Math.sin(Math.toRadians(startAngle+asin+proAngle)));
// LogUtil.d(TAG, "角度 "+(startAngle+asin+proAngle)+" 弧度:"+Math.toRadians(startAngle+asin+proAngle));
// LogUtil.d(TAG, "cos "+Math.cos(Math.toRadians(startAngle+asin+proAngle)));
// LogUtil.d(TAG, "sin "+Math.sin(Math.toRadians(startAngle+asin+proAngle)));
Path path2=new Path();
path2.moveTo(centerArcX1,centerArcY1);
path2.lineTo(endx2,endy2);
path2.lineTo(centerArcX2,centerArcY2);
path2.close();
paint.setColor(pointerColor);
canvas.drawPath(path2, paint);
}
}
/**创建动画*/
@Override
protected ValueAnimator initAnim() {
anim = ValueAnimator.ofObject(new AngleEvaluator(), 0f, 1.0f);
anim.setInterpolator(new AccelerateDecelerateInterpolator());
return anim;
}
/**动画值变化之后计算数据*/
@Override
protected void evaluatorData(ValueAnimator animation) {
animPro = (float)animation.getAnimatedValue()*progress;
}
}
@@ -0,0 +1,162 @@
package com.openxu.cview.xmstock20191205;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.animation.AccelerateDecelerateInterpolator;
import com.openxu.cview.R;
import com.openxu.cview.chart.anim.AngleEvaluator;
import com.openxu.cview.xmstock.BaseChart;
import com.openxu.utils.DensityUtil;
import com.openxu.utils.FontUtil;
public class LevelProgressView extends BaseChart {
private int total; //总数量
private int progress; //占比数量
private float animPro; //动画计算的占比数量
private String lvStr = "Lv";
private String lable;
protected PointF startPoint;
//文字大小
private int lableTextSize = (int)getResources().getDimension(R.dimen.ts_chart_lable);
private int lvTextSize = (int)getResources().getDimension(R.dimen.ts_chart_xy);
private int textSpace = DensityUtil.dip2px(getContext(), 3); //文字间距
private int raidus = DensityUtil.dip2px(getContext(), 5); //圆环半径
private int barSize = DensityUtil.dip2px(getContext(), 5); //宽度
//默认Lv字体颜色(白色)
private int textColor = Color.WHITE;
//进度颜色(黄色)
private int proColor = Color.parseColor("#fda33c");
public LevelProgressView(Context context) {
super(context, null);
}
public LevelProgressView(Context context, AttributeSet attrs) {
super(context, attrs, 0);
}
public LevelProgressView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public void init(Context context, AttributeSet attrs, int defStyleAttr) {
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getMeasuredWidth();
float height = getMeasuredHeight();
paintLabel.setTextSize(lvTextSize);
int centerY = (int)(getPaddingBottom()+FontUtil.getFontHeight(paintLabel) + textSpace + raidus);
height = getPaddingBottom()+FontUtil.getFontHeight(paintLabel) + textSpace*2 + raidus*2+getPaddingTop();
paintLabel.setTextSize(lvTextSize);
height += FontUtil.getFontHeight(paintLabel);
float lv0 = FontUtil.getFontlength(paintLabel, lvStr+"0");
float lv10 = FontUtil.getFontlength(paintLabel, lvStr+"10");
startPoint = new PointF(getPaddingLeft() + lv0/2, centerY);
rectChart = new RectF(startPoint.x, getPaddingTop(), width - getPaddingRight() - lv10/2, height-getPaddingBottom());
setMeasuredDimension(width, (int)height);
centerPoint = new PointF(width/2, height/2);
}
public void setLable(String lable) {
this.lable = lable;
}
public void setData(int total, int progress){
this.total = total;
this.progress = progress;
startDraw = false;
invalidate();
}
/**绘制图表基本框架*/
@Override
public void drawDefult(Canvas canvas) {
paint.setStyle(Paint.Style.FILL);
paint.setColor(defColor);
canvas.drawRect(rectChart.left, startPoint.y-barSize/2,
rectChart.right, startPoint.y+barSize/2, paint);
float itemWidth = rectChart.width()/(total-1);
float x;
paintLabel.setTextSize(lvTextSize);
paintLabel.setColor(textColor);
float lvHeight = FontUtil.getFontHeight(paintLabel);
float lvLead = FontUtil.getFontLeading(paintLabel);
for(int i = 0; i < total; i++){
x = rectChart.left + i*itemWidth;
canvas.drawCircle(x, startPoint.y, raidus, paint);
//绘制文字
canvas.drawText(lvStr+i, x - FontUtil.getFontlength(paintLabel, lvStr+i)/2,
rectChart.bottom-lvHeight+lvLead, paintLabel);
}
}
/**绘制图表*/
@Override
public void drawChart(Canvas canvas) {
if(progress>0) {
paint.setStyle(Paint.Style.FILL);
paint.setColor(proColor);
float itemWidth = rectChart.width()/(total-1);
Log.w(TAG, "动画进度:"+animPro);
canvas.drawRect(rectChart.left, startPoint.y-barSize/2,
rectChart.left+animPro*itemWidth, startPoint.y+barSize/2, paint);
for(int i = 0; i <= (int)animPro; i++){
canvas.drawCircle(rectChart.left + i*itemWidth, startPoint.y, raidus, paint);
if(i!=0 && i==animPro){
//绘制颜色Lv文字
paintLabel.setTextSize(lvTextSize);
paintLabel.setColor(proColor);
float textHeight = FontUtil.getFontHeight(paintLabel);
float textLead = FontUtil.getFontLeading(paintLabel);
canvas.drawText(lvStr+i, rectChart.left + i*itemWidth
- FontUtil.getFontlength(paintLabel, lvStr+i)/2,
rectChart.bottom-textHeight+textLead, paintLabel);
//绘制上方文字
if(null!=lable){
paintLabel.setTextSize(lableTextSize);
textHeight = FontUtil.getFontHeight(paintLabel);
textLead = FontUtil.getFontLeading(paintLabel);
float textLength = FontUtil.getFontlength(paintLabel, lable);
canvas.drawText(lable,
(rectChart.left + i*itemWidth + textLength/2)<=getWidth() - getPaddingRight()?
rectChart.left + i*itemWidth-textLength/2 :
getWidth() - getPaddingRight() - textLength,
startPoint.y -raidus - textSpace - textHeight+textLead, paintLabel);
}
}
}
}
}
/**创建动画*/
@Override
protected ValueAnimator initAnim() {
if(progress>0) {
ValueAnimator anim = ValueAnimator.ofObject(new AngleEvaluator(), 1f, Float.valueOf(progress+""));
anim.setInterpolator(new AccelerateDecelerateInterpolator());
return anim;
}
return null;
}
/**动画值变化之后计算数据*/
@Override
protected void evaluatorData(ValueAnimator animation) {
animPro = (float)animation.getAnimatedValue() - 1;
}
}
@@ -0,0 +1,673 @@
package com.openxu.cview.xmstock20191205;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathEffect;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.animation.AccelerateDecelerateInterpolator;
import com.openxu.cview.R;
import com.openxu.cview.chart.anim.AngleEvaluator;
import com.openxu.cview.xmstock.BaseChart;
import com.openxu.cview.xmstock.bean.DataPoint;
import com.openxu.cview.xmstock.bean.FocusInfo;
import com.openxu.cview.xmstock20191205.bean.NSFocusInfo;
import com.openxu.utils.DensityUtil;
import com.openxu.utils.FontUtil;
import com.openxu.utils.LogUtil;
import com.openxu.utils.NumberFormatUtil;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
public class NorthSouthChart extends BaseChart {
//计算后的数据
private int dataNumCount = 60*4;// 今日数据(60分钟*4小时) ,历史日周数据按照接口返回数量
private List<DataPoint> lableXPointList = new ArrayList<>(); //计算X轴坐标点容器
private List<DataPoint> lableYLPointList = new ArrayList<>(); //计算左侧Y轴坐标点容器
private List<DataPoint> lableYRPointList = new ArrayList<>(); //计算右侧Y轴坐标点容器
private List<ArrayList<DataPoint>> linePointList;
private float YMARK_MAX_L = Float.MIN_VALUE; //Y轴刻度最大值
private float YMARK_MIN_L = Float.MAX_VALUE; //Y轴刻度最小值
private float YMARK_MAX_R = Float.MIN_VALUE; //Y轴刻度最大值
private float YMARK_MIN_R = Float.MAX_VALUE; //Y轴刻度最小值
private float lableLead, lableHeight;
public enum ChartType{
TYPE_T_NORTH, //北向资金今日流向
TYPE_T_SOUTH, //南向资金今日流向
TYPE_DW_NORTH, //北向资金 历史每日/周流向
TYPE_DW_SOUTH, //南向资金 历史每日/周流向
}
/**可以设置的属性 ★表示必设置*/
//设置数据
private List<List<String>> dataList; //★图表数据
private String[] lableXArray; // X坐标刻度文字设置 new String[]{"09:30", "10:30", "11:30/13:00", "14:00", "15:00"};
private String[] lableArray; //★指标文字
private ChartType chartType; //★图表类型
private int XMARK_NUM = 5; //X轴刻度数量 和lableXArray互斥(必须设置其中一个)
private int YMARK_NUM = 5; //Y轴刻度数量
private int[] lableColor;//★设置lable对应折线颜色
private int[] upDownColor;//★设置涨跌颜色(注意顺序,第一个为涨(红色), 第二个为跌(绿色))
private int lineSize = DensityUtil.dip2px(getContext(), 1.2f);//设置曲线粗细
private int barSize = DensityUtil.dip2px(getContext(), 7); //设置柱子宽度
private float ONE_BAR_WEDTH; //不需要设置 (计算)单个柱状占的宽度(包含space)
private int lableTextSize = (int)getResources().getDimension(R.dimen.ts_chart_lable); //指标文字大小
private int textSpaceLable = DensityUtil.dip2px(getContext(), 12); //指标文字间距
private int textSize = (int)getResources().getDimension(R.dimen.ts_chart_xy);//坐标刻度文字大小
private int textColor = getResources().getColor(R.color.tc_chart_xy);//设置坐标文字颜色
private int textSpaceX = DensityUtil.dip2px(getContext(), 5);//设置X坐标字体与横轴的距离
private int textSpaceY = DensityUtil.dip2px(getContext(), 1); //设置Y坐标字体与左右的距离
//设置焦点相关
private int focusLineColor = getResources().getColor(R.color.tc_chart_focus_line);//焦点交互线颜色
private int focusLineSize = DensityUtil.dip2px(getContext(), 0.8f); //焦点交互线宽度
private int focusPanelColor = Color.parseColor("#5f93e7"); //焦点面板背景色
private int focusTextColor = Color.parseColor("#ffffff"); //焦点面板上文字颜色
private int focusTextSize = DensityUtil.sp2px(getContext(), 8); //焦点面板上文字大小
private String[] focusLableArray; //★焦点面板文字
public NorthSouthChart(Context context) {
this(context, null);
}
public NorthSouthChart(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public NorthSouthChart(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public void init(Context context, AttributeSet attrs, int defStyleAttr) {
dataList = new ArrayList<>();
touchEnable = true;
}
/***********************************设置属性set方法**********************************/
public void setChartType(ChartType chartType) {
this.chartType = chartType;
}
public void setYMARK_NUM(int YMARK_NUM) {
this.YMARK_NUM = YMARK_NUM;
}
public void setXMARK_NUM(int XMARK_NUM) {
this.XMARK_NUM = XMARK_NUM;
}
/**
* 设置x刻度
* @param lableXArray X轴显示的刻度,如果不设置,会自动配置,但是需要设置x刻度显示数量
*/
public void setLableX(String[] lableXArray){
if(null!=lableXArray && lableXArray.length>0)
this.lableXArray = lableXArray;
}
public void setlableArray(String[] lableArray) {
this.lableArray = lableArray;
}
public void setFocusLableArray(String[] focusLableArray) {
this.focusLableArray = focusLableArray;
}
public void setlableColor(int[] lableColor) {
this.lableColor = lableColor;
}
public void setUpDownColor(int[] upDownColor) {
this.upDownColor = upDownColor;
}
/***********************************设置属性set方法over**********************************/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(widthSize, heightSize);
evaluatorByData();
invalidate();
}
/**设置数据*/
public void setData(List<List<String>> dataList){
if(null==dataList)
return;
this.dataList.clear();
this.dataList.addAll(dataList);
}
/**重绘制*/
public void refresh(){
if(getMeasuredWidth()>0) {
evaluatorByData();
startDraw = false;
invalidate();
}
}
/**设置数据后,计算相关值*/
private void evaluatorByData(){
if(dataList.size()<=0)
return;
if(chartType == ChartType.TYPE_T_SOUTH){//南向资金今日流向
dataNumCount = (int)(60*5.5f)+2; //9:30 - 12:00 13:00 - 16:00 2.5+3=5.5小时
}else if(chartType == ChartType.TYPE_T_NORTH){//北向资金今日流向
dataNumCount = 60*4+2; //9:30 - 11:30 13:00 - 15:00 2+2=4小时
}else{
dataNumCount = dataList.size();
lableXArray = null;
}
/**①、计算字体相关以及图表原点坐标*/
paintLabel.setTextSize(textSize);
lableHeight = FontUtil.getFontHeight(paintLabel);
lableLead = FontUtil.getFontLeading(paintLabel);
paintLabel.setTextSize(lableTextSize);
//图表主体矩形
rectChart = new RectF(getPaddingLeft(),
getPaddingTop() + FontUtil.getFontHeight(paintLabel) +
textSpaceLable +lableHeight/2,
getMeasuredWidth()-getPaddingRight(),
getMeasuredHeight()-getPaddingBottom() - lableHeight - textSpaceX - lableHeight/2);
centerPoint = new PointF(rectChart.left+(rectChart.right-rectChart.left)/2,
rectChart.top+(rectChart.bottom-rectChart.top)/2);
/**②、计算X标签绘制坐标*/
float lableXSpace = 0;
if(lableXArray==null){
//从数据中抽取x刻度
if(dataNumCount<=XMARK_NUM)
XMARK_NUM = dataNumCount;
lableXArray = new String[XMARK_NUM];
if(dataNumCount==XMARK_NUM){
for(int i = 0 ; i < XMARK_NUM; i++)
lableXArray[i] = dataList.get(i).get(0);
}else{
lableXArray[0] = dataList.get(0).get(0); //取第一条数据的x坐标
lableXArray[XMARK_NUM-1] = dataList.get(dataNumCount-1).get(0);
float space = dataNumCount/XMARK_NUM;
boolean m = dataNumCount%XMARK_NUM>0;
float indexCount = 0;
for(int i = 1; i < XMARK_NUM-1; i++){
indexCount += space;
//四舍五入,均匀分布
lableXArray[i] = dataList.get(
((indexCount - (int)indexCount)>0.5)?
(int)indexCount + 1 : (int)indexCount
).get(0);
}
}
}
paintLabel.setTextSize(textSize);
//计算x刻度坐标
for(String lableX : lableXArray)
lableXSpace += FontUtil.getFontlength(paintLabel, lableX);
lableXSpace = (rectChart.right - rectChart.left - lableXSpace)/(lableXArray.length -1);
lableXPointList = new ArrayList<>();
if(lableXSpace>0){
float left = rectChart.left;
for(int i = 0; i<lableXArray.length; i++){
String lableX = lableXArray[i];
//借用valueY字段表示 x轴刻度点x坐标, point表示字体绘制开始点坐标
lableXPointList.add(new DataPoint(lableX, i==0?rectChart.left:(i==lableXArray.length-1?rectChart.right:(left+FontUtil.getFontlength(paintLabel, lableXArray[i])/2)),
new PointF(left,rectChart.bottom + textSpaceX + lableLead)));
left += (FontUtil.getFontlength(paintLabel, lableXArray[i])+lableXSpace);
}
}else{
//如果X轴标签字体过长的情况需要特殊处理
float oneWidth = (rectChart.right - rectChart.left)/lableXArray.length;
for(int i = 0; i<lableXArray.length; i++){
String lableX = lableXArray[i];
float xLen = FontUtil.getFontlength(paintLabel, lableX);
lableXPointList.add(new DataPoint(lableX, 0, new PointF(
rectChart.left+i*oneWidth+(oneWidth-xLen)/2,rectChart.bottom + textSpaceX + lableLead)));
}
}
/**③、计算Y刻度最大值和最小值以及幅度*/
evaluatorYMark(true);
evaluatorYMark(false);
/**④、计算点的坐标,如果有动画的情况下,边绘制边计算会耗费性能,所以先计算*/
linePointList = new ArrayList<>();
linePointList.add(new ArrayList<DataPoint>());
linePointList.add(new ArrayList<DataPoint>());
if(chartType==ChartType.TYPE_T_SOUTH||chartType==ChartType.TYPE_T_NORTH){
float oneSpace = (rectChart.right - rectChart.left) / dataNumCount; //分时图
//今日 ["1558","27.3亿元","26208.240","+0.56%"]
for(int i=0; i<dataList.size(); i++){
List<String> itemList = dataList.get(i);
//右Y刻度线
float valueY = Float.valueOf(itemList.get(1).replace("亿元", ""));
PointF point = new PointF(rectChart.left + i * oneSpace, rectChart.bottom -
(rectChart.bottom-rectChart.top)/(YMARK_MAX_R - YMARK_MIN_R) * (valueY-YMARK_MIN_R));
linePointList.get(0).add(new DataPoint(itemList.get(0), valueY, point));
//左Y刻度线
valueY = Float.valueOf(itemList.get(2));
point = new PointF(rectChart.left + i * oneSpace,
rectChart.bottom -
(rectChart.bottom-rectChart.top)/(YMARK_MAX_L - YMARK_MIN_L) * (valueY-YMARK_MIN_L));
linePointList.get(1).add(new DataPoint(itemList.get(0), valueY, point));
}
}else{
//历史 ["2019-07-05","37.60亿元","28774.83","+0.81%"]
ONE_BAR_WEDTH = (rectChart.right - rectChart.left)/dataList.size();
for(int i=0; i<dataList.size(); i++){
List<String> itemList = dataList.get(i);
//右Y刻度线
float valueY = Float.valueOf(itemList.get(1).replace("亿元", ""));
PointF point = new PointF(rectChart.left+ONE_BAR_WEDTH*i+ONE_BAR_WEDTH/2,
valueY>0? centerPoint.y - (centerPoint.y-rectChart.top)/YMARK_MAX_R * valueY :
centerPoint.y + (rectChart.bottom-centerPoint.y)/YMARK_MIN_R * valueY);
linePointList.get(0).add(new DataPoint(itemList.get(0), valueY, point));
//左Y刻度线
valueY = Float.valueOf(itemList.get(2));
point = new PointF(rectChart.left+ONE_BAR_WEDTH*i+ONE_BAR_WEDTH/2, rectChart.bottom -
(rectChart.bottom-rectChart.top)/(YMARK_MAX_L - YMARK_MIN_L) * (valueY-YMARK_MIN_L));
linePointList.get(1).add(new DataPoint(itemList.get(0), valueY, point));
}
}
// if(null!=onFocusChangeListener)
// onFocusChangeListener.onfocus(focusInfo);
}
private void evaluatorYMark(boolean left){
float YMARK;
float YMARK_MAX = Float.MIN_VALUE;
float YMARK_MIN = Float.MAX_VALUE;
for(List<String> list : dataList){
String str = left?list.get(2):list.get(1);
if(str.contains("亿元"))
str = str.replaceAll("亿元", "");
YMARK = Float.parseFloat(str);
if(YMARK>YMARK_MAX)
YMARK_MAX = YMARK;
if(YMARK<YMARK_MIN)
YMARK_MIN = YMARK;
}
LogUtil.w(TAG, "Y轴真实YMARK_MIN="+YMARK_MIN+" YMARK_MAX="+YMARK_MAX);
float ce = (YMARK_MAX-YMARK_MIN)/10;
YMARK_MAX += ce;
YMARK_MIN -= ce;
if((chartType == ChartType.TYPE_DW_NORTH || chartType == ChartType.TYPE_DW_SOUTH) && !left){
//历史日、周,计算净流入资金中点值0
float YMARK_ABS = Math.abs(YMARK_MAX) > Math.abs(YMARK_MIN)?Math.abs(YMARK_MAX):Math.abs(YMARK_MIN);
YMARK_MAX = YMARK_ABS;
YMARK_MIN = -YMARK_ABS;
YMARK = (YMARK_MAX-YMARK_MIN)/(YMARK_NUM - 1);
}else{
YMARK = (YMARK_MAX-YMARK_MIN)/(YMARK_NUM - 1);
}
if(left){
YMARK_MAX_L = YMARK_MAX;
YMARK_MIN_L = YMARK_MIN;
}else{
YMARK_MAX_R = YMARK_MAX;
YMARK_MIN_R = YMARK_MIN;
}
List<DataPoint> lableYPointList = left?lableYLPointList:lableYRPointList;
lableYPointList.clear();
float yMarkSpace = (rectChart.bottom - rectChart.top)/(YMARK_NUM-1);
DecimalFormat df = new DecimalFormat("0.0");
for (int i = 0; i < YMARK_NUM; i++) {
float mark = Float.parseFloat(df.format(YMARK_MIN + i * YMARK));
lableYPointList.add(new DataPoint("", mark,
new PointF(left?rectChart.left+textSpaceY:rectChart.right-textSpaceY-
FontUtil.getFontlength(paintLabel, mark+""),
rectChart.bottom - yMarkSpace * i - lableHeight/2 + lableLead)));
}
}
private NSFocusInfo focusInfo;
@Override
protected void onTouchMoved(PointF point) {
if(null==dataList)
return;
onFocus = (null != point);
if(null != point && null!=dataList && dataList.size()>0) {
//获取焦点对应的数据的索引
int index = (int) ((point.x - rectChart.left) * dataNumCount / (rectChart.right - rectChart.left));
//避免滑出
List<DataPoint> dataPoints = linePointList.get(0);
if(point.x > dataPoints.get(dataPoints.size()-1).getPoint().x)
point.x = dataPoints.get(dataPoints.size()-1).getPoint().x;
if(point.x < dataPoints.get(0).getPoint().x)
point.x = dataPoints.get(0).getPoint().x;
index = Math.max(0, Math.min(index, dataPoints.size() - 1));
focusInfo = new NSFocusInfo();
focusInfo.setPoint(point);
List<DataPoint> objs = new ArrayList<>();
for(List<DataPoint> points : linePointList)
objs.add(points.get(index));
//交点处坐标数据
focusInfo.setDataPoints(objs);
//焦点处后台原始数据
focusInfo.setFocusData(dataList.get(index));
if(null!=onFocusChangeListener)
onFocusChangeListener.onfocus(focusInfo);
}
invalidate();
}
private OnFocusChangeListener onFocusChangeListener;
public void setOnFocusChangeListener(OnFocusChangeListener onFocusChangeListener) {
this.onFocusChangeListener = onFocusChangeListener;
}
public interface OnFocusChangeListener{
void onfocus(NSFocusInfo focusInfo);
}
/**绘制图表基本框架*/
@Override
public void drawDefult(Canvas canvas) {
if(null==linePointList || linePointList.size()<=0)
return;
paint.setStyle(Paint.Style.FILL);
paint.setStrokeWidth(lineWidth);
paint.setColor(defColor);
drawTopLable(canvas);
drawGrid(canvas);
drawXLable(canvas);
drawYLable(canvas);
}
/**绘制debug辅助线*/
@Override
public void drawDebug(Canvas canvas) {
super.drawDebug(canvas);
}
/**绘制图表*/
@Override
public void drawChart(Canvas canvas) {
if(null==linePointList || linePointList.size()<=0)
return;
drawDataPath(canvas);
drawFocus(canvas);
}
/*** 绘制上方lable*/
private void drawTopLable(Canvas canvas) {
if(null==lableArray)
return;
String lableL, lableR;
if(chartType==ChartType.TYPE_T_SOUTH||chartType==ChartType.TYPE_T_NORTH){
lableL = "指数价格";
lableR = "金额 (亿)";
}else{
lableL = "指数价格";
lableR = "金额 (亿)";
}
paintLabel.setTextSize(lableTextSize);
int itemSpace = DensityUtil.dip2px(getContext(), 12); //指标lable间距
int lableSpace = DensityUtil.dip2px(getContext(), 2); //指标y与圆圈的距离
int radis = DensityUtil.dip2px(getContext(), 3); //圆圈半径
float lableHeight = FontUtil.getFontHeight(paintLabel);
float lableLead = FontUtil.getFontLeading(paintLabel);
int lableLengthCount = (lableSpace + radis*2) * lableArray.length + itemSpace * (lableArray.length-1);
for(String lable : lableArray){
lableLengthCount += FontUtil.getFontlength(paintLabel, lable);
}
float lableL_len = FontUtil.getFontlength(paintLabel, lableL);
float lableR_len = FontUtil.getFontlength(paintLabel, lableR);
//指标开始绘制x坐标
float startX = rectChart.left + lableL_len +
(rectChart.right - rectChart.left - lableLengthCount - lableL_len - lableR_len)/2;
paintLabel.setColor(Color.parseColor("#5E5E5E"));
paintLabel.setFakeBoldText(true); //加粗
canvas.drawText(lableL, rectChart.left, getPaddingTop()+lableLead, paintLabel);
canvas.drawText(lableR, rectChart.right-lableR_len, getPaddingTop()+lableLead, paintLabel);
paintLabel.setColor(Color.parseColor("#939393"));
paintLabel.setFakeBoldText(false);
for(int i = 0; i<lableArray.length; i++){
paint.setColor(lableColor[i]);
canvas.drawCircle(startX+radis, getPaddingTop()+lableHeight/2, radis, paint);
startX += radis*2+lableSpace;
canvas.drawText(lableArray[i],startX,getPaddingTop()+lableLead, paintLabel);
startX += FontUtil.getFontlength(paintLabel, lableArray[i]) + itemSpace;
}
}
/**绘制X轴方向辅助网格*/
private void drawGrid(Canvas canvas){
float yMarkSpace = (rectChart.bottom - rectChart.top)/(YMARK_NUM-1);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(lineWidth);
paint.setColor(defColor);
paintEffect.setStyle(Paint.Style.STROKE);
paintEffect.setStrokeWidth(lineWidth);
paintEffect.setColor(defColor);
canvas.drawLine(rectChart.left, rectChart.top, rectChart.left, rectChart.bottom, paint);
canvas.drawLine(rectChart.right, rectChart.top, rectChart.right, rectChart.bottom, paint);
if(chartType==ChartType.TYPE_T_SOUTH||chartType==ChartType.TYPE_T_NORTH){
//今日图表,需要绘制x刻度线
for(DataPoint lable : lableXPointList){
canvas.drawLine(lable.getValueY(), rectChart.bottom, lable.getValueY(),
rectChart.bottom-DensityUtil.dip2px(getContext(), 3), paint);
}
}
for (int i = 0; i < YMARK_NUM; i++) {
if(i==0||i ==YMARK_NUM-1) { //实线
canvas.drawLine(rectChart.left, rectChart.bottom-yMarkSpace*i,
rectChart.right, rectChart.bottom-yMarkSpace*i, paint);
} else { //虚线
Path path = new Path();
path.moveTo(rectChart.left, rectChart.bottom-yMarkSpace*i);
path.lineTo(rectChart.right,rectChart.bottom-yMarkSpace*i);
PathEffect effects = new DashPathEffect(new float[]{15,8,15,8},0);
paintEffect.setPathEffect(effects);
canvas.drawPath(path, paintEffect);
}
}
}
/**绘制X轴刻度*/
private void drawXLable(Canvas canvas){
paintLabel.setTextSize(textSize);
paintLabel.setColor(textColor);
for(DataPoint lable : lableXPointList){
canvas.drawText(lable.getValueX(), lable.getPoint().x, lable.getPoint().y, paintLabel);
}
}
/**绘制Y轴刻度*/
private void drawYLable(Canvas canvas){
paintLabel.setTextSize(textSize);
paintLabel.setColor(textColor);
for(DataPoint lable : lableYLPointList){
canvas.drawText(lable.getValueY()+"", lable.getPoint().x, lable.getPoint().y, paintLabel);
}
for(DataPoint lable : lableYRPointList){
canvas.drawText(lable.getValueY()+"", lable.getPoint().x, lable.getPoint().y, paintLabel);
}
}
/**绘制曲线*/
private void drawDataPath(Canvas canvas) {
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setStrokeJoin(Paint.Join.ROUND);
paint.setStrokeWidth(lineSize);
if(chartType==ChartType.TYPE_T_SOUTH||chartType==ChartType.TYPE_T_NORTH){
//今日 ["1558","27.3亿元","26208.240","+0.56%"]
for(int j = 0; j < linePointList.size(); j++){ //先画右边刻度线,再画左边
List<DataPoint> lineList = linePointList.get(j);
paint.setColor(lableColor[j]);
Path path = new Path();
PointF lastPoint = null;
for(int i = 0 ; i<lineList.size(); i++){
if(i == 0){
path.moveTo(lineList.get(i).getPoint().x, lineList.get(i).getPoint().y);
}else{
//quadTo:二阶贝塞尔曲线连接前后两点,这样使得曲线更加平滑
path.quadTo(lastPoint.x, lastPoint.y, lineList.get(i).getPoint().x, lineList.get(i).getPoint().y);
}
lastPoint = lineList.get(i).getPoint();
if(i==lineList.size()-1){
//绘制今日图 最后一个点的值,显示在折线上
paintLabel.setColor(lableColor[j]);
paintLabel.setTextSize(textSize);
float length = FontUtil.getFontlength(paintLabel, lineList.get(i).getValueY()+"");
canvas.drawText(lineList.get(i).getValueY()+"",
(lastPoint.x + length/2)>rectChart.right? rectChart.right : (lastPoint.x - length/2),
lastPoint.y - DensityUtil.dip2px(getContext(), 6) - lableHeight + lableLead,
paintLabel);
}
}
canvas.drawPath(path, paint);
paint.setStyle(Paint.Style.FILL);
paint.setAlpha(100); //设置alpha不透明度,范围为0~255
canvas.drawCircle(lastPoint.x, lastPoint.y, DensityUtil.dip2px(getContext(), 5), paint);
paint.setAlpha(255); //设置alpha不透明度,范围为0~255
canvas.drawCircle(lastPoint.x, lastPoint.y, DensityUtil.dip2px(getContext(), 2), paint);
paint.setStyle(Paint.Style.STROKE);
}
}else{
//历史 ["2019-07-05","37.60亿元","28774.83","+0.81%"]
//绘制柱子
List<DataPoint> barList = linePointList.get(0);
paint.setStyle(Paint.Style.FILL);
for(int i = 0 ;i<barList.size(); i++) {
DataPoint dataPoint = barList.get(i);
//"37.60亿元" 净流入、流出颜色选择
//以中心点0为分割线,红色向上,绿色向下
if(dataPoint.getValueY()>0){
paint.setColor(upDownColor[0]); //红色
canvas.drawRect(new Rect((int)(dataPoint.getPoint().x-barSize/2),
(int)dataPoint.getPoint().y,
(int)(dataPoint.getPoint().x+barSize/2),
(int)centerPoint.y), paint);
}else{
paint.setColor(upDownColor[1]); //绿色
canvas.drawRect(new Rect((int)(dataPoint.getPoint().x-barSize/2),
(int)centerPoint.y,
(int)(dataPoint.getPoint().x+barSize/2),
(int)dataPoint.getPoint().y), paint);
}
}
//绘制指数线
List<DataPoint> lineList = linePointList.get(1);
paint.setStyle(Paint.Style.STROKE);
paint.setColor(lableColor[2]);
Path path = new Path();
PointF lastPoint = null;
for(int i = 0 ; i<lineList.size(); i++){
if(i == 0){
path.moveTo(lineList.get(i).getPoint().x, lineList.get(i).getPoint().y);
}else{
//quadTo:二阶贝塞尔曲线连接前后两点,这样使得曲线更加平滑
path.quadTo(lastPoint.x, lastPoint.y, lineList.get(i).getPoint().x, lineList.get(i).getPoint().y);
}
lastPoint = lineList.get(i).getPoint();
}
canvas.drawPath(path, paint);
}
}
/**绘制焦点*/
private void drawFocus(Canvas canvas){
if(!onFocus || null==focusInfo)
return;
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.FILL);
paint.setStrokeWidth(focusLineSize);
paint.setColor(focusLineColor);
//垂直方向焦点线
canvas.drawLine(focusInfo.getDataPoints().get(0).getPoint().x, rectChart.bottom,
focusInfo.getDataPoints().get(0).getPoint().x, rectChart.top, paint);
int radis = DensityUtil.dip2px(getContext(), 2);
if(chartType==ChartType.TYPE_T_SOUTH||chartType==ChartType.TYPE_T_NORTH){
//横向焦点线
canvas.drawLine(rectChart.left, focusInfo.getDataPoints().get(0).getPoint().y,
rectChart.right, focusInfo.getDataPoints().get(0).getPoint().y, paint);
//折线上面焦点圆圈
for(int i = 0; i< focusInfo.getDataPoints().size(); i++){
DataPoint dataPoint = focusInfo.getDataPoints().get(i);
paint.setColor(lableColor[i]);
canvas.drawCircle(dataPoint.getPoint().x, dataPoint.getPoint().y, radis, paint);
}
}else{
//历史图,指数焦点
DataPoint dataPoint = focusInfo.getDataPoints().get(1); //指数
canvas.drawLine(rectChart.left, dataPoint.getPoint().y,
rectChart.right, dataPoint.getPoint().y, paint);
paint.setColor(lableColor[2]);
canvas.drawCircle(dataPoint.getPoint().x, dataPoint.getPoint().y, radis, paint);
}
//绘制焦点数据显示面板
paintLabel.setTextSize(focusTextSize);
String maxStr = "恒生指数跌涨浮 00.00亿元";
float maxStrLength = FontUtil.getFontlength(paintLabel, maxStr);
float txSpace = DensityUtil.dip2px(getContext(), 2);
float txHeight = FontUtil.getFontHeight(paintLabel);
float txLead = FontUtil.getFontLeading(paintLabel);
float panelWidth = maxStrLength+txSpace*4;
float panelHeight = FontUtil.getFontHeight(paintLabel)*4+txSpace*3+txSpace*4;//四行字,3个字行距,2个2倍上下间距
paint.setColor(focusPanelColor);
RectF rectF;
if(focusInfo.getPoint().x>=centerPoint.x) //绘制在左上角
rectF = new RectF(rectChart.left, rectChart.top, rectChart.left+panelWidth, rectChart.top+panelHeight);
else
rectF = new RectF(rectChart.right-panelWidth, rectChart.top, rectChart.right, rectChart.top+panelHeight);
//绘制面板背景
paint.setColor(focusPanelColor);
paint.setStyle(Paint.Style.FILL);
canvas.drawRect(rectF, paint);
//绘制文字
if(chartType==ChartType.TYPE_T_SOUTH||chartType==ChartType.TYPE_T_NORTH){
//["0930","1.0亿元","26300.510","+0.91%"]
// new String[]{"时间", "净流入金额", "恒生指数价格", "恒生指数跌涨幅"}
drawFocusText(canvas, focusLableArray[0], focusInfo.getFocusData().get(0), focusTextColor, focusTextColor,1, rectF, txSpace, txHeight, txLead);
drawFocusText(canvas, focusLableArray[1], focusInfo.getFocusData().get(1), focusTextColor, focusTextColor,2, rectF, txSpace, txHeight, txLead);
int udColor = Float.parseFloat(focusInfo.getFocusData().get(3).replace("%", ""))>0?upDownColor[0]:upDownColor[1];
drawFocusText(canvas, focusLableArray[2], focusInfo.getFocusData().get(2), focusTextColor, udColor,3, rectF, txSpace, txHeight, txLead);
drawFocusText(canvas, focusLableArray[3], focusInfo.getFocusData().get(3), focusTextColor, udColor,4, rectF, txSpace, txHeight, txLead);
}else{
//["2019-11-14","22.85亿元","2909.87","+0.16%"]
drawFocusText(canvas, focusLableArray[0], focusInfo.getFocusData().get(0), focusTextColor, focusTextColor,1, rectF, txSpace, txHeight, txLead);
drawFocusText(canvas, focusLableArray[1], focusInfo.getFocusData().get(1), focusTextColor, focusTextColor,2, rectF, txSpace, txHeight, txLead);
int udColor = Float.parseFloat(focusInfo.getFocusData().get(3).replace("%", ""))>0?upDownColor[0]:upDownColor[1];
drawFocusText(canvas, focusLableArray[2], focusInfo.getFocusData().get(2), focusTextColor, udColor,3, rectF, txSpace, txHeight, txLead);
drawFocusText(canvas, focusLableArray[3], focusInfo.getFocusData().get(3), focusTextColor, udColor,4, rectF, txSpace, txHeight, txLead);
}
}
private void drawFocusText(Canvas canvas, String text, String value, int txColor1, int txColor2,
int line, RectF rectF, float txSpace, float txHeight, float txLead){
float y = rectF.top + txSpace*2 + (line-1)*(txSpace+txHeight) + txLead;
paintLabel.setColor(txColor1);
canvas.drawText(text, rectF.left+txSpace*2, y, paintLabel);
paintLabel.setColor(txColor2);
canvas.drawText(value, rectF.right - txSpace*2 - FontUtil.getFontlength(paintLabel, value), y, paintLabel);
}
private float animPro; //动画计算的占比数量
/**创建动画*/
@Override
protected ValueAnimator initAnim() {
if(dataList.size()>0) {
ValueAnimator anim = ValueAnimator.ofObject(new AngleEvaluator(), 0f, 1f);
anim.setInterpolator(new AccelerateDecelerateInterpolator());
return anim;
}
return null;
}
/**动画值变化之后计算数据*/
@Override
protected void evaluatorData(ValueAnimator animation) {
animPro = (float)animation.getAnimatedValue();
}
}
@@ -0,0 +1,114 @@
package com.openxu.cview.xmstock20191205;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathMeasure;
import android.graphics.RectF;
import android.graphics.SweepGradient;
import android.util.AttributeSet;
import android.view.View;
/**
* Author: openXu
* Time: 2020/4/9 14:13
* class: OilTableLine
* Description:
*/
public class OilTableLine extends View {
public static final int[] SWEEP_GRADIENT_COLORS = new int[]{Color.GREEN, Color.GREEN, Color.BLUE, Color.RED, Color.RED};
private int tableWidth = 50;
private Paint mPaint;
private Path mPath;
private RectF mTableRectF;
//把路径分成虚线段的
private DashPathEffect dashPathEffect;
//给路径上色
private SweepGradient mColorShader;
//指针的路径
private Path mPointerPath;
private float mCurrentDegree = 60;
public OilTableLine(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setDither(true);
mPaint.setColor(Color.BLACK);
mPath = new Path();
mPointerPath = new Path();
startAnimator();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
float size = Math.min(w, h) - tableWidth * 2;
//油表的位置方框
mTableRectF = new RectF(0, 0, size, size);
mPath.reset();
//在油表路径中增加一个从起始弧度
mPath.addArc(mTableRectF, 60, 240);
//计算路径的长度
PathMeasure pathMeasure = new PathMeasure(mPath, false);
float length = pathMeasure.getLength();
float step = length / 60;
dashPathEffect = new DashPathEffect(new float[]{step / 3, step * 2 / 3}, 0);
float radius = size / 2;
mColorShader = new SweepGradient(radius, radius,SWEEP_GRADIENT_COLORS,null);
//设置指针的路径位置
mPointerPath.reset();
mPointerPath.moveTo(radius, radius - 20);
mPointerPath.lineTo(radius, radius + 20);
mPointerPath.lineTo(radius * 2 - tableWidth, radius);
mPointerPath.close();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float dx = (getWidth() - mTableRectF.width()) / 2;
float dy = (getHeight() - mTableRectF.height()) / 2;
//把油表的方框平移到正中间
canvas.translate(dx, dy);
canvas.save();
//旋转画布
canvas.rotate(90, mTableRectF.width() / 2, mTableRectF.height() / 2);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(tableWidth);
mPaint.setPathEffect(dashPathEffect);
mPaint.setShader(mColorShader);
canvas.drawPath(mPath, mPaint);
canvas.restore();
//还原画笔
mPaint.setPathEffect(null);
mPaint.setShader(null);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setStrokeWidth(tableWidth / 10);
canvas.save();
canvas.rotate(150 + mCurrentDegree, mTableRectF.width() / 2, mTableRectF.height() / 2);
canvas.drawPath(mPointerPath, mPaint);
canvas.restore();
}
public void startAnimator() {
ValueAnimator animator = ValueAnimator.ofFloat(0, 240);
animator.setDuration(40000);
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.setRepeatMode(ValueAnimator.RESTART);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mCurrentDegree = (int) (0 + (Float) animation.getAnimatedValue());
invalidate();
}
});
animator.start();
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,46 @@
package com.openxu.cview.xmstock20191205.bean;
import android.graphics.PointF;
import com.openxu.cview.xmstock.bean.DataPoint;
import java.util.List;
/**
* autour : xiami
* date : 2018/3/14 10:23
* className : FocusInfo
* version : 1.0
* description : 触摸焦点封装
*/
public class NSFocusInfo {
private PointF point;
private List<DataPoint> dataPoints;
private List<String> focusData;
public PointF getPoint() {
return point;
}
public void setPoint(PointF point) {
this.point = point;
}
public List<DataPoint> getDataPoints() {
return dataPoints;
}
public void setDataPoints(List<DataPoint> dataPoints) {
this.dataPoints = dataPoints;
}
public List<String> getFocusData() {
return focusData;
}
public void setFocusData(List<String> focusData) {
this.focusData = focusData;
}
}
@@ -0,0 +1,22 @@
package com.openxu.cview.xmstock20191205.bean;
import java.util.List;
/**
* Author: openXu
* Time: 2019/12/5 17:22
* class: NorthSouth
* Description:
*/
public class NorthSouth {
private List<List<String>> data;
public List<List<String>> getData() {
return data;
}
public void setData(List<List<String>> data) {
this.data = data;
}
}
@@ -0,0 +1,609 @@
package com.openxu.cview.xmstock20201030;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.PathEffect;
import android.graphics.PointF;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import com.openxu.cview.R;
import com.openxu.cview.xmstock20201030.bean.DayFinish;
import com.openxu.utils.FontUtil;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* author : openXu
* create at : 2017/1/3 9:56
* blog : http://blog.csdn.net/xmxkf
* gitHub : https://github.com/openXu
* project : CustomCalendar
* class name : CustomCalendar
* version : 1.0
* class describe:自定义日历控件
*/
public class CustomCalendar extends View {
private String TAG = "CustomCalendar";
/** 各部分背景*/
private int mBgMonth, mBgWeek, mBgDay, mBgPre;
/** 标题的颜色、大小*/
private int mTextColorMonth;
private float mTextSizeMonth;
private int mMonthRowL, mMonthRowR;
private float mMonthRowSpac;
private float mMonthSpac;
/** 星期的颜色、大小*/
private int mTextColorWeek, mSelectWeekTextColor;
private float mTextSizeWeek;
/** 日期文本的颜色、大小*/
private int mTextColorDay;
private float mTextSizeDay;
/** 任务次数文本的颜色、大小*/
private int mTextColorPreFinish, mTextColorPreUnFinish, mTextColorPreNull;
private float mTextSizePre;
/** 选中的文本的颜色*/
private int mSelectTextColor;
/** 选中背景*/
private int mSelectBg, mCurrentBg;
private float mSelectRadius, mCurrentBgStrokeWidth;
private float[] mCurrentBgDashPath;
/** 行间距*/
private float mLineSpac;
/** 字体上下间距*/
private float mTextSpac;
private Paint mPaint;
private Paint bgPaint;
private float titleHeight, weekHeight, dayHeight, preHeight, oneHeight;
private int columnWidth; //每列宽度
private Date month; //当前的月份
private boolean isCurrentMonth; //展示的月份是否是当前月
private int currentDay, selectDay, lastSelectDay; //当前日期 、 选中的日期 、上一次选中的日期(避免造成重复回调请求)
private int dayOfMonth; //月份天数
private int firstIndex; //当月第一天位置索引
private int todayWeekIndex;//今天是星期几
private int firstLineNum, lastLineNum; //第一行、最后一行能展示多少日期
private int lineNum; //日期行数
private String[] WEEK_STR = new String[]{"Sun", "Mon", "Tues", "Wed", "Thur", "Fri", "Sat", };
public CustomCalendar(Context context) {
this(context, null);
}
public CustomCalendar(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomCalendar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//获取自定义属性的值
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomCalendar, defStyleAttr, 0);
mBgMonth = a.getColor(R.styleable.CustomCalendar_mBgMonth, Color.TRANSPARENT);
mBgWeek = a.getColor(R.styleable.CustomCalendar_mBgWeek, Color.TRANSPARENT);
mBgDay = a.getColor(R.styleable.CustomCalendar_mBgDay, Color.TRANSPARENT);
mBgPre = a.getColor(R.styleable.CustomCalendar_mBgPre, Color.TRANSPARENT);
mMonthRowL = a.getResourceId(R.styleable.CustomCalendar_mMonthRowL, R.mipmap.custom_calendar_row_left);
mMonthRowR = a.getResourceId(R.styleable.CustomCalendar_mMonthRowR, R.mipmap.custom_calendar_row_right);
mMonthRowSpac = a.getDimension(R.styleable.CustomCalendar_mMonthRowSpac, 20);
mTextColorMonth = a.getColor(R.styleable.CustomCalendar_mTextColorMonth, Color.BLACK);
mTextSizeMonth = a.getDimension(R.styleable.CustomCalendar_mTextSizeMonth, 100);
mMonthSpac = a.getDimension(R.styleable.CustomCalendar_mMonthSpac, 20);
mTextColorWeek = a.getColor(R.styleable.CustomCalendar_mTextColorWeek, Color.BLACK);
mSelectWeekTextColor = a.getColor(R.styleable.CustomCalendar_mSelectWeekTextColor, Color.BLACK);
mTextSizeWeek = a.getDimension(R.styleable.CustomCalendar_mTextSizeWeek, 70);
mTextColorDay = a.getColor(R.styleable.CustomCalendar_mTextColorDay, Color.GRAY);
mTextSizeDay = a.getDimension(R.styleable.CustomCalendar_mTextSizeDay, 70);
mTextColorPreFinish = a.getColor(R.styleable.CustomCalendar_mTextColorPreFinish, Color.BLUE);
mTextColorPreUnFinish = a.getColor(R.styleable.CustomCalendar_mTextColorPreUnFinish, Color.BLUE);
mTextColorPreNull = a.getColor(R.styleable.CustomCalendar_mTextColorPreNull, Color.BLUE);
mTextSizePre = a.getDimension(R.styleable.CustomCalendar_mTextSizePre, 40);
mSelectTextColor = a.getColor(R.styleable.CustomCalendar_mSelectTextColor, Color.YELLOW);
mCurrentBg = a.getColor(R.styleable.CustomCalendar_mCurrentBg, Color.GRAY);
try {
int dashPathId = a.getResourceId(R.styleable.CustomCalendar_mCurrentBgDashPath, R.array.customCalendar_currentDay_bg_DashPath);
int[] array = getResources().getIntArray(dashPathId);
mCurrentBgDashPath = new float[array.length];
for(int i=0;i<array.length;i++){
mCurrentBgDashPath[i]=array[i];
}
}catch (Exception e){
e.printStackTrace();
mCurrentBgDashPath = new float[]{2, 3, 2, 3};
}
mSelectBg = a.getColor(R.styleable.CustomCalendar_mSelectBg, Color.YELLOW);
mSelectRadius = a.getDimension(R.styleable.CustomCalendar_mSelectRadius, 20);
mCurrentBgStrokeWidth = a.getDimension(R.styleable.CustomCalendar_mCurrentBgStrokeWidth, 5);
mLineSpac = a.getDimension(R.styleable.CustomCalendar_mLineSpac, 20);
mTextSpac = a.getDimension(R.styleable.CustomCalendar_mTextSpac, 20);
a.recycle(); //注意回收
initCompute();
}
/**计算相关常量,构造方法中调用*/
private void initCompute(){
mPaint = new Paint();
bgPaint = new Paint();
mPaint.setAntiAlias(true); //抗锯齿
bgPaint.setAntiAlias(true); //抗锯齿
map = new HashMap<>();
//标题高度
mPaint.setTextSize(mTextSizeMonth);
titleHeight = FontUtil.getFontHeight(mPaint) + 2 * mMonthSpac;
//星期高度
mPaint.setTextSize(mTextSizeWeek);
weekHeight = FontUtil.getFontHeight(mPaint);
//日期高度
mPaint.setTextSize(mTextSizeDay);
dayHeight = FontUtil.getFontHeight(mPaint);
//次数字体高度
mPaint.setTextSize(mTextSizePre);
preHeight = FontUtil.getFontHeight(mPaint);
//每行高度 = 行间距 + 日期字体高度 + 字间距 + 次数字体高度
oneHeight = mLineSpac + dayHeight + mTextSpac + preHeight;
//默认当前月份
String cDateStr = getMonthStr(new Date());
// cDateStr = "2015年08月";
setMonth(cDateStr);
}
/**设置月份*/
private void setMonth(String Month){
//设置的月份(2017年01月)
month = str2Date(Month);
Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date());
//获取今天是多少号
currentDay = calendar.get(Calendar.DAY_OF_MONTH);
todayWeekIndex = calendar.get(Calendar.DAY_OF_WEEK)-1;
Date cM = str2Date(getMonthStr(new Date()));
//判断是否为当月
if(cM.getTime() == month.getTime()){
isCurrentMonth = true;
selectDay = currentDay;//当月默认选中当前日
}else{
isCurrentMonth = false;
selectDay = 0;
}
Log.d(TAG, "设置月份:"+month+" 今天"+currentDay+"号, 是否为当前月:"+isCurrentMonth);
calendar.setTime(month);
dayOfMonth = calendar.getActualMaximum(Calendar.DAY_OF_MONTH);
//第一行1号显示在什么位置(星期几)
firstIndex = calendar.get(Calendar.DAY_OF_WEEK)-1;
lineNum = 1;
//第一行能展示的天数
firstLineNum = 7-firstIndex;
lastLineNum = 0;
int shengyu = dayOfMonth - firstLineNum;
while (shengyu>7){
lineNum ++;
shengyu-=7;
}
if(shengyu>0){
lineNum ++;
lastLineNum = shengyu;
}
Log.i(TAG, getMonthStr(month)+"一共有"+dayOfMonth+"天,第一天的索引是:"+firstIndex+" 有"+lineNum+
"行,第一行"+firstLineNum+"个,最后一行"+lastLineNum+"个");
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//宽度 = 填充父窗体
int widthSize = MeasureSpec.getSize(widthMeasureSpec); //获取宽的尺寸
columnWidth = widthSize / 7;
//高度 = 标题高度 + 星期高度 + 日期行数*每行高度
float height = titleHeight + weekHeight + (lineNum * oneHeight);
Log.v(TAG, "标题高度:"+titleHeight+" 星期高度:"+weekHeight+" 每行高度:"+oneHeight+
" 行数:"+ lineNum + " \n控件高度:"+height);
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
(int)height);
}
@Override
protected void onDraw(Canvas canvas) {
drawMonth(canvas);
drawWeek(canvas);
drawDayAndPre(canvas);
}
/**绘制月份*/
private int rowLStart, rowRStart, rowWidth;
private void drawMonth(Canvas canvas){
//背景
bgPaint.setColor(mBgMonth);
RectF rect = new RectF(0, 0, getWidth(), titleHeight);
canvas.drawRect(rect, bgPaint);
//绘制月份
mPaint.setTextSize(mTextSizeMonth);
mPaint.setColor(mTextColorMonth);
float textLen = FontUtil.getFontlength(mPaint, getMonthStr(month));
float textStart = (getWidth() - textLen)/ 2;
canvas.drawText(getMonthStr(month), textStart,
mMonthSpac+FontUtil.getFontLeading(mPaint), mPaint);
/*绘制左右箭头*/
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), mMonthRowL);
int h = bitmap.getHeight();
rowWidth = bitmap.getWidth();
//float left, float top
rowLStart = (int)(textStart-2*mMonthRowSpac-rowWidth);
canvas.drawBitmap(bitmap, rowLStart+mMonthRowSpac , (titleHeight - h)/2, new Paint());
bitmap = BitmapFactory.decodeResource(getResources(), mMonthRowR);
rowRStart = (int)(textStart+textLen);
canvas.drawBitmap(bitmap, rowRStart+mMonthRowSpac, (titleHeight - h)/2, new Paint());
}
/**绘制绘制星期*/
private void drawWeek(Canvas canvas){
//背景
bgPaint.setColor(mBgWeek);
RectF rect = new RectF(0, titleHeight, getWidth(), titleHeight + weekHeight);
canvas.drawRect(rect, bgPaint);
//绘制星期:七天
mPaint.setTextSize(mTextSizeWeek);
for(int i = 0; i < WEEK_STR.length; i++){
if(todayWeekIndex == i && isCurrentMonth){
mPaint.setColor(mSelectWeekTextColor);
}else{
mPaint.setColor(mTextColorWeek);
}
int len = (int)FontUtil.getFontlength(mPaint, WEEK_STR[i]);
int x = i * columnWidth + (columnWidth - len)/2;
canvas.drawText(WEEK_STR[i], x, titleHeight + FontUtil.getFontLeading(mPaint), mPaint);
}
}
/**绘制日期和次数*/
private void drawDayAndPre(Canvas canvas){
//某行开始绘制的Y坐标,第一行开始的坐标为标题高度+星期部分高度
float top = titleHeight+weekHeight;
//行
for(int line = 0; line < lineNum; line++){
if(line == 0){
//第一行
drawDayAndPre(canvas, top, firstLineNum, 0, firstIndex);
}else if(line == lineNum-1){
//最后一行
top += oneHeight;
drawDayAndPre(canvas, top, lastLineNum, firstLineNum+(line-1)*7, 0);
}else{
//满行
top += oneHeight;
drawDayAndPre(canvas, top, 7, firstLineNum+(line-1)*7, 0);
}
}
}
/**
* 绘制某一行的日期
* @param canvas
* @param top 顶部坐标
* @param count 此行需要绘制的日期数量(不一定都是7天)
* @param overDay 已经绘制过的日期,从overDay+1开始绘制
* @param startIndex 此行第一个日期的星期索引
*/
private void drawDayAndPre(Canvas canvas, float top,
int count, int overDay, int startIndex){
// Log.e(TAG, "总共"+dayOfMonth+"天 有"+lineNum+"行"+ " 已经画了"+overDay+"天,下面绘制:"+count+"天");
//背景
float topPre = top + mLineSpac + dayHeight;
bgPaint.setColor(mBgDay);
RectF rect = new RectF(0, top, getWidth(), topPre);
canvas.drawRect(rect, bgPaint);
bgPaint.setColor(mBgPre);
rect = new RectF(0, topPre, getWidth(), topPre + mTextSpac + dayHeight);
canvas.drawRect(rect, bgPaint);
mPaint.setTextSize(mTextSizeDay);
float dayTextLeading = FontUtil.getFontLeading(mPaint);
mPaint.setTextSize(mTextSizePre);
float preTextLeading = FontUtil.getFontLeading(mPaint);
// Log.v(TAG, "当前日期:"+currentDay+" 选择日期:"+selectDay+" 是否为当前月:"+isCurrentMonth);
for(int i = 0; i<count; i++){
int left = (startIndex + i)*columnWidth;
int day = (overDay+i+1);
mPaint.setTextSize(mTextSizeDay);
//如果是当前月,当天日期需要做处理
if(isCurrentMonth && currentDay == day){
mPaint.setColor(mTextColorDay);
bgPaint.setColor(mCurrentBg);
bgPaint.setStyle(Paint.Style.STROKE); //空心
PathEffect effect = new DashPathEffect(mCurrentBgDashPath, 1);
bgPaint.setPathEffect(effect); //设置画笔曲线间隔
bgPaint.setStrokeWidth(mCurrentBgStrokeWidth); //画笔宽度
//绘制空心圆背景
canvas.drawCircle(left+columnWidth/2, top + mLineSpac +dayHeight/2,
mSelectRadius-mCurrentBgStrokeWidth, bgPaint);
}
//绘制完后将画笔还原,避免脏笔
bgPaint.setPathEffect(null);
bgPaint.setStrokeWidth(0);
bgPaint.setStyle(Paint.Style.FILL);
//选中的日期,如果是本月,选中日期正好是当天日期,下面的背景会覆盖上面绘制的虚线背景
if(selectDay == day){
//选中的日期字体白色,橙色背景
mPaint.setColor(mSelectTextColor);
bgPaint.setColor(mSelectBg);
//绘制橙色圆背景,参数一是中心点的x轴,参数二是中心点的y轴,参数三是半径,参数四是paint对象;
canvas.drawCircle(left+columnWidth/2, top + mLineSpac +dayHeight/2, mSelectRadius, bgPaint);
}else{
mPaint.setColor(mTextColorDay);
}
int len = (int)FontUtil.getFontlength(mPaint, day+"");
int x = left + (columnWidth - len)/2;
canvas.drawText(day+"", x, top + mLineSpac + dayTextLeading, mPaint);
//绘制次数
mPaint.setTextSize(mTextSizePre);
DayFinish finish = map.get(day);
String preStr = "0/0";
if(isCurrentMonth){
if(day>currentDay){
mPaint.setColor(mTextColorPreNull);
}else if(finish!=null){
//区分完成未完成
if(finish.finish >= finish.all) {
mPaint.setColor(mTextColorPreFinish);
}else{
mPaint.setColor(mTextColorPreUnFinish);
}
preStr = finish.finish+"/"+finish.all;
}else{
mPaint.setColor(mTextColorPreNull);
}
}else{
if(finish!=null){
//区分完成未完成
if(finish.finish >= finish.all) {
mPaint.setColor(mTextColorPreFinish);
}else{
mPaint.setColor(mTextColorPreUnFinish);
}
preStr = finish.finish+"/"+finish.all;
}else{
mPaint.setColor(mTextColorPreNull);
}
}
len = (int)FontUtil.getFontlength(mPaint, preStr);
x = left + (columnWidth - len)/2;
canvas.drawText(preStr, x, topPre + mTextSpac + preTextLeading, mPaint);
}
}
/**获取月份标题*/
private String getMonthStr(Date month){
SimpleDateFormat df = new SimpleDateFormat("yyyy年MM月");
return df.format(month);
}
private Date str2Date(String str){
try {
SimpleDateFormat df = new SimpleDateFormat("yyyy年MM月");
return df.parse(str);
}catch (Exception e){
e.printStackTrace();
return null;
}
}
/****************************事件处理↓↓↓↓↓↓↓****************************/
//焦点坐标
private PointF focusPoint = new PointF();
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction() & MotionEvent.ACTION_MASK;
switch (action) {
case MotionEvent.ACTION_DOWN:
focusPoint.set(event.getX(), event.getY());
touchFocusMove(focusPoint, false);
break;
case MotionEvent.ACTION_MOVE:
focusPoint.set(event.getX(), event.getY());
touchFocusMove(focusPoint, false);
break;
case MotionEvent.ACTION_OUTSIDE:
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
focusPoint.set(event.getX(), event.getY());
touchFocusMove(focusPoint, true);
break;
}
return true;
}
/**焦点滑动*/
public void touchFocusMove(final PointF point, boolean eventEnd) {
Log.e(TAG, "点击坐标:("+point.x+" ,"+point.y+"),事件是否结束:"+eventEnd);
/**标题和星期只有在事件结束后才响应*/
if(point.y<=titleHeight){
//事件在标题上
if(eventEnd && listener!=null){
if(point.x>=rowLStart && point.x<(rowLStart+2*mMonthRowSpac+rowWidth)){
Log.w(TAG, "点击左箭头");
listener.onLeftRowClick();
}else if(point.x>rowRStart && point.x<(rowRStart + 2*mMonthRowSpac+rowWidth)){
Log.w(TAG, "点击右箭头");
listener.onRightRowClick();
}else if(point.x>rowLStart && point.x <rowRStart){
listener.onTitleClick(getMonthStr(month), month);
}
}
}else if(point.y<=(titleHeight+weekHeight)){
//事件在星期部分
if(eventEnd && listener!=null){
//根据X坐标找到具体的焦点日期
int xIndex = (int)point.x / columnWidth;
Log.e(TAG, "列宽:"+columnWidth+" x坐标余数:"+(point.x / columnWidth));
if((point.x / columnWidth-xIndex)>0){
xIndex += 1;
}
if(listener!=null){
listener.onWeekClick(xIndex-1, WEEK_STR[xIndex-1]);
}
}
}else{
/**日期部分按下和滑动时重绘,只有在事件结束后才响应*/
touchDay(point, eventEnd);
}
}
//控制事件是否响应
private boolean responseWhenEnd = false;
/**事件点在 日期区域 范围内*/
private void touchDay(final PointF point, boolean eventEnd){
//根据Y坐标找到焦点行
boolean availability = false; //事件是否有效
//日期部分
float top = titleHeight+weekHeight+oneHeight;
int foucsLine = 1;
while(foucsLine<=lineNum){
if(top>=point.y){
availability = true;
break;
}
top += oneHeight;
foucsLine ++;
}
if(availability){
//根据X坐标找到具体的焦点日期
int xIndex = (int)point.x / columnWidth;
if((point.x / columnWidth-xIndex)>0){
xIndex += 1;
}
// Log.e(TAG, "列宽:"+columnWidth+" x坐标余数:"+(point.x / columnWidth));
if(xIndex<=0)
xIndex = 1; //避免调到上一行最后一个日期
if(xIndex>7)
xIndex = 7; //避免调到下一行第一个日期
// Log.e(TAG, "事件在日期部分,第"+foucsLine+"/"+lineNum+"行, "+xIndex+"列");
if(foucsLine == 1){
//第一行
if(xIndex<=firstIndex){
Log.e(TAG, "点到开始空位了");
setSelectedDay(selectDay, true);
}else{
setSelectedDay(xIndex-firstIndex, eventEnd);
}
}else if(foucsLine == lineNum){
//最后一行
if(xIndex>lastLineNum){
Log.e(TAG, "点到结尾空位了");
setSelectedDay(selectDay, true);
}else{
setSelectedDay(firstLineNum + (foucsLine-2)*7+ xIndex, eventEnd);
}
}else{
setSelectedDay(firstLineNum + (foucsLine-2)*7+ xIndex, eventEnd);
}
}else{
//超出日期区域后,视为事件结束,响应最后一个选择日期的回调
setSelectedDay(selectDay, true);
}
}
/**设置选中的日期*/
private void setSelectedDay(int day, boolean eventEnd){
Log.w(TAG, "选中:"+day+" 事件是否结束"+eventEnd);
selectDay = day;
invalidate();
if(listener!=null && eventEnd && responseWhenEnd && lastSelectDay!=selectDay) {
lastSelectDay = selectDay;
listener.onDayClick(selectDay, getMonthStr(month) + selectDay + "日", map.get(selectDay));
}
responseWhenEnd = !eventEnd;
}
/****************************事件处理↑↑↑↑↑↑↑****************************/
@Override
public void invalidate() {
requestLayout();
super.invalidate();
}
/***********************接口API↓↓↓↓↓↓↓**************************/
private Map<Integer, DayFinish> map;
public void setRenwu(String month, List<DayFinish> list){
setMonth(month);
if(list!=null && list.size()>0){
map.clear();
for(DayFinish finish : list){
map.put(finish.day, finish);
}
}
invalidate();
}
public void setRenwu(List<DayFinish> list){
if(list!=null && list.size()>0){
map.clear();
for(DayFinish finish : list){
map.put(finish.day, finish);
}
}
invalidate();
}
/**月份增减*/
public void monthChange(int change){
Calendar calendar = Calendar.getInstance();
calendar.setTime(month);
calendar.add(Calendar.MONTH, change);
setMonth(getMonthStr(calendar.getTime()));
map.clear();
invalidate();
}
private onClickListener listener;
public void setOnClickListener(onClickListener listener){
this.listener = listener;
}
public interface onClickListener{
public abstract void onLeftRowClick();
public abstract void onRightRowClick();
public abstract void onTitleClick(String monthStr, Date month);
public abstract void onWeekClick(int weekIndex, String weekStr);
public abstract void onDayClick(int day, String dayStr, DayFinish finish);
}
/***********************接口API↑↑↑↑↑↑↑**************************/
}
@@ -0,0 +1,578 @@
package com.openxu.cview.xmstock20201030;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathEffect;
import android.graphics.PointF;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.animation.AccelerateDecelerateInterpolator;
import com.openxu.hkchart.config.YAxisMark;
import com.openxu.cview.R;
import com.openxu.cview.chart.anim.AngleEvaluator;
import com.openxu.cview.xmstock.BaseChart;
import com.openxu.cview.xmstock.bean.DataPoint;
import com.openxu.utils.DensityUtil;
import com.openxu.utils.FontUtil;
import com.openxu.utils.LogUtil;
import com.openxu.utils.NumberFormatUtil;
import java.util.ArrayList;
import java.util.List;
/**
* autour : xiami
* date : 2020/11/13 14:26
* className : GlzsLinesChart
* version : 1.0
* description : 2. 概念走势图
*/
public class GlzsLinesChart extends BaseChart {
//设置数据 概念走势图, 每个元素表示:时间、概念、热度 [20200803,"1582.08","30.00"],
private List<List<Object>> dataList;
private String[] lableXList;
//计算后的数据
private List<DataPoint> lableXPointList;
private List<List<DataPoint>> linePointList;
//y轴刻度设置
private YAxisMark yLeft, yRight;
/**可以设置的属性*/
//设置线条颜色(蓝色,红色)
private int[] lineColor = new int[]{Color.parseColor("#3d7cc9"),
Color.parseColor("#d74b3c")};
//设置曲线粗细
private int lineSize = DensityUtil.dip2px(getContext(), 1.5f);
//设置坐标文字大小
private int textSizeX = (int)getResources().getDimension(R.dimen.ts_chart_xy);
//设置坐标文字颜色
private int textColorX = getResources().getColor(R.color.tc_chart_xy);
//设置X坐标字体与横轴的距离
private int textSpaceX = DensityUtil.dip2px(getContext(), 5);
//设置动画类型
private AnimType animType = AnimType.SLOW_DRAW;
//设置焦点线颜色 及 粗细
private int focusLineColor = Color.parseColor("#5E5E5E");
private int focusLineSize = DensityUtil.dip2px(getContext(), 1f);
//焦点面板矩形,只记录矩形宽高
private int foucsRectTextSpace = DensityUtil.dip2px(getContext(), 3);
private int foucsRectSpace = DensityUtil.dip2px(getContext(), 6);
private float foucsRectWidth;
private float foucsRectHeight;
public enum AnimType{
LEFT_TO_RIGHT, //动画从左往右
BOTTOM_TO_TOP, //动画从下网上上升
SLOW_DRAW //动画缓慢绘制
}
/**需要计算相关值*/
// private float lableLead, lableHeight;
public GlzsLinesChart(Context context) {
this(context, null);
}
public GlzsLinesChart(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public GlzsLinesChart(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public void init(Context context, AttributeSet attrs, int defStyleAttr) {
dataList = new ArrayList<>();
touchEnable = true;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(widthSize, heightSize);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
evaluatorByData();
startDraw = false;
isLoading = false;
invalidate();
}
//20201028 --> 10-28
private String formatDate(String date){
String str = date.substring(4, 6)+"-"+date.substring(6);
Log.v(TAG, date+ "---->"+str);
return str;
}
/**
* 设置数据
*/
public void setData(List<List<Object>> dataList){
if(null==dataList)
return;
this.dataList.clear();
this.dataList.addAll(dataList);
//[20200803,"1582.08","30.00"],
Log.v(TAG, "---->"+dataList.get(0));
//重组X轴刻度, 取其中5个日期
lableXList = new String[5];
int part = dataList.size()/4;
lableXList[0] = dataList.get(0).get(0).toString(); //第一天
// Log.v(TAG, 0+ "---->"+Integer.parseInt(dataList.get(0).get(0).toString()));
lableXList[1] = dataList.get(part).get(0).toString(); //
lableXList[2] = dataList.get(dataList.size()/2).get(0).toString(); //中间一天
lableXList[3] = dataList.get(part*3).get(0).toString(); //
lableXList[4] = dataList.get(dataList.size()-1).get(0).toString(); //最后一天
for(int i = 0; i<lableXList.length; i++){
lableXList[i] = formatDate(lableXList[i]);
}
yLeft = new YAxisMark.Builder(getContext())
.lableNum(4)
.textSize(textSizeX)
.textColor(lineColor[0])
.build();
yRight = new YAxisMark.Builder(getContext())
.lableNum(4)
.textSize(textSizeX)
.textColor(lineColor[1])
.build();
if(getMeasuredWidth()>0) {
evaluatorByData();
startDraw = false;
isLoading = false;
invalidate();
}
}
/**设置数据后,计算相关值*/
private void evaluatorByData(){
if(dataList.size()<=0)
return;
/**③、计算Y刻度最大值和最小值以及幅度*/
//时间、概念、热度 [20200803,"1582.08","30.00"],
//概念
calYLable(yLeft, 1);
//热度
calYLable(yRight, 2);
/**①、计算字体相关以及图表原点坐标*/
paintLabel.setTextSize(textSizeX);
float xlableHeight = FontUtil.getFontHeight(paintLabel);
float xlableLead = FontUtil.getFontLeading(paintLabel);
float xFoucsLenght = FontUtil.getFontlength(paintLabel, lableXList[0]);
paintLabel.setTextSize(yLeft.textSize);
float yLeftlableHeight = FontUtil.getFontHeight(paintLabel);
float yLeftlableMaxLength = FontUtil.getFontlength(paintLabel, (int)yLeft.cal_mark_max+"");
float yLeftFoucsLenght = FontUtil.getFontlength(paintLabel, "概念:"+ NumberFormatUtil.formattedDecimal(yLeft.cal_mark_max));
Log.w(TAG, "计算面板最长字符串:"+"概念:"+yLeft.cal_mark_max);
paintLabel.setTextSize(yRight.textSize);
float yRightlableHeight = FontUtil.getFontHeight(paintLabel);
float yRightlableMaxLength = FontUtil.getFontlength(paintLabel, (int)yRight.cal_mark_max+"");
float yRightFoucsLenght = FontUtil.getFontlength(paintLabel, "热度:"+NumberFormatUtil.formattedDecimal(yRight.cal_mark_max));
Log.w(TAG, "计算面板最长字符串:"+"热度:"+yRight.cal_mark_max);
//图表主体矩形
rectChart = new RectF(getPaddingLeft() + yLeft.textSpace + yLeftlableMaxLength,
getPaddingTop() + Math.max(yLeftlableHeight, yRightlableHeight)/2,
getMeasuredWidth()-getPaddingRight()-yRight.textSpace - yRightlableMaxLength,
getMeasuredHeight()-getPaddingBottom() - xlableHeight - textSpaceX);
//焦点面板
//10-16
//概念:1234.02
//热度:45.00
foucsRectWidth = Math.max(xFoucsLenght, Math.max(yLeftFoucsLenght, yRightFoucsLenght)) + foucsRectSpace*2;
foucsRectHeight = xlableHeight + yLeftlableHeight + yRightlableHeight + foucsRectTextSpace*2 + foucsRectSpace*2;
/**②、计算X标签绘制坐标*/
float lableXSpace = 0;
for(String lableX : lableXList){
lableXSpace += FontUtil.getFontlength(paintLabel, lableX);
}
lableXSpace = (rectChart.right - rectChart.left - lableXSpace)/(lableXList.length -1);
lableXPointList = new ArrayList<>();
if(lableXSpace>0){
float left = rectChart.left;
for(int i = 0; i<lableXList.length; i++){
String lableX = lableXList[i];
lableXPointList.add(new DataPoint(lableX, 0, new PointF(
left,rectChart.bottom + textSpaceX + xlableLead)));
left += (FontUtil.getFontlength(paintLabel, lableXList[i])+lableXSpace);
}
}else{
//如果X轴标签字体过长的情况需要特殊处理
float oneWidth = (rectChart.right - rectChart.left)/lableXList.length;
for(int i = 0; i<lableXList.length; i++){
String lableX = lableXList[i];
float xLen = FontUtil.getFontlength(paintLabel, lableX);
lableXPointList.add(new DataPoint(lableX, 0, new PointF(
rectChart.left+i*oneWidth+(oneWidth-xLen)/2,rectChart.bottom + textSpaceX + xlableLead)));
}
}
/**④、计算点的坐标,如果有动画的情况下,边绘制边计算会耗费性能,所以先计算*/
linePointList = new ArrayList<>();
//两条线
linePointList.add(new ArrayList<>());
linePointList.add(new ArrayList<>());
float oneSpace = (rectChart.right - rectChart.left) / (dataList.size()-1);
for(int i = 0; i < dataList.size(); i++){
//每个元素表示:时间、概念、热度 [20200803,"1582.08","30.00"],
List<Object> onePart = dataList.get(i);
try {
//第一条线的数据
float valueY = Float.parseFloat(onePart.get(1).toString());
PointF point = new PointF(rectChart.left + i * oneSpace,
rectChart.bottom -
(rectChart.bottom-rectChart.top)/(yLeft.cal_mark_max - yLeft.cal_mark_min) * (valueY-yLeft.cal_mark_min));
linePointList.get(0).add(new DataPoint(onePart.get(0).toString(), valueY, point));
//第二条线的数据
valueY = Float.parseFloat(onePart.get(2).toString());
// Log.w(TAG, i+"计算右侧y值"+valueY);
point = new PointF(rectChart.left + i * oneSpace,
rectChart.bottom - (rectChart.bottom-rectChart.top)/(yRight.cal_mark_max - yRight.cal_mark_min) * (valueY-yRight.cal_mark_min));
linePointList.get(1).add(new DataPoint(onePart.get(0).toString(), valueY, point));
}catch (Exception e){
e.printStackTrace();
}
}
}
private FocusData focusData;
@Override
protected void onTouchMoved(PointF point) {
if(null==dataList)
return;
onFocus = (null != point);
if(null != point && null!=dataList && dataList.size()>0) {
//避免滑出
if(point.x > linePointList.get(0).get(dataList.size()-1).getPoint().x)
point.x = linePointList.get(0).get(dataList.size()-1).getPoint().x;
if(point.x < linePointList.get(0).get(0).getPoint().x)
point.x = linePointList.get(0).get(0).getPoint().x;
//获取焦点对应的数据的索引
int index = (int) ((point.x - rectChart.left) * dataList.size() / (rectChart.right - rectChart.left));
LogUtil.e(getClass().getSimpleName(), "========焦点索引:"+index+" 数据总数:"+dataList.size()+" 线条数量:"+linePointList.size());
index = Math.max(0, Math.min(index, dataList.size() - 1));
focusData = new FocusData();
focusData.setData(dataList.get(index));
focusData.setPoints(new ArrayList<>());
focusData.getPoints().add(linePointList.get(0).get(index));
focusData.getPoints().add(linePointList.get(1).get(index));
if(null!=onFocusChangeListener)
onFocusChangeListener.onfocus(focusData);
}
invalidate();
}
private OnFocusChangeListener onFocusChangeListener;
public void setOnFocusChangeListener(OnFocusChangeListener onFocusChangeListener) {
this.onFocusChangeListener = onFocusChangeListener;
}
public interface OnFocusChangeListener{
public void onfocus(FocusData focusData);
}
/**绘制图表基本框架*/
@Override
public void drawDefult(Canvas canvas) {
if(null==linePointList || linePointList.size()<=0)
return;
paint.setStyle(Paint.Style.FILL);
paint.setStrokeWidth(lineWidth);
paint.setColor(defColor);
drawGrid(canvas);
drawXLable(canvas);
drawYLable(canvas);
}
/**绘制debug辅助线*/
@Override
public void drawDebug(Canvas canvas) {
super.drawDebug(canvas);
}
/**绘制图表*/
@Override
public void drawChart(Canvas canvas) {
if(null==linePointList || linePointList.size()<=0)
return;
drawDataPath(canvas);
drawFocus(canvas);
}
/**绘制X轴方向辅助网格*/
private void drawGrid(Canvas canvas){
float yMarkSpace = (rectChart.bottom - rectChart.top)/(yLeft.lableNum-1);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(lineWidth);
paint.setColor(defColor);
paintEffect.setStyle(Paint.Style.STROKE);
paintEffect.setStrokeWidth(lineWidth);
paintEffect.setColor(defColor);
// canvas.drawLine(rectChart.left, rectChart.top, rectChart.left, rectChart.bottom, paint);
// canvas.drawLine(rectChart.right, rectChart.top, rectChart.right, rectChart.bottom, paint);
PathEffect effects = new DashPathEffect(new float[]{15,6,15,6},0);
Path path = new Path();
for (int i = 0; i < yLeft.lableNum; i++) {
path.reset();
path.moveTo(rectChart.left, rectChart.bottom-yMarkSpace*i);
path.lineTo(rectChart.right,rectChart.bottom-yMarkSpace*i);
paintEffect.setPathEffect(effects);
canvas.drawPath(path, paintEffect);
}
}
/**绘制X轴刻度*/
private void drawXLable(Canvas canvas){
paintLabel.setTextSize(textSizeX);
paintLabel.setColor(textColorX);
for(DataPoint lable : lableXPointList){
canvas.drawText(lable.getValueX(), lable.getPoint().x, lable.getPoint().y, paintLabel);
}
}
/**绘制Y轴刻度*/
private void drawYLable(Canvas canvas){
float yMarkSpace = (rectChart.bottom - rectChart.top)/(yLeft.lableNum-1);
paintLabel.setTextSize(yLeft.textSize);
paintLabel.setColor(yLeft.textColor);
float lableHeight = FontUtil.getFontHeight(paintLabel);
float lableLead = FontUtil.getFontLeading(paintLabel);
for (int i = 0; i < yLeft.lableNum; i++) {
String text = (int) (yLeft.cal_mark_min + i * yLeft.cal_mark) + "";
canvas.drawText(text,
rectChart.left - yLeft.textSpace - FontUtil.getFontlength(paintLabel, text),
rectChart.bottom - yMarkSpace * i - lableHeight/2 + lableLead, paintLabel);
}
paintLabel.setTextSize(yRight.textSize);
paintLabel.setColor(yRight.textColor);
lableHeight = FontUtil.getFontHeight(paintLabel);
lableLead = FontUtil.getFontLeading(paintLabel);
for (int i = 0; i < yRight.lableNum; i++) {
String text = (int) (yRight.cal_mark_min + i * yRight.cal_mark) + "";
canvas.drawText(text,
rectChart.right + yRight.textSpace,
rectChart.bottom - yMarkSpace * i - lableHeight/2 + lableLead, paintLabel);
}
}
/**绘制曲线*/
private void drawDataPath(Canvas canvas) {
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setStrokeJoin(Paint.Join.ROUND);
paint.setStrokeWidth(lineSize);
for(int j = 0; j < linePointList.size(); j++){
List<DataPoint> lineList = linePointList.get(j);
paint.setColor(lineColor[j]);
//一条一条的绘制
Path path = new Path();
PointF lastPoint = null;
for(int i = 0 ; i<lineList.size(); i++){
DataPoint dataPoint = lineList.get(i);
if(i == 0){
if(animType == AnimType.LEFT_TO_RIGHT){
path.moveTo(rectChart.left+(dataPoint.getPoint().x-rectChart.left)*animPro, dataPoint.getPoint().y);
}else if(animType == AnimType.BOTTOM_TO_TOP){
path.moveTo(dataPoint.getPoint().x, rectChart.bottom-(rectChart.bottom- dataPoint.getPoint().y)* animPro);
}else{
path.moveTo(dataPoint.getPoint().x, dataPoint.getPoint().y);
}
}else{
//quadTo:二阶贝塞尔曲线连接前后两点,这样使得曲线更加平滑
if(animType == AnimType.LEFT_TO_RIGHT){
path.quadTo(rectChart.left+(lastPoint.x-rectChart.left)*animPro, lastPoint.y,
rectChart.left+(dataPoint.getPoint().x-rectChart.left)*animPro , dataPoint.getPoint().y);
}else if(animType == AnimType.BOTTOM_TO_TOP){
path.quadTo(lastPoint.x, rectChart.bottom-(rectChart.bottom-lastPoint.y)* animPro,
dataPoint.getPoint().x, rectChart.bottom-(rectChart.bottom- dataPoint.getPoint().y)* animPro);
}else if(animType == AnimType.SLOW_DRAW){
if(i>lineList.size()*animPro)
break;
path.quadTo(lastPoint.x, lastPoint.y, dataPoint.getPoint().x, dataPoint.getPoint().y);
}else{
path.quadTo(lastPoint.x, lastPoint.y, dataPoint.getPoint().x, dataPoint.getPoint().y);
}
}
lastPoint = dataPoint.getPoint();
}
canvas.drawPath(path, paint);
}
}
/**绘制焦点*/
private void drawFocus(Canvas canvas){
if(!onFocus || null==focusData)
return;
PointF point1 = focusData.getPoints().get(0).getPoint();
PointF point2 = focusData.getPoints().get(1).getPoint();
//绘制竖直虚线
PathEffect effects = new DashPathEffect(new float[]{15,10,15,10},0);
paintEffect.setStyle(Paint.Style.STROKE);
paintEffect.setStrokeWidth(focusLineSize);
paintEffect.setColor(focusLineColor);
paintEffect.setPathEffect(effects);
Path path = new Path();
path.moveTo(point1.x, rectChart.bottom);
path.lineTo(point1.x, rectChart.top);
canvas.drawPath(path , paintEffect);
// canvas.drawLine(focusData.getPoints().get(0).getPoint().x, rectChart.bottom, focusData.getPoints().get(0).getPoint().x, rectChart.top, paint);
//绘制水平线
// canvas.drawLine(rectChart.left, focusData.getPoints().get(0).getPoint().y, rectChart.right, focusData.getPoints().get(0).getPoint().y, paint);
// canvas.drawLine(rectChart.left, focusData.getPoints().get(1).getPoint().y, rectChart.right, focusData.getPoints().get(1).getPoint().y, paint);
//绘制焦点
paint.setAntiAlias(true);
//半透明圆圈
int radius = DensityUtil.dip2px(getContext(), 8);
int strokeWidth = DensityUtil.dip2px(getContext(), 3);
paint.setStrokeWidth(strokeWidth);
paint.setStyle(Paint.Style.STROKE);
paint.setColor(lineColor[0]);
paint.setAlpha(80); //透明度
canvas.drawCircle(point1.x, point1.y, radius,paint);
paint.setColor(lineColor[1]);
paint.setAlpha(80);
canvas.drawCircle(point2.x, point2.y, radius,paint);
//里面
int radius1 = radius-strokeWidth/2 - 4;
paint.setStyle(Paint.Style.FILL);
paint.setColor(lineColor[0]);
paint.setAlpha(255);
canvas.drawCircle(point1.x, point1.y, radius1, paint);
paint.setColor(lineColor[1]);
paint.setAlpha(255);
// canvas.rotate(-90);
// canvas.drawRect(new Rect((int), (int)point2.y - radius1, (int)point2.x+radius1, (int)point2.y+radius1), paint);
radius1 = radius-strokeWidth/2;
path.reset();
path.moveTo(point2.x - radius1, point2.y);
path.lineTo(point2.x, point2.y - radius1);
path.lineTo(point2.x + radius1, point2.y);
path.lineTo(point2.x, point2.y + radius1);
canvas.drawPath(path , paint);
//面板
radius1 = radius*2;
boolean showLeft = point1.x-rectChart.left > (rectChart.right - rectChart.left)/2;
RectF rect = new RectF(
showLeft?point1.x - foucsRectWidth - radius1:point1.x + radius1,
rectChart.top + (rectChart.bottom - rectChart.top)/2 - foucsRectHeight/2,
showLeft? point1.x - radius1 : point1.x + foucsRectWidth + radius1,
rectChart.top + (rectChart.bottom - rectChart.top)/2 + foucsRectHeight/2
);
paint.setColor(lineColor[0]);
strokeWidth = DensityUtil.dip2px(getContext(), 0.8f);
paint.setStrokeWidth(strokeWidth);
paint.setStyle(Paint.Style.STROKE);
path.reset();
path.moveTo(rect.left, rect.top);
path.lineTo(rect.right, rect.top);
path.lineTo(rect.right, rect.bottom);
path.lineTo(rect.left, rect.bottom);
path.close();
canvas.drawPath(path , paint);
paint.setStyle(Paint.Style.FILL);
paint.setColor(Color.WHITE);
paint.setAlpha(210);
canvas.drawRect(rect , paint);
//面板中的文字
paintLabel.setTextSize(textSizeX);
paintLabel.setColor(textColorX);
float lableHeight = FontUtil.getFontHeight(paintLabel);
float lableLead = FontUtil.getFontLeading(paintLabel);
float top = rect.top + foucsRectSpace;
float left = showLeft?rect.left + foucsRectSpace:rect.left + foucsRectSpace;
canvas.drawText(formatDate(focusData.getPoints().get(0).getValueX()),
left, top + lableLead, paintLabel);
top += (lableHeight+foucsRectTextSpace);
paintLabel.setTextSize(yLeft.textSize);
paintLabel.setColor(yLeft.textColor);
lableHeight = FontUtil.getFontHeight(paintLabel);
lableLead = FontUtil.getFontLeading(paintLabel);
canvas.drawText("概念:"+NumberFormatUtil.formattedDecimal(focusData.getPoints().get(0).getValueY()),
left, top + lableLead, paintLabel);
top += (lableHeight+foucsRectTextSpace);
paintLabel.setTextSize(yRight.textSize);
paintLabel.setColor(yRight.textColor);
lableLead = FontUtil.getFontLeading(paintLabel);
canvas.drawText("热度:"+NumberFormatUtil.formattedDecimal(focusData.getPoints().get(1).getValueY()),
left, top + lableLead, paintLabel);
}
private float animPro; //动画计算的占比数量
/**创建动画*/
@Override
protected ValueAnimator initAnim() {
if(dataList.size()>0) {
ValueAnimator anim = ValueAnimator.ofObject(new AngleEvaluator(), 0f, 1f);
anim.setInterpolator(new AccelerateDecelerateInterpolator());
return anim;
}
return null;
}
/**动画值变化之后计算数据*/
@Override
protected void evaluatorData(ValueAnimator animation) {
animPro = (float)animation.getAnimatedValue();
}
/**焦点数据*/
public static class FocusData {
private List<DataPoint> points;
private List<Object> data;
public List<DataPoint> getPoints() {
return points;
}
public void setPoints(List<DataPoint> points) {
this.points = points;
}
public List<Object> getData() {
return data;
}
public void setData(List<Object> data) {
this.data = data;
}
}
/**
* 根据传入的Y轴信息,从数据中获取最小最大值,并获取到Y刻度点值
*/
private void calYLable(YAxisMark axisMark, int index){
float redundance = 1.01f; //y轴最大和最小值冗余
axisMark.cal_mark_max = Float.MIN_VALUE; //Y轴刻度最大值
axisMark.cal_mark_min = Float.MAX_VALUE; //Y轴刻度最小值
//设置数据 概念走势图, 每个元素表示:时间、概念、热度 [20200803,"1582.08","30.00"],
for(List<Object> data : dataList){
try {
axisMark.cal_mark = Float.parseFloat(data.get(index).toString());
if(axisMark.cal_mark>axisMark.cal_mark_max)
axisMark.cal_mark_max = axisMark.cal_mark;
if(axisMark.cal_mark<axisMark.cal_mark_min)
axisMark.cal_mark_min = axisMark.cal_mark;
}catch (Exception e){
}
}
LogUtil.i(TAG, "Y轴真实axisMark.cal_mark_min="+axisMark.cal_mark_min+" axisMark.cal_mark_max="+axisMark.cal_mark_max);
if(axisMark.cal_mark_max>0)
axisMark.cal_mark_max *= redundance;
else
axisMark.cal_mark_max /= redundance;
if(axisMark.cal_mark_min>0)
axisMark.cal_mark_min /= redundance;
else
axisMark.cal_mark_min *= redundance;
if(index == 2)
axisMark.cal_mark_min = 0;
axisMark.cal_mark = (axisMark.cal_mark_max-axisMark.cal_mark_min)/(axisMark.lableNum - 1);
LogUtil.i(TAG, "Y轴axisMark.cal_mark_min="+axisMark.cal_mark_min+" axisMark.cal_mark_max="+axisMark.cal_mark_max+" axisMark.cal_mark="+axisMark.cal_mark);
}
}
@@ -0,0 +1,586 @@
package com.openxu.cview.xmstock20201030;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.PathEffect;
import android.graphics.PointF;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import com.openxu.cview.R;
import com.openxu.cview.xmstock20201030.bean.CalendarData;
import com.openxu.cview.xmstock20201030.bean.CalendarDataStock;
import com.openxu.cview.xmstock20201030.bean.CalendarDetail;
import com.openxu.cview.xmstock20201030.bean.Constacts;
import com.openxu.cview.xmstock20201030.bean.DayFinish;
import com.openxu.utils.DensityUtil;
import com.openxu.utils.FontUtil;
import com.openxu.utils.LogUtil;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* author : xm
* create at : 2020/11/3 9:56
* class name : MyCalendar
* version : 1.0
* class describe:自定义日历控件
*/
public class MyCalendar extends View {
private String TAG = "MyCalendar";
private boolean debug = false;
/** 字体颜色、大小*/
private int textColorMonth = Color.parseColor("#868686");
private int textSizeMonth = DensityUtil.sp2px(getContext(), 10);
private int textColorLable = Color.parseColor("#212025");
private int textSizeLable = DensityUtil.sp2px(getContext(), 14);
private int textColorWeek = Color.parseColor("#afafaf");
private int textSizeWeek = DensityUtil.sp2px(getContext(), 14);
private int textColorDate = Color.parseColor("#3d3d3d");
private int textColorDateNoStock = Color.parseColor("#dcdcdc"); //没有股票的日期字体颜色
private int textSizeDate = DensityUtil.sp2px(getContext(), 14);
private int textColorStock = Color.parseColor("#484848");
private int textSizeStock = DensityUtil.sp2px(getContext(), 10);
/**颜色*/
private int colorDot = Color.parseColor("#5f90c8");
private int colorSelected = Color.parseColor("#d36647");
/**间隙*/
private int lableTextSpace = DensityUtil.dip2px(getContext(), 10); //lable与上方的间距
private int dateLineSpace = DensityUtil.dip2px(getContext(), 20); //日期每行间距
private int stockTextSpace = DensityUtil.dip2px(getContext(), 4); //股票字之间的间距
private int dateTopSpace = DensityUtil.dip2px(getContext(), 5); //日子与月份、星期的间距
private int dotSpace = DensityUtil.dip2px(getContext(), 6); //圆点与字的间距
private float selectRadius = DensityUtil.dip2px(getContext(), 13); //选中日期的圆圈半径
private float dotRadius = DensityUtil.dip2px(getContext(), 3); //小圆点半径
private float bitmapSpace = DensityUtil.dip2px(getContext(), 8);
/**常量*/
private Bitmap bitmapDown, bitmapUp;
private String[] weekStrs = new String[]{"一", "二", "三", "四", "五", "六", "日", };
private Paint paint, paintText;
/**计算*/
private float textMonthHeight, textMonthLead;
private float textLableHeight, textLableLead;
private float textWeekHeight, textWeekLead;
private float textDateHeight, textDateLead;
private float textStockHeight, textStockLead;
private float topHeight; //上方简要日期高度
private float lableHeight; //lable高度
private float weekHeight;
private float dateLineHeight; //每行日期高度,包括下间隙
private RectF rectTop, rectLable, rectWeek, rectDate, rectRow;
private int lineNum; //日期行数
private int lastLineDataNum;
private int startIndex; //第一行开始绘制索引0-6
private boolean showCalendar = false;
public MyCalendar(Context context) {
this(context, null);
}
public MyCalendar(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyCalendar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// setBackgroundColor(Color.parseColor("#aa000000"));
bitmapDown = BitmapFactory.decodeResource(getResources(), R.mipmap.calendar_down);
bitmapUp = BitmapFactory.decodeResource(getResources(), R.mipmap.calendar_up);
paint = new Paint();
paintText = new Paint();
paint.setAntiAlias(true);
paintText.setAntiAlias(true);
paintText.setTextSize(textSizeMonth);
textMonthHeight = FontUtil.getFontHeight(paintText);
textMonthLead = FontUtil.getFontLeading(paintText);
paintText.setTextSize(textSizeLable);
textLableHeight = FontUtil.getFontHeight(paintText);
textLableLead = FontUtil.getFontLeading(paintText);
paintText.setTextSize(textSizeWeek);
textWeekHeight = FontUtil.getFontHeight(paintText);
textWeekLead = FontUtil.getFontLeading(paintText);
paintText.setTextSize(textSizeDate);
textDateHeight = FontUtil.getFontHeight(paintText);
textDateLead = FontUtil.getFontLeading(paintText);
paintText.setTextSize(textSizeStock);
textStockHeight = FontUtil.getFontHeight(paintText);
textStockLead = FontUtil.getFontLeading(paintText);
//上方简要日期高度
topHeight = textMonthHeight + dateTopSpace + textDateHeight + dotSpace + dotRadius*2;
//lable高度
lableHeight = lableTextSpace + textLableHeight;
weekHeight = dateLineSpace + textWeekHeight + dateTopSpace;
//每行日期高度
dateLineHeight = textDateHeight + dotSpace+dotRadius*2+ (stockTextSpace+textStockHeight)*2+dateLineSpace;
}
/***********************接口API↓↓↓↓↓↓↓**************************/
List<CalendarData> calendar_data = new ArrayList<>();
List<CalendarDetail> calendar_detail = new ArrayList<>();
//上方显示的7条数据
List<CalendarData> calendar_data_top = new ArrayList<>();
//焦点数据
CalendarData focusData;
public void setData(List<CalendarData> calendar, List<CalendarDetail> detail) {
Log.w(TAG, "设置数据:"+calendar.size());
calendar_data.clear();
calendar_data.addAll(calendar);
calendar_detail.clear();
calendar_detail.addAll(detail);
calendar_data_top.clear();
for(int i = 0; i<7; i++){
if(i<calendar_data.size())
calendar_data_top.add(calendar_data.get(i));
}
if(calendar_data.size()>0)
focusData = calendar_data.get(0);
requestLayout();
invalidate();
}
private ItemClickListener listener;
public void setItemClickListener(ItemClickListener listener){
this.listener = listener;
}
public interface ItemClickListener{
void onDayClick(CalendarData calendar, List<CalendarDetail> detail);
}
/***********************接口API↑↑↑↑↑↑↑**************************/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//宽度 = 填充父窗体
int widthSize = MeasureSpec.getSize(widthMeasureSpec); //获取宽的尺寸
float rowHeight = bitmapDown.getHeight() + bitmapSpace * 2;
float weekHeight1 = 0;
float dateHeight1 = 0;
if(showCalendar && calendar_data!=null && calendar_data.size()>0){
weekHeight1 = weekHeight;
//"date":"2020-10-20","week":"2","stock_list"
startIndex = Integer.parseInt(calendar_data.get(0).getWeek())-1;
lineNum = 1 + ((calendar_data.size()-(7-startIndex))/7) +
((calendar_data.size()-(7-startIndex))%7>0?1:0);
//最后一行不足7个
if((calendar_data.size()-(7-startIndex))%7>0){
lineNum = 1 + ((calendar_data.size()-(7-startIndex))/7) + 1;
lastLineDataNum = calendar_data.size() - (7-startIndex) - ((lineNum-2)*7);
}else{
lineNum = 1 + ((calendar_data.size()-(7-startIndex))/7);
lastLineDataNum = 7;
}
Log.w(TAG, "行数:"+(calendar_data.size()-(7-startIndex))/7);
Log.w(TAG, "行数:"+((calendar_data.size()-(7-startIndex))%7>0?1:0));
//计算日历高度
dateHeight1 = dateLineHeight * lineNum;
// Log.w(TAG, "总共"+calendar_data.size()+"个数据,第一个数据显示索引"+startIndex+" 总行数:"+lineNum+" 最后一行有"+lastLineDataNum+"个数据 日历数据部分高度:"+dateHeight1);
}
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
(int)(getPaddingTop() + getPaddingBottom() +
topHeight +
lableHeight +
weekHeight1 +
dateHeight1 +
rowHeight));
Log.w(TAG, "测量:"+getMeasuredWidth()+"*"+getMeasuredHeight());
rectTop = new RectF(getPaddingLeft(), getPaddingTop(), getMeasuredWidth()-getPaddingRight(),getPaddingTop()+topHeight);
rectLable = new RectF(rectTop.left, rectTop.bottom, rectTop.right, rectTop.bottom+lableHeight);
rectWeek = new RectF(rectTop.left, rectLable.bottom, rectTop.right, rectLable.bottom+weekHeight1);
rectDate = new RectF(rectTop.left, rectWeek.bottom, rectTop.right, rectWeek.bottom+dateHeight1);
rectRow = new RectF(rectTop.left, rectDate.bottom, rectTop.right, rectDate.bottom+rowHeight);
}
@Override
protected void onDraw(Canvas canvas) {
Log.w(TAG, "绘制"+calendar_data);
if(calendar_data==null || calendar_data.size()<=0)
return;
if(debug)
drawRect(canvas);
drawTop(canvas);
drawLable(canvas);
if(showCalendar){
drawWeek(canvas);
drawDayAndPre(canvas);
}
drawRow(canvas);
}
private void drawRect(Canvas canvas){
paint.setStyle(Paint.Style.FILL);
// paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(3);
paint.setColor(Color.YELLOW);
canvas.drawRect(rectTop, paint);
paint.setColor(Color.BLUE);
canvas.drawRect(rectLable, paint);
paint.setColor(Color.GREEN);
canvas.drawRect(rectWeek, paint);
paint.setColor(Color.WHITE);
canvas.drawRect(rectDate, paint);
paint.setColor(Color.BLACK);
canvas.drawRect(rectRow, paint);
}
private void drawRow(Canvas canvas) {
canvas.drawBitmap(showCalendar?bitmapUp:bitmapDown,
rectRow.left+(rectRow.right-rectRow.left)/2-bitmapUp.getWidth()/2,
rectRow.top + bitmapSpace ,paint);
}
private void drawTop(Canvas canvas) {
float top;
float left = rectTop.left;
float oneWidth = (rectTop.right-rectTop.left)/7;
float textWidth;
String text;
paint.setStyle(Paint.Style.FILL);
for(int i = 0; i<calendar_data_top.size(); i++){
top = rectTop.top;
CalendarData data = calendar_data_top.get(i);
//月
paintText.setTextSize(textSizeMonth);
paintText.setColor(textColorMonth);
text = getMonthStr(data.getDate());
textWidth = FontUtil.getFontlength(paintText, text);
canvas.drawText(text, left + oneWidth/2 - textWidth/2, top + textMonthLead , paintText);
//日
top += textMonthHeight+dateTopSpace;
paintText.setTextSize(textSizeDate);
paintText.setColor(textColorDate);
text = getDateStr(data.getDate());
textWidth = FontUtil.getFontlength(paintText, text);
if(focusData!=null && focusData.getDate().equals(data.getDate())){
//被选中的日子需要标红圈
paint.setColor(colorSelected);
canvas.drawCircle(left + oneWidth/2, top+textDateHeight/2, selectRadius, paint);
paintText.setColor(Color.WHITE);
}
canvas.drawText(text, left + oneWidth/2 - textWidth/2, top + textDateLead , paintText);
//点
top += textDateHeight+dotSpace;
paint.setColor(colorDot);
canvas.drawCircle(left + oneWidth/2, top+dotRadius, dotRadius, paint);
left+=oneWidth;
//分割线
if(debug)
canvas.drawLine(left, rectTop.top, left, rectTop.bottom,paint);
}
}
private void drawLable(Canvas canvas) {
int dotLableSpace = DensityUtil.dip2px(getContext(), 4);
paintText.setTextSize(textSizeLable);
paintText.setColor(textColorLable);
String str = "日历效应";
float textWidth = FontUtil.getFontlength(paintText, str);
float left = rectLable.right - lableTextSpace - textWidth;
canvas.drawText(str, left, rectLable.bottom - textLableHeight + textLableLead ,paintText);
if(debug)
canvas.drawLine(left, rectLable.top, left, rectLable.bottom,paint);
//点
left -= (dotLableSpace+dotRadius);
if(debug)
canvas.drawLine(left, rectLable.top, left, rectLable.bottom,paint);
paint.setStyle(Paint.Style.FILL);
paint.setColor(colorSelected);
canvas.drawCircle(left, rectLable.bottom - textLableHeight/2, dotRadius, paint);
str = "重大事件";
textWidth = FontUtil.getFontlength(paintText, str);
left -= (dotRadius+lableTextSpace+textWidth);
canvas.drawText(str, left, rectLable.bottom - textLableHeight + textLableLead ,paintText);
if(debug)
canvas.drawLine(left, rectLable.top, left, rectLable.bottom,paint);
left -= (dotLableSpace+dotRadius);
paint.setStyle(Paint.Style.FILL);
paint.setColor(colorDot);
canvas.drawCircle(left, rectLable.bottom - textLableHeight/2, dotRadius, paint);
}
private void drawWeek(Canvas canvas){
float left = rectWeek.left;
float oneWidth = (rectWeek.right-rectWeek.left)/7;
float textWidth;
for(int i = 0 ;i<weekStrs.length; i++){
paintText.setTextSize(textSizeWeek);
paintText.setColor(textColorWeek);
textWidth = FontUtil.getFontlength(paintText, weekStrs[i]);
canvas.drawText(weekStrs[i], left + oneWidth/2 - textWidth/2,
rectWeek.top + dateLineSpace + textWeekLead ,paintText);
left += oneWidth;
}
}
/**绘制日历主体*/
private void drawDayAndPre(Canvas canvas){
if(!showCalendar || calendar_data==null || calendar_data.size()<=0)
return;
for(int i = 0 ; i < lineNum; i++){
//一行行绘制
drawDayAndPre(canvas, i, i==0?startIndex+1:1);
}
}
/**
* 绘制某一行的日期
* @param canvas
* @param startWeek 此行第一个日期的星期值
*/
private void drawDayAndPre(Canvas canvas, int line, int startWeek){
//该行绘制的第一个数据索引
int firstDataIndex = line == 0 ? 0 : (7-startIndex) + (line-1)*7;
float top;
float oneWidth = (rectDate.right-rectDate.left)/7;
float left = rectDate.left + (startWeek-1)*oneWidth;
float textWidth;
// Log.w(TAG, "绘制日期,当前第"+line+"行,第一条数据为星期"+startWeek+", 数据开始索引:"+firstDataIndex+", 当前行绘制数量:"+(7 - startWeek+1));
for(int i = 0; i<=7 - startWeek; i++){
if(firstDataIndex+i>=calendar_data.size())
return;
paint.setStyle(Paint.Style.FILL);
top = rectDate.top + line * dateLineHeight;
CalendarData data = calendar_data.get(firstDataIndex+i);
paintText.setTextSize(textSizeDate);
paintText.setColor((data.getStock_list()==null || data.getStock_list().size()<=0)?textColorDateNoStock:textColorDate);
textWidth = FontUtil.getFontlength(paintText, getDateStr(data.getDate()));
//日期
if(focusData!=null && focusData.getDate().equals(data.getDate())){
//被选中的日子需要标红圈
paint.setColor(colorSelected);
canvas.drawCircle(left + oneWidth/2, top+textDateHeight/2, selectRadius, paint);
paintText.setColor(Color.WHITE);
}
canvas.drawText(getDateStr(data.getDate()), left + oneWidth/2 - textWidth/2, top +textDateLead ,paintText);
if(data.getStock_list()!=null && data.getStock_list().size()>0){
top += textDateHeight+dotSpace+dotRadius;
//点
paint.setColor(colorDot);
canvas.drawCircle(left + oneWidth/2, top, dotRadius, paint);
top += dotRadius+stockTextSpace;
//股票
paintText.setTextSize(textSizeStock);
paintText.setColor(textColorStock);
for(CalendarDataStock stock : data.getStock_list()){
textWidth = FontUtil.getFontlength(paintText, stock.getName());
canvas.drawText(stock.getName(), left + oneWidth/2 - textWidth/2, top+textStockLead,paintText);
top += textStockHeight+stockTextSpace;
}
}
left += oneWidth;
if(debug)
canvas.drawLine(left, rectDate.top + line * dateLineHeight, left, top,paint);
}
}
/****************************事件处理↓↓↓↓↓↓↓****************************/
/* @Override
public boolean dispatchTouchEvent(MotionEvent event) {
if(showCalendar){
getParent().requestDisallowInterceptTouchEvent(true);
}
return super.dispatchTouchEvent(event);
}*/
//焦点坐标
private PointF downPoint = new PointF();
private PointF upPoint = new PointF();
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction() & MotionEvent.ACTION_MASK;
switch (action) {
case MotionEvent.ACTION_DOWN:
Log.i(TAG, "========onTouchEvent按下");
downPoint.set(event.getX(), event.getY());
return true;
case MotionEvent.ACTION_MOVE:
Log.i(TAG, "========onTouchEvent滑动");
break;
case MotionEvent.ACTION_OUTSIDE:
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
Log.i(TAG, "========onTouchEvent抬起");
upPoint.set(event.getX(), event.getY());
if(Math.abs(upPoint.x - downPoint.x)<20 && Math.abs(upPoint.y - downPoint.y)<20 )
touchFocus(upPoint);
break;
}
return false;
}
public void touchFocus(final PointF point) {
Log.e(TAG, "点击坐标:("+point.x+" ,"+point.y);
/**标题和星期只有在事件结束后才响应*/
if(point.y<=rectTop.bottom){
// Log.i(TAG, "事件在标题上,处理");
//计算点落在哪个日期上
float oneWidth = (rectTop.right-rectTop.left)/7;
int xIndex = (int)((point.x - rectTop.left) / oneWidth);
if(((point.x - rectTop.left) % oneWidth)>0)
xIndex += 1;
if(xIndex<=0 || xIndex>calendar_data_top.size())
return;
Log.e(TAG, "第"+xIndex+"列, 列宽:"+oneWidth+" x坐标余数:"+((point.x - rectTop.left) % oneWidth));
focusData = calendar_data_top.get(xIndex-1);
showCalendar = false;
requestLayout();
if(listener!=null) {
List<CalendarDetail> details = getDetail(focusData);
if(details!=null)
listener.onDayClick(focusData, details);
}
invalidate();
}else if(point.y<=rectLable.bottom){
// Log.i(TAG, "事件在lable栏,不处理");
}else if(point.y<=rectWeek.bottom){
// Log.i(TAG, "事件在星期栏,不处理");
}else if(point.y<=rectDate.bottom){
// Log.w(TAG, "=============事件在日期上,处理");
//计算点落在哪个日期上
float oneWidth = (rectDate.right-rectDate.left)/7;
int xIndex = (int)((point.x - rectTop.left) / oneWidth);
int foucsLine = (int)((point.y - rectDate.top)/dateLineHeight);
if(((point.y - rectDate.top) % dateLineHeight)>0)
foucsLine += 1;
Log.e(TAG, "点击到第"+foucsLine+"行 "+xIndex+" 列");
int dataIndex = 0;
if(foucsLine == 1){ //第一行
if(xIndex<startIndex){
Log.e(TAG, "点到开始空位了");
return;
}else{
dataIndex = xIndex-startIndex;
}
}else if(foucsLine == lineNum){
//最后一行
if(xIndex>=lastLineDataNum){
Log.e(TAG, "点到结尾空位了");
return;
}else{
dataIndex = calendar_data.size() - (lastLineDataNum - xIndex);
}
}else{
dataIndex = (7-startIndex) + (foucsLine -1)*7 - (7-xIndex);
}
CalendarData data = calendar_data.get(dataIndex);
if(data.getStock_list()==null||data.getStock_list().size()<=0){
return;
}
focusData = data;
// Log.e(TAG, "点击的数据索引为"+dataIndex+" 数据为:"+focusData);
selectCanendar(dataIndex);
showCalendar = false;
requestLayout();
if(listener!=null) {
List<CalendarDetail> details = getDetail(focusData);
if(details!=null)
listener.onDayClick(focusData, details);
}
}else if(point.y<=rectRow.bottom){
Log.i(TAG, "事件在箭头栏,处理");
float center = rectRow.left + (rectRow.right-rectRow.left)/2;
if(point.x < center - bitmapDown.getWidth()||
point.x>center + bitmapDown.getWidth())
return;
showCalendar = !showCalendar;
requestLayout();
}
}
//根据选中的日期,选出7个数据展示在顶部
private void selectCanendar(int selectIndex){
calendar_data_top.clear();
int count = 0;
int preCount = 0;
int lastPreIndex = selectIndex;
int houCount = 0;
//从开始位置从前取4个
for(int i = selectIndex; i >=0 ; i--){
CalendarData data = calendar_data.get(i);
if(data.getStock_list()!=null && data.getStock_list().size()>0){
lastPreIndex = i;
calendar_data_top.add(0, data);
count ++;
preCount ++;
if(preCount >= 4)
break;
}
}
//从开始位置向后取3个
for(int i = selectIndex+1; i < calendar_data.size() ; i++){
CalendarData data = calendar_data.get(i);
if(data.getStock_list()!=null && data.getStock_list().size()>0){
calendar_data_top.add(data);
count ++;
houCount ++;
if(count>=7)
break;
if(preCount == 4){
if(houCount >= 3) //前面取4个后面3个
break;
}else{ //前面没取到4个,继续从后面取,直到取7个,或者遍历到最后
continue;
}
}
}
if(houCount<3){ //后面没取到3个,从前面取
//从开始位置从前取4个
for(int i = lastPreIndex-1; i >=0 ; i--){
CalendarData data = calendar_data.get(i);
if(data.getStock_list()!=null && data.getStock_list().size()>0){
calendar_data_top.add(0, data);
count ++;
preCount ++;
if(count>=7)
break;
}
}
}
Log.w(TAG, "取7个数据:======="+selectIndex+"前面取"+preCount+"个,后面取"+houCount+"个, 总共"+calendar_data_top.size());
}
private List<CalendarDetail> getDetail(CalendarData focusData) {
List<CalendarDetail> details = null;
for(CalendarDetail detail : calendar_detail){
if(focusData.getDate().equals(detail.getDate())){
if(details==null)
details = new ArrayList<>();
details.add(detail);
return details;
}
}
return details;
}
/****************************事件处理↑↑↑↑↑↑↑****************************/
@Override
public void invalidate() {
requestLayout();
super.invalidate();
}
//2020-10-20
private String getMonthStr(String date){
return date.substring(5, 7)+"月";
}
private String getDateStr(String date){
return date.substring(8, date.length());
}
}
@@ -0,0 +1,174 @@
package com.openxu.cview.xmstock20201030;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathEffect;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.util.Log;
import android.view.animation.AccelerateDecelerateInterpolator;
import com.openxu.cview.R;
import com.openxu.cview.chart.anim.AngleEvaluator;
import com.openxu.cview.xmstock.BaseChart;
import com.openxu.cview.xmstock.bean.DataPoint;
import com.openxu.utils.DensityUtil;
import com.openxu.utils.FontUtil;
import com.openxu.utils.LogUtil;
import com.openxu.utils.NumberFormatUtil;
import java.util.ArrayList;
import java.util.List;
/**
* autour : xiami
* date : 2020/11/13 14:26
* className : ProgressBar
* version : 1.0
* description : 不规则进度条
*/
public class ProgressBar extends BaseChart {
/**可以设置的属性*/
private int[] barColor = new int[]{Color.parseColor("#e9403b"),
Color.parseColor("#f98683")};
private float total = 100;
private float progress = 0;
//左边图
private int bitmapID = R.mipmap.ic_concept_hot_more_fire;
//进度条高度
private int barHeight = DensityUtil.dip2px(getContext(), 8f);
private int chonghe = DensityUtil.dip2px(getContext(), 3f);//重合度
private float up = 0.2f; //预留图片下方白色区域
private Bitmap bitmap;
public ProgressBar(Context context) {
this(context, null);
}
public ProgressBar(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ProgressBar(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public void init(Context context, AttributeSet attrs, int defStyleAttr) {
touchEnable = false;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if(bitmap==null)
bitmap = BitmapFactory.decodeResource(getResources(), bitmapID);
float uph = up*bitmap.getHeight();
float bottom = bitmap.getHeight() - uph - barHeight;
heightSize = bitmap.getHeight() + (int)bottom; //让进度条居中
setMeasuredDimension(widthSize, heightSize);
rectChart = new RectF(getPaddingLeft(),getPaddingTop(),getMeasuredWidth()-getPaddingRight(),
getMeasuredHeight()-getPaddingBottom());
centerPoint = new PointF(getMeasuredWidth()/2, getMeasuredHeight()/2);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
evaluatorByData();
startDraw = false;
isLoading = false;
invalidate();
}
public void setData(float total, float progress){
this.total = total;
this.progress = progress;
if(getMeasuredWidth()>0) {
evaluatorByData();
startDraw = false;
isLoading = false;
invalidate();
}
}
private void evaluatorByData(){
}
@Override
public void drawDefult(Canvas canvas) {
}
@Override
public void drawChart(Canvas canvas) {
float left = bitmap.getWidth()-chonghe;
float proSize = (rectChart.right-left)/total*progress * animPro;
//添加路径信息
Path pathDef = new Path();
Path pathPro = new Path();
pathDef.moveTo(left , centerPoint.y-barHeight/2); //左上
pathPro.moveTo(left , centerPoint.y-barHeight/2);
pathDef.lineTo(rectChart.right-barHeight/2, centerPoint.y-barHeight/2); //右上
pathPro.lineTo(left + proSize, centerPoint.y-barHeight/2);
RectF rectFDef = new RectF(rectChart.right-barHeight, centerPoint.y-barHeight/2, rectChart.right, centerPoint.y+barHeight/2);
RectF rectFPro = new RectF(left + proSize -barHeight, centerPoint.y-barHeight/2, left + proSize , centerPoint.y+barHeight/2);
pathDef.arcTo(rectFDef, -90, 180);
pathPro.arcTo(rectFPro, -90, 180);
// canvas.drawArc(rectFDef, -90, 180, false ,paint);
pathDef.lineTo(rectChart.right - barHeight/2, centerPoint.y+barHeight/2); //右下
pathPro.lineTo(rectChart.right - proSize - barHeight/2, centerPoint.y+barHeight/2);
pathDef.lineTo(left, centerPoint.y+barHeight/2); //左下
pathPro.lineTo(left, centerPoint.y+barHeight/2);
//1.绘制默认灰色
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.FILL);
paint.setColor(defColor);
canvas.drawPath(pathDef, paint);
//2.绘制进度
//线性渐变:前两个参数是渐变开始的点坐标,第三四个参数是渐变结束的点的坐标;渐变的颜色,渐变颜色的分布,模式
Shader mShader = new LinearGradient(left, centerPoint.y ,rectChart.right, centerPoint.y,
new int[] {barColor[0],barColor[1]},null,Shader.TileMode.CLAMP);
// Log.w(TAG, "从"+left+"*"+centerPoint.y+"->"+rectChart.right+"*"+centerPoint.y+"渐变");
paintEffect.setStyle(Paint.Style.FILL);
paintEffect.setColor(defColor);
paintEffect.setShader(mShader);
canvas.drawPath(pathPro, paintEffect);
paintEffect.setShader(null);
//圆角矩形
// canvas.drawRoundRect(new RectF(bitmap.getWidth()-chonghe, centerPoint.y+barHeight/2,
// rectChart.right, centerPoint.y-barHeight/2),
// barHeight/2, barHeight/2, paint);
canvas.drawBitmap(bitmap, rectChart.left, rectChart.top, paint);
}
private float animPro; //动画计算的占比数量
/**创建动画*/
@Override
protected ValueAnimator initAnim() {
if(progress>0) {
ValueAnimator anim = ValueAnimator.ofObject(new AngleEvaluator(), 0f, 1f);
anim.setInterpolator(new AccelerateDecelerateInterpolator());
return anim;
}
return null;
}
/**动画值变化之后计算数据*/
@Override
protected void evaluatorData(ValueAnimator animation) {
animPro = (float)animation.getAnimatedValue();
}
}
@@ -0,0 +1,447 @@
package com.openxu.cview.xmstock20201030;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathEffect;
import android.graphics.PointF;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.animation.AccelerateDecelerateInterpolator;
import com.openxu.cview.R;
import com.openxu.cview.chart.anim.AngleEvaluator;
import com.openxu.cview.xmstock.BaseChart;
import com.openxu.cview.xmstock.bean.DataPoint;
import com.openxu.cview.xmstock20201030.bean.TopDetailNew;
import com.openxu.utils.DensityUtil;
import com.openxu.utils.FontUtil;
import com.openxu.utils.LogUtil;
import java.util.ArrayList;
import java.util.List;
/**
* autour : xiami
* date : 2020/11/13 14:26
* className : QsrdLinesChart
* version : 1.0
* description : 1. 券商热点走势对比图
*/
public class QsrdLinesChart extends BaseChart {
/**可以设置的属性*/
//设置Y轴刻度数量
private int YMARK_NUM = 3;
//设置线条颜色 粗细
private int lineColor = Color.parseColor("#358ce5"); //蓝色
private int lineSize = DensityUtil.dip2px(getContext(), 1.5f);
//新闻圆点颜色
private int newCircleColor = Color.parseColor("#e7413d");
//设置坐标文字大小
private int textSize = (int)getResources().getDimension(R.dimen.ts_chart_xy);
//设置坐标文字颜色
private int textColor = Color.parseColor("#5E5E5E");
//设置X坐标字体与横轴的距离
private int textSpaceX = DensityUtil.dip2px(getContext(), 5);
//设置Y坐标字体与横轴的距离
private int textSpaceY = DensityUtil.dip2px(getContext(), 3);
//设置动画类型
private AnimType animType = AnimType.SLOW_DRAW;
//设置焦点线颜色 及 粗细
private int focusLineColor = Color.parseColor("#5E5E5E");
private int focusLineSize = DensityUtil.dip2px(getContext(), 1f);
/**设置数据*/
private List<List<Object>> dataList = new ArrayList<>();
//需要标点的新闻日期
private List<TopDetailNew> newsList = new ArrayList<>();
private List<String> newsDate = new ArrayList<>();
//计算后的数据
private String[] lableXList;
private List<DataPoint> lableXPointList;
private List<DataPoint> linePointList;
public enum AnimType{
LEFT_TO_RIGHT, //动画从左往右
BOTTOM_TO_TOP, //动画从下网上上升
SLOW_DRAW //动画缓慢绘制
}
/**需要计算相关值*/
private float lableLead, lableHeight;
/*字体绘制相关*/
private float YMARK = 1; //Y轴刻度间隔
private float YMARK_MAX = Float.MIN_VALUE; //Y轴刻度最大值
private float YMARK_MIN = Float.MAX_VALUE; //Y轴刻度最小值
public QsrdLinesChart(Context context) {
this(context, null);
}
public QsrdLinesChart(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public QsrdLinesChart(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public void init(Context context, AttributeSet attrs, int defStyleAttr) {
dataList = new ArrayList<>();
touchEnable = true;
}
public void setYMARK_NUM(int YMARK_NUM) {
this.YMARK_NUM = YMARK_NUM;
}
/***********************************设置属性set方法**********************************/
public void setAnimType(AnimType animType) {
this.animType = animType;
}
/**
* 设置数据
*/
public void setData(List<List<Object>> dataList, List<TopDetailNew> newsList){
if(null==dataList)
return;
this.dataList.clear();
this.dataList.addAll(dataList);
this.newsList.clear();
this.newsList.addAll(newsList);
this.newsDate.clear();
for(TopDetailNew news : newsList){
//新闻日期:[2020-11-03 08:00:00, 2020-10-21 08:00:00, 2020-08-03 07:54:00, 2020-10-14 08:00:00]
newsDate.add(news.getPublishdate().contains(" ")?
news.getPublishdate().substring(0, news.getPublishdate().indexOf(" "))
: news.getPublishdate());
}
LogUtil.v(TAG, "新闻日期:"+newsDate);
//获取X轴刻度
lableXList = new String[]{dataList.get(0).get(0).toString(), dataList.get(dataList.size()-1).get(0).toString()};
if(getMeasuredWidth()>0) {
evaluatorByData();
startDraw = false;
isLoading = false;
invalidate();
}
}
/***********************************设置属性set方法over**********************************/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(widthSize, heightSize);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
evaluatorByData();
startDraw = false;
invalidate();
}
/**设置数据后,计算相关值*/
private void evaluatorByData(){
if(dataList.size()<=0)
return;
/**③、计算Y刻度最大值和最小值以及幅度*/
YMARK_MAX = Float.MIN_VALUE; //Y轴刻度最大值
YMARK_MIN = Float.MAX_VALUE; //Y轴刻度最小值
float redundance = 1.01f; //y轴最大和最小值冗余
for(List<Object> list : dataList){
//["2020-07-10", 2491.7 ]
try {
YMARK = Float.parseFloat(list.get(1).toString());
// LogUtil.d(TAG, list.get(1)+" Y轴 被识别为Float = "+YMARK);
if(YMARK>YMARK_MAX)
YMARK_MAX = YMARK;
if(YMARK<YMARK_MIN)
YMARK_MIN = YMARK;
}catch (Exception e){
}
}
LogUtil.w(TAG, "Y轴真实YMARK_MIN="+YMARK_MIN+" YMARK_MAX="+YMARK_MAX);
if(YMARK_MAX>0)
YMARK_MAX *= redundance;
else
YMARK_MAX /= redundance;
if(YMARK_MIN>0)
YMARK_MIN /= redundance;
else
YMARK_MIN *= redundance;
YMARK = (YMARK_MAX-YMARK_MIN)/(YMARK_NUM - 1);
YMARK = (int)YMARK + 1;
YMARK_MIN = (int)YMARK_MIN;
YMARK_MAX = (int)(YMARK_MIN+YMARK*(YMARK_NUM-1));
LogUtil.i(TAG, "Y轴YMARK_MIN="+YMARK_MIN+" YMARK_MAX="+YMARK_MAX+" YMARK="+YMARK);
/**①、计算字体相关以及图表原点坐标*/
paintLabel.setTextSize(textSize);
lableHeight = FontUtil.getFontHeight(paintLabel);
lableLead = FontUtil.getFontLeading(paintLabel);
//图表主体矩形
rectChart = new RectF(getPaddingLeft() + textSpaceY + FontUtil.getFontlength(paintLabel, (int)YMARK_MAX+""),
getPaddingTop(),
getMeasuredWidth()-getPaddingRight(),
getMeasuredHeight()-getPaddingBottom() - lableHeight - textSpaceX);
/**②、计算X标签绘制坐标*/
float lableXSpace = 0;
for(String lableX : lableXList){
lableXSpace += FontUtil.getFontlength(paintLabel, lableX);
}
lableXSpace = (rectChart.right - rectChart.left - lableXSpace)/(lableXList.length -1);
lableXPointList = new ArrayList<>();
float left = rectChart.left;
for(int i = 0; i<lableXList.length; i++){
String lableX = lableXList[i];
lableXPointList.add(new DataPoint(lableX, 0, new PointF(
left,rectChart.bottom + textSpaceX + lableLead)));
left += (FontUtil.getFontlength(paintLabel, lableXList[i])+lableXSpace);
}
/**④、计算点的坐标,如果有动画的情况下,边绘制边计算会耗费性能,所以先计算*/
linePointList = new ArrayList<>();
float oneSpace = (rectChart.right - rectChart.left) / (dataList.size()-1);
List<DataPoint> lastDataGroup = null; //最后一组数据
for(int i = 0; i < dataList.size(); i++){
// List<String> oneData = dataList.get(0);
try {
////["2020-07-10", 2491.7 ]
float valueY = Float.parseFloat(dataList.get(i).get(1).toString());
PointF point = new PointF();
point.x = rectChart.left + i * oneSpace;
point.y = rectChart.bottom -
(rectChart.bottom-rectChart.top)/(YMARK_MAX - YMARK_MIN) * (valueY-YMARK_MIN);
linePointList.add(new DataPoint(dataList.get(i).get(0).toString(), valueY, point));
}catch (Exception e){
e.printStackTrace();
}
}
}
@Override
protected void onTouchMoved(PointF point) {
if(null==dataList)
return;
onFocus = (null != point);
if(null != point && null!=dataList && dataList.size()>0) {
//避免滑出
if(point.x > linePointList.get(linePointList.size()-1).getPoint().x)
point.x = linePointList.get(linePointList.size()-1).getPoint().x;
if(point.x < linePointList.get(0).getPoint().x)
point.x = linePointList.get(0).getPoint().x;
//获取焦点对应的数据的索引
int index = (int) ((point.x - rectChart.left) * dataList.size() / (rectChart.right - rectChart.left));
// LogUtil.e(getClass().getSimpleName(), "========焦点索引:"+index+" 数据总数:"+dataList.size()+" 绘制点总数:"+linePointList.size());
index = Math.max(0, Math.min(index, dataList.size() - 1));
focusData = new FocusData();
focusData.setPoint(linePointList.get(index));
focusData.setData(dataList.get(index));
//获取该日期对应的新闻数据
if(newsDate.contains(dataList.get(index).get(0))) {
for(TopDetailNew news : newsList)
if(news.getPublishdate().contains(dataList.get(index).get(0).toString()))
focusData.setNews(news);
}
if(null!=onFocusChangeListener)
onFocusChangeListener.onfocus(focusData);
}
invalidate();
}
private FocusData focusData;
private OnFocusChangeListener onFocusChangeListener;
public void setOnFocusChangeListener(OnFocusChangeListener onFocusChangeListener) {
this.onFocusChangeListener = onFocusChangeListener;
}
public interface OnFocusChangeListener{
void onfocus(FocusData focusData);
}
/**绘制图表基本框架*/
@Override
public void drawDefult(Canvas canvas) {
if(null==linePointList || linePointList.size()<=0)
return;
paint.setStyle(Paint.Style.FILL);
paint.setStrokeWidth(lineWidth);
paint.setColor(defColor);
drawGrid(canvas);
drawXLable(canvas);
drawYLable(canvas);
}
/**绘制debug辅助线*/
@Override
public void drawDebug(Canvas canvas) {
super.drawDebug(canvas);
}
/**绘制图表*/
@Override
public void drawChart(Canvas canvas) {
if(null==linePointList || linePointList.size()<=0)
return;
drawDataPath(canvas);
drawFocus(canvas);
}
/**绘制X轴方向辅助网格*/
private void drawGrid(Canvas canvas){
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(lineWidth);
paint.setColor(defColor);
canvas.drawLine(rectChart.left, rectChart.bottom, rectChart.right, rectChart.bottom, paint);
}
/**绘制X轴刻度*/
private void drawXLable(Canvas canvas){
paintLabel.setTextSize(textSize);
paintLabel.setColor(textColor);
for(DataPoint lable : lableXPointList){
canvas.drawText(lable.getValueX(), lable.getPoint().x, lable.getPoint().y, paintLabel);
}
}
/**绘制Y轴刻度*/
private void drawYLable(Canvas canvas){
float yMarkSpace = (rectChart.bottom - rectChart.top)/(YMARK_NUM-1);
paintLabel.setTextSize(textSize);
paintLabel.setColor(textColor);
for (int i = 0; i < YMARK_NUM; i++) {
canvas.drawText((int) (YMARK_MIN + i * YMARK) + "",
rectChart.left - textSpaceY - FontUtil.getFontlength(paintLabel, (int) (YMARK_MIN + i * YMARK) + ""),
rectChart.bottom - yMarkSpace * i - lableHeight/2 + lableLead, paintLabel);
}
}
/**绘制曲线*/
private void drawDataPath(Canvas canvas) {
Path path = new Path();
PointF lastPoint = null;
List<DataPoint> newDatePoint = new ArrayList<>();
for(int i = 0 ; i<linePointList.size(); i++){
DataPoint dataPoint = linePointList.get(i);
if(i == 0){
if(animType == AnimType.LEFT_TO_RIGHT){
path.moveTo(rectChart.left+(dataPoint.getPoint().x-rectChart.left)*animPro, dataPoint.getPoint().y);
}else if(animType == AnimType.BOTTOM_TO_TOP){
path.moveTo(dataPoint.getPoint().x, rectChart.bottom-(rectChart.bottom- dataPoint.getPoint().y)* animPro);
}else{
path.moveTo(dataPoint.getPoint().x, dataPoint.getPoint().y);
}
}else{
//quadTo:二阶贝塞尔曲线连接前后两点,这样使得曲线更加平滑
if(animType == AnimType.LEFT_TO_RIGHT){
path.quadTo(rectChart.left+(lastPoint.x-rectChart.left)*animPro, lastPoint.y,
rectChart.left+(dataPoint.getPoint().x-rectChart.left)*animPro , dataPoint.getPoint().y);
}else if(animType == AnimType.BOTTOM_TO_TOP){
path.quadTo(lastPoint.x, rectChart.bottom-(rectChart.bottom-lastPoint.y)* animPro,
dataPoint.getPoint().x, rectChart.bottom-(rectChart.bottom- dataPoint.getPoint().y)* animPro);
}else if(animType == AnimType.SLOW_DRAW){
if(i>linePointList.size()*animPro)
break;
path.quadTo(lastPoint.x, lastPoint.y, dataPoint.getPoint().x, dataPoint.getPoint().y);
}else{
path.quadTo(lastPoint.x, lastPoint.y, dataPoint.getPoint().x, dataPoint.getPoint().y);
}
}
lastPoint = dataPoint.getPoint();
//绘制新闻点
//["2020-09-03",2359.21]
if(newsDate.contains(dataList.get(i).get(0))){
newDatePoint.add(dataPoint);
}
}
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setStrokeJoin(Paint.Join.ROUND);
paint.setStrokeWidth(lineSize);
paint.setColor(lineColor);
canvas.drawPath(path, paint);
//绘制新闻点
int radius = DensityUtil.dip2px(getContext(), 4);
for(DataPoint point : newDatePoint){
// LogUtil.v(TAG, "绘制新闻点:"+point.getPoint());
paint.setStyle(Paint.Style.STROKE);
//新闻点圆圈宽度
paint.setStrokeWidth(DensityUtil.dip2px(getContext(), 3));
paint.setColor(newCircleColor);
canvas.drawCircle(point.getPoint().x, point.getPoint().y, radius,paint);
paint.setStyle(Paint.Style.FILL);
paint.setStrokeWidth(DensityUtil.dip2px(getContext(), 3));
paint.setColor(Color.WHITE);
canvas.drawCircle(point.getPoint().x, point.getPoint().y, radius/2,paint);
}
}
/**绘制焦点*/
private void drawFocus(Canvas canvas){
if(!onFocus || null==focusData)
return;
//竖直线
PathEffect effects = new DashPathEffect(new float[]{15,10,15,10},0);
paintEffect.setStyle(Paint.Style.STROKE);
paintEffect.setStrokeWidth(focusLineSize);
paintEffect.setColor(focusLineColor);
paintEffect.setPathEffect(effects);
Path path = new Path();
path.moveTo(focusData.getPoint().getPoint().x, rectChart.bottom);
path.lineTo(focusData.getPoint().getPoint().x, rectChart.top);
canvas.drawPath(path , paintEffect);
}
private float animPro; //动画计算的占比数量
/**创建动画*/
@Override
protected ValueAnimator initAnim() {
if(dataList.size()>0) {
ValueAnimator anim = ValueAnimator.ofObject(new AngleEvaluator(), 0f, 1f);
anim.setInterpolator(new AccelerateDecelerateInterpolator());
return anim;
}
return null;
}
/**动画值变化之后计算数据*/
@Override
protected void evaluatorData(ValueAnimator animation) {
animPro = (float)animation.getAnimatedValue();
}
/**焦点数据*/
public static class FocusData {
private DataPoint point;
private List<Object> data;
private TopDetailNew news;
public DataPoint getPoint() {
return point;
}
public void setPoint(DataPoint point) {
this.point = point;
}
public List<Object> getData() {
return data;
}
public void setData(List<Object> data) {
this.data = data;
}
public TopDetailNew getNews() {
return news;
}
public void setNews(TopDetailNew news) {
this.news = news;
}
}
}
@@ -0,0 +1,460 @@
package com.openxu.cview.xmstock20201030;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathEffect;
import android.graphics.PointF;
import android.graphics.RectF;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.util.Log;
import android.view.animation.AccelerateDecelerateInterpolator;
import com.openxu.hkchart.config.YAxisMark;
import com.openxu.cview.R;
import com.openxu.cview.chart.anim.AngleEvaluator;
import com.openxu.cview.xmstock.BaseChart;
import com.openxu.cview.xmstock.bean.DataPoint;
import com.openxu.utils.DensityUtil;
import com.openxu.utils.FontUtil;
import com.openxu.utils.NumberFormatUtil;
import java.util.ArrayList;
import java.util.List;
/**
* autour : xiami
* date : 2020/11/13 14:26
* className : SshqLinesChart
* version : 1.0
* description : 3. 实时行情图
*/
public class SshqLinesChart extends BaseChart {
//实时行情走势图,每个元素字段分别是:时间、现价、均价 、跌涨浮["0930",1639.83,1625.58,"0.10"]
private List<List<Object>> dataList;
private int dataNumCount = 60*4+2; //9:30 - 11:30 13:00 - 15:00 2+2=4小时
private String[] lableXList = new String[]{"09:30", "11:30/13:00", "15:00"};
//计算后的数据
private List<DataPoint> lableXPointList;
private List<DataPoint> linePointList;
//y轴刻度设置
private YAxisMark yLeft, yRight;
/**可以设置的属性*/
//设置线条颜色(蓝色,渐变色开始)
private int[] lineColor = new int[]{Color.parseColor("#3d7cc9"),
Color.parseColor("#3d7cc9")};
//设置曲线粗细
private int lineSize = DensityUtil.dip2px(getContext(), 1.5f);
//设置坐标文字大小
private int textSizeX = (int)getResources().getDimension(R.dimen.ts_chart_xy);
//设置坐标文字颜色
private int textColorX = getResources().getColor(R.color.tc_chart_xy);
//设置X坐标字体与横轴的距离
private int textSpaceX = DensityUtil.dip2px(getContext(), 5);
//设置焦点线颜色 及 粗细
private int focusLineColor = getResources().getColor(R.color.tc_chart_focus_line);
private int focusLineSize = DensityUtil.dip2px(getContext(), 0.8f);
public SshqLinesChart(Context context) {
this(context, null);
}
public SshqLinesChart(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SshqLinesChart(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public void init(Context context, AttributeSet attrs, int defStyleAttr) {
dataList = new ArrayList<>();
touchEnable = false;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(widthSize, heightSize);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
evaluatorByData();
startDraw = false;
isLoading = false;
invalidate();
}
//20201028 --> 10-28
private String formatDate(String date){
String str = date.substring(4, 6)+"-"+date.substring(6);
Log.v(TAG, date+ "---->"+str);
return str;
}
/**
* 设置数据
*/
public void setData(List<List<Object>> dataList){
if(null==dataList)
return;
this.dataList.clear();
this.dataList.addAll(dataList);
//实时行情走势图,每个元素字段分别是:时间、现价、均价 、跌涨浮["0930",1639.83,1625.58,"0.10"]
yLeft = new YAxisMark.Builder(getContext())
.lableNum(3)
.textSize(textSizeX)
.textColor(lineColor[0])
.build();
yRight = new YAxisMark.Builder(getContext())
.lableNum(3)
.textSize(textSizeX)
.textColor(lineColor[1])
.build();
if(getMeasuredWidth()>0) {
evaluatorByData();
startDraw = false;
isLoading = false;
invalidate();
}
}
/**设置数据后,计算相关值*/
private void evaluatorByData(){
if(dataList.size()<=0)
return;
/**③、计算Y刻度最大值和最小值以及幅度*/
//实时行情走势图,每个元素字段分别是:时间、现价、均价 、跌涨浮["0930",1639.83,1625.58,"0.10"]
calYLable();
/**①、计算字体相关以及图表原点坐标*/
paintLabel.setTextSize(textSizeX);
float xlableHeight = FontUtil.getFontHeight(paintLabel);
float xlableLead = FontUtil.getFontLeading(paintLabel);
paintLabel.setTextSize(yLeft.textSize);
float yLeftlableHeight = FontUtil.getFontHeight(paintLabel);
float yLeftlableMaxLength = FontUtil.getFontlength(paintLabel, (int)yLeft.cal_mark_max+"");
paintLabel.setTextSize(yRight.textSize);
float yRightlableHeight = FontUtil.getFontHeight(paintLabel);
String text = NumberFormatUtil.formattedDecimalToPercentage(yRight.cal_mark_min, 2);
float yRightlableMaxLength = FontUtil.getFontlength(paintLabel, text);
//图表主体矩形
rectChart = new RectF(getPaddingLeft() + yLeft.textSpace + yLeftlableMaxLength,
getPaddingTop() + Math.max(yLeftlableHeight, yRightlableHeight)/2,
getMeasuredWidth()-getPaddingRight()-yRight.textSpace - yRightlableMaxLength,
getMeasuredHeight()-getPaddingBottom() - xlableHeight - textSpaceX);
Log.w(TAG, "计算图表矩形:"+rectChart);
/**②、计算X标签绘制坐标*/
float lableXSpace = 0;
for(String lableX : lableXList){
lableXSpace += FontUtil.getFontlength(paintLabel, lableX);
}
lableXSpace = (rectChart.right - rectChart.left - lableXSpace)/(lableXList.length -1);
lableXPointList = new ArrayList<>();
if(lableXSpace>0){
float left = rectChart.left;
for(int i = 0; i<lableXList.length; i++){
String lableX = lableXList[i];
lableXPointList.add(new DataPoint(lableX, 0, new PointF(
left,rectChart.bottom + textSpaceX + xlableLead)));
left += (FontUtil.getFontlength(paintLabel, lableXList[i])+lableXSpace);
}
}else{
//如果X轴标签字体过长的情况需要特殊处理
float oneWidth = (rectChart.right - rectChart.left)/lableXList.length;
for(int i = 0; i<lableXList.length; i++){
String lableX = lableXList[i];
float xLen = FontUtil.getFontlength(paintLabel, lableX);
lableXPointList.add(new DataPoint(lableX, 0,
new PointF(rectChart.left+i*oneWidth+(oneWidth-xLen)/2,
rectChart.bottom + textSpaceX + xlableLead)));
}
}
/**④、计算点的坐标,如果有动画的情况下,边绘制边计算会耗费性能,所以先计算*/
linePointList = new ArrayList<>();
float oneSpace = (rectChart.right - rectChart.left) / (dataNumCount-1);
for(int i = 0; i < dataList.size(); i++){
// //实时行情走势图,每个元素字段分别是:时间、现价、均价 、跌涨浮["0930",1639.83,1625.58,"0.10"]
List<Object> onePart = dataList.get(i);
DataPoint dataPoint = new DataPoint(onePart.get(0).toString(), 0, new PointF(rectChart.left + i * oneSpace,
rectChart.bottom));
try {
//第一条线的数据
float valueY = Float.parseFloat(onePart.get(1).toString());
dataPoint.setValueY(valueY);
dataPoint.getPoint().y = rectChart.bottom - (rectChart.bottom-rectChart.top)/(yLeft.cal_mark_max - yLeft.cal_mark_min) * (valueY-yLeft.cal_mark_min);
}catch (Exception e){
e.printStackTrace();
}
linePointList.add(dataPoint);
}
}
private FocusData focusData;
@Override
protected void onTouchMoved(PointF point) {
if(null==dataList)
return;
/* onFocus = (null != point);
if(null != point && null!=dataList && dataList.size()>0) {
//避免滑出
if(point.x > linePointList.get(0).get(dataList.size()-1).getPoint().x)
point.x = linePointList.get(0).get(dataList.size()-1).getPoint().x;
if(point.x < linePointList.get(0).get(0).getPoint().x)
point.x = linePointList.get(0).get(0).getPoint().x;
//获取焦点对应的数据的索引
int index = (int) ((point.x - rectChart.left) * dataList.size() / (rectChart.right - rectChart.left));
LogUtil.e(getClass().getSimpleName(), "========焦点索引:"+index+" 数据总数:"+dataList.size()+" 线条数量:"+linePointList.size());
index = Math.max(0, Math.min(index, dataList.size() - 1));
focusData = new FocusData();
focusData.setData(dataList.get(index));
focusData.setPoints(new ArrayList<>());
focusData.getPoints().add(linePointList.get(0).get(index));
focusData.getPoints().add(linePointList.get(1).get(index));
if(null!=onFocusChangeListener)
onFocusChangeListener.onfocus(focusData);
}
invalidate();*/
}
private OnFocusChangeListener onFocusChangeListener;
public void setOnFocusChangeListener(OnFocusChangeListener onFocusChangeListener) {
this.onFocusChangeListener = onFocusChangeListener;
}
public interface OnFocusChangeListener{
public void onfocus(FocusData focusData);
}
/**绘制图表基本框架*/
@Override
public void drawDefult(Canvas canvas) {
if(null==linePointList || linePointList.size()<=0)
return;
paint.setStyle(Paint.Style.FILL);
paint.setStrokeWidth(lineWidth);
paint.setColor(defColor);
drawGrid(canvas);
drawXLable(canvas);
drawYLable(canvas);
}
/**绘制debug辅助线*/
@Override
public void drawDebug(Canvas canvas) {
super.drawDebug(canvas);
}
/**绘制图表*/
@Override
public void drawChart(Canvas canvas) {
if(null==linePointList || linePointList.size()<=0)
return;
drawDataPath(canvas);
drawFocus(canvas);
}
/**绘制X轴方向辅助网格*/
private void drawGrid(Canvas canvas){
float yMarkSpace = (rectChart.bottom - rectChart.top)/(yLeft.lableNum-1);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(lineWidth);
paint.setColor(defColor);
paintEffect.setStyle(Paint.Style.STROKE);
paintEffect.setStrokeWidth(lineWidth);
paintEffect.setColor(defColor);
// canvas.drawLine(rectChart.left, rectChart.top, rectChart.left, rectChart.bottom, paint);
// canvas.drawLine(rectChart.right, rectChart.top, rectChart.right, rectChart.bottom, paint);
PathEffect effects = new DashPathEffect(new float[]{15,6,15,6},0);
Path path = new Path();
for (int i = 0; i < yLeft.lableNum; i++) {
path.reset();
path.moveTo(rectChart.left, rectChart.bottom-yMarkSpace*i);
path.lineTo(rectChart.right,rectChart.bottom-yMarkSpace*i);
paintEffect.setPathEffect(effects);
canvas.drawPath(path, paintEffect);
}
}
/**绘制X轴刻度*/
private void drawXLable(Canvas canvas){
paintLabel.setTextSize(textSizeX);
paintLabel.setColor(textColorX);
for(DataPoint lable : lableXPointList){
canvas.drawText(lable.getValueX(), lable.getPoint().x, lable.getPoint().y, paintLabel);
}
}
/**绘制Y轴刻度*/
private void drawYLable(Canvas canvas){
float yMarkSpace = (rectChart.bottom - rectChart.top)/(yLeft.lableNum-1);
paintLabel.setTextSize(yLeft.textSize);
paintLabel.setColor(yLeft.textColor);
float lableHeight = FontUtil.getFontHeight(paintLabel);
float lableLead = FontUtil.getFontLeading(paintLabel);
for (int i = 0; i < yLeft.lableNum; i++) {
String text = (int) (yLeft.cal_mark_min + i * yLeft.cal_mark) + "";
canvas.drawText(text,
rectChart.left - yLeft.textSpace - FontUtil.getFontlength(paintLabel, text),
rectChart.bottom - yMarkSpace * i - lableHeight/2 + lableLead, paintLabel);
}
paintLabel.setTextSize(yRight.textSize);
lableHeight = FontUtil.getFontHeight(paintLabel);
lableLead = FontUtil.getFontLeading(paintLabel);
for (int i = 0; i < yRight.lableNum; i++) {
//最后一个涨幅值刻度为红色
paintLabel.setColor(i==yRight.lableNum-1?Color.RED: yRight.textColor);
String text = NumberFormatUtil.formattedDecimal(yRight.cal_mark_min + i * yRight.cal_mark)+"%";
canvas.drawText(text,
rectChart.right + yRight.textSpace,
rectChart.bottom - yMarkSpace * i - lableHeight/2 + lableLead, paintLabel);
}
}
/**绘制曲线*/
private void drawDataPath(Canvas canvas) {
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setStrokeJoin(Paint.Join.ROUND);
paint.setStrokeWidth(lineSize);
paint.setColor(lineColor[0]);
//曲线路径
Path path = new Path();
//渐变色路径
Path jianBianPath = new Path();
PointF lastPoint = null;
PointF lastDrawPoint = new PointF();
float minYValue = 0;
for(int i = 0 ; i<linePointList.size(); i++){
DataPoint dataPoint = linePointList.get(i);
if(i == 0){
jianBianPath.moveTo(rectChart.left, rectChart.bottom);
lastDrawPoint.x = dataPoint.getPoint().x;
lastDrawPoint.y = dataPoint.getPoint().y;
minYValue = Math.min(lastDrawPoint.y,minYValue);
path.moveTo(lastDrawPoint.x, lastDrawPoint.y);
jianBianPath.lineTo(lastDrawPoint.x, lastDrawPoint.y);
}else{
if(i>linePointList.size()*animPro)
break;
lastDrawPoint.x = dataPoint.getPoint().x;
lastDrawPoint.y = dataPoint.getPoint().y;
path.quadTo(lastPoint.x, lastPoint.y, lastDrawPoint.x, lastDrawPoint.y);
minYValue = Math.min(lastDrawPoint.y,minYValue);
// path.moveTo(lastDrawPoint.x, lastDrawPoint.y);
jianBianPath.lineTo(lastDrawPoint.x, lastDrawPoint.y);
}
lastPoint = dataPoint.getPoint();
}
canvas.drawPath(path, paint);
//https://blog.csdn.net/s297165331/article/details/52875624
jianBianPath.lineTo(lastDrawPoint.x, rectChart.bottom);
jianBianPath.close();
//线性渐变:前两个参数是渐变开始的点坐标,第三四个参数是渐变结束的点的坐标;渐变的颜色,渐变颜色的分布,模式
Shader mShader = new LinearGradient(rectChart.left, minYValue ,rectChart.left, rectChart.bottom,
new int[] {lineColor[1],Color.TRANSPARENT},null,Shader.TileMode.CLAMP);
// Log.w(TAG, "从"+rectChart.left+"*"+minYValue+"->"+rectChart.left+"*"+rectChart.bottom+"渐变");
paintEffect.setStyle(Paint.Style.FILL);
paintEffect.setColor(defColor);
paintEffect.setShader(mShader);
canvas.drawPath(jianBianPath, paintEffect);
paintEffect.setShader(null);
}
/**绘制焦点*/
private void drawFocus(Canvas canvas){
if(!onFocus || null==focusData)
return;
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.FILL);
paint.setStrokeWidth(focusLineSize);
paint.setColor(focusLineColor);
canvas.drawLine(focusData.getPoints().get(0).getPoint().x, rectChart.bottom, focusData.getPoints().get(0).getPoint().x, rectChart.top, paint);
canvas.drawLine(rectChart.left, focusData.getPoints().get(0).getPoint().y, rectChart.right, focusData.getPoints().get(0).getPoint().y, paint);
canvas.drawLine(rectChart.left, focusData.getPoints().get(1).getPoint().y, rectChart.right, focusData.getPoints().get(1).getPoint().y, paint);
}
private float animPro; //动画计算的占比数量
/**创建动画*/
@Override
protected ValueAnimator initAnim() {
if(dataList.size()>0) {
ValueAnimator anim = ValueAnimator.ofObject(new AngleEvaluator(), 0f, 1f);
anim.setInterpolator(new AccelerateDecelerateInterpolator());
return anim;
}
return null;
}
/**动画值变化之后计算数据*/
@Override
protected void evaluatorData(ValueAnimator animation) {
animPro = (float)animation.getAnimatedValue();
}
/**焦点数据*/
public static class FocusData {
private List<DataPoint> points;
private List<Object> data;
public List<DataPoint> getPoints() {
return points;
}
public void setPoints(List<DataPoint> points) {
this.points = points;
}
public List<Object> getData() {
return data;
}
public void setData(List<Object> data) {
this.data = data;
}
}
/**
* 根据传入的Y轴信息,从数据中获取最小最大值,并获取到Y刻度点值
*/
private void calYLable(){
float redundance = 1.01f; //y轴最大和最小值冗余
yLeft.cal_mark_max = Float.MIN_VALUE; //Y轴刻度最大值
yLeft.cal_mark_min = Float.MAX_VALUE; //Y轴刻度最小值
yRight.cal_mark_max = Float.MIN_VALUE; //记录最大涨幅
yRight.cal_mark_min = Float.MAX_VALUE; //记录最大跌幅
//实时行情走势图,每个元素字段分别是:时间、现价、均价 、跌涨浮["0930",1639.83,1625.58,"0.10"]
float price, downUp;
for(List<Object> data : dataList){
try {
price = Float.parseFloat(data.get(1).toString());
downUp = Float.parseFloat(data.get(3).toString());
yLeft.cal_mark_max = Math.max(yLeft.cal_mark_max, price);
yLeft.cal_mark_min = Math.min(yLeft.cal_mark_min, price);
yRight.cal_mark_max = Math.max(yRight.cal_mark_max, downUp);
yRight.cal_mark_min = Math.min(yRight.cal_mark_min, downUp);
}catch (Exception e){
e.printStackTrace();
}
}
Log.i(TAG, "Y轴真实yLeft.cal_mark_min="+yLeft.cal_mark_min+" yLeft.cal_mark_max="+yLeft.cal_mark_max);
Log.i(TAG, "Y轴真实yRight.cal_mark_min="+yRight.cal_mark_min+" yRight.cal_mark_max="+yRight.cal_mark_max);
yLeft.cal_mark_max *= redundance;
yLeft.cal_mark_min /= redundance;
yLeft.cal_mark = (yLeft.cal_mark_max-yLeft.cal_mark_min)/(yLeft.lableNum - 1);
//获取正负跌涨浮绝对值最大值
downUp = Math.max(Math.abs(yRight.cal_mark_max), Math.abs(yRight.cal_mark_min));
downUp *= redundance;
yRight.cal_mark_max = downUp;
yRight.cal_mark_min = -downUp;
yRight.cal_mark = downUp;
Log.i(TAG, "yLeft.cal_mark_min="+yLeft.cal_mark_min+" yLeft.cal_mark_max="+yLeft.cal_mark_max+" yLeft.cal_mark="+yLeft.cal_mark);
Log.i(TAG, "yRight.cal_mark_min="+yRight.cal_mark_min+" yRight.cal_mark_max="+yRight.cal_mark_max+" yRight.cal_mark="+yRight.cal_mark);
}
}
@@ -0,0 +1,618 @@
package com.openxu.cview.xmstock20201030;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathEffect;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.animation.AccelerateDecelerateInterpolator;
import com.openxu.cview.R;
import com.openxu.cview.chart.anim.AngleEvaluator;
import com.openxu.cview.xmstock.BaseChart;
import com.openxu.cview.xmstock20201030.build.AnimType;
import com.openxu.cview.xmstock20201030.build.AxisLine;
import com.openxu.cview.xmstock20201030.build.AxisLineType;
import com.openxu.cview.xmstock20201030.build.AxisMark;
import com.openxu.cview.xmstock20201030.build.ChartFocus;
import com.openxu.cview.xmstock20201030.build.DataPoint;
import com.openxu.cview.xmstock20201030.build.Line;
import com.openxu.cview.xmstock20201030.build.Orientation;
import com.openxu.utils.ChartCalUtil;
import com.openxu.utils.DensityUtil;
import com.openxu.utils.FontUtil;
import com.openxu.utils.ReflectUtil;
import java.util.ArrayList;
import java.util.List;
/**
* autour : openXu
* date : 2020/10/31 14:26
* className : StandLinesChart
* version : 1.0
* description : 通用的折线图表
*/
public class StandLinesChart extends BaseChart {
//折线数据, 多少元素就有多少条线, 需要计算
private List<Line> lineList;
//坐标轴刻度
private AxisMark xAxisMark;
private AxisMark yLeftAxisMark;
private AxisMark yRightAxisMark;
//坐标轴线
private AxisLine[] xAxisLines;
private AxisLine[] yAxisLines;
private Builder builder;
public Builder builder(){
if (builder==null)
this.builder = new Builder();
return builder;
}
public class Builder{
/**
* 设置一条曲线
* @param line
* @return
*/
public Builder line(Line line){
if(lineList == null)
lineList = new ArrayList<>();
lineList.add(line) ;
return this;
}
/**
* 设置X轴刻度
* @param xAxisMark
* @return
*/
public Builder xAxisMark(AxisMark xAxisMark){
if(xAxisMark.lableOrientation==null)
xAxisMark.lableOrientation = Orientation.BOTTOM;
//计算x轴刻度值
if(xAxisMark.lables==null)
ChartCalUtil.calXLable(xAxisMark);
StandLinesChart.this.xAxisMark = xAxisMark;
//上面计算了X轴刻度,说明确定了竖直方向有几根轴,下面给予默认的竖直方向轴样式
yAxisLines = new AxisLine[xAxisMark.lableNum];
for(int i = 0; i<xAxisMark.lableNum; i++){
//第一条和最后一条线为实线,其他竖直方向不划线
yAxisLines[i] = new AxisLine.Builder(getContext())
.lineType((i ==0 || i == xAxisMark.lableNum-1)?AxisLineType.SOLID:AxisLineType.NONE)
.build();
}
return this;
}
/**
* 设置Y轴左侧刻度
* @param yLeftAxisMark
* @return
*/
public Builder yLeftAxisMark(AxisMark yLeftAxisMark){
if(yLeftAxisMark.lableOrientation==null)
yLeftAxisMark.lableOrientation = Orientation.LEFT;
//计算Y刻度
if(yLeftAxisMark.lables==null)
ChartCalUtil.calYLable(yLeftAxisMark);
StandLinesChart.this.yLeftAxisMark = yLeftAxisMark;
setXAxisLines(yLeftAxisMark);
return this;
}
/**
* 设置Y轴右侧刻度
* @param yRightAxisMark
* @return
*/
public Builder yRightAxisMark(AxisMark yRightAxisMark){
if(yRightAxisMark.lableOrientation==null)
yRightAxisMark.lableOrientation = Orientation.RIGHT;
if(yRightAxisMark.showLable && yRightAxisMark.lables==null)
ChartCalUtil.calYLable(yRightAxisMark);
StandLinesChart.this.yRightAxisMark = yRightAxisMark;
setXAxisLines(yRightAxisMark);
return this;
}
/**
* 设置竖直方向第index条网格线,0 <= index<= xAxisMark.lableNum-1
* @param index
* @param axisLine
* @return
*/
public Builder verticalAxisLine(int index, AxisLine axisLine){
if(xAxisMark==null) {
//设置竖直方向坐标轴网格线样式时,必须线调用xAxisMark()方法设置x轴刻度
throw new RuntimeException("please call xAxisMark() to determine the vertical line before this");
}
if(index>=yAxisLines.length){
throw new RuntimeException("index over size");
}
yAxisLines[index] = axisLine;
return this;
}
/**
* 设置水平方向第index条网格线,0 <= index<= yAxisMark.lableNum-1
* @param index
* @param axisLine
* @return
*/
public Builder horizontalAxisLine(int index, AxisLine axisLine){
if(yLeftAxisMark==null && yRightAxisMark==null) {
//设置水平方向坐标轴网格线样式时,必须线调用yLeftAxisMark()或者yRightAxisMark()设置y轴刻度
throw new RuntimeException("please call yLeftAxisMark() or yRightAxisMark() to determine the horizontal line before this");
}
if(index>=xAxisLines.length){
throw new RuntimeException("index over size");
}
xAxisLines[index] = axisLine;
return this;
}
private void setXAxisLines(AxisMark yAxisMark){
if(xAxisLines!=null)
return;
xAxisLines = new AxisLine[yAxisMark.lableNum];
for(int i = 0; i<yAxisMark.lableNum; i++){
//第一条和最后一条线为实线,其他竖直方向不划线
xAxisLines[i] = new AxisLine.Builder(getContext())
.lineType((i ==0 || i == xAxisMark.lableNum-1)?AxisLineType.SOLID:AxisLineType.DASHE)
.build();
}
}
public void build(){
isLoading = false;
evaluatorByData();
startDraw = false;
invalidate();
}
}
public StandLinesChart(Context context) {
this(context, null);
}
public StandLinesChart(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public StandLinesChart(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public void init(Context context, AttributeSet attrs, int defStyleAttr) {
touchEnable = true;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(widthSize, heightSize);
Log.w(TAG, "======onMeasure:"+widthSize+" * "+heightSize);
}
/**设置数据后,计算相关值*/
private void evaluatorByData() {
float lableHeight;
float lableLead;
/**1. 确定图表绘制矩形*/
if (xAxisMark != null) {
paintLabel.setTextSize(xAxisMark.textSize);
lableHeight = FontUtil.getFontHeight(paintLabel);
//确定图表最下放绘制位置
if (xAxisMark.showLable && xAxisMark.lableOrientation == Orientation.BOTTOM) {
rectChart.bottom = getMeasuredHeight() - getPaddingBottom() - lableHeight - xAxisMark.textSpace;
}
}
//计算Y刻度
if (yLeftAxisMark != null) {
if (yLeftAxisMark.showLable && yLeftAxisMark.lableOrientation == Orientation.LEFT) {
paintLabel.setTextSize(yLeftAxisMark.textSize);
String maxLable = "";
for (String lable : yLeftAxisMark.lables) {
if (maxLable.length() < lable.length())
maxLable = lable;
}
rectChart.left = getPaddingLeft() + yLeftAxisMark.textSpace +
FontUtil.getFontlength(paintLabel, maxLable);
}
}
if (yRightAxisMark != null) {
if (yRightAxisMark.showLable && yRightAxisMark.lableOrientation == Orientation.RIGHT) {
paintLabel.setTextSize(yRightAxisMark.textSize);
String maxLable = "";
for (String lable : yRightAxisMark.lables) {
if (maxLable.length() < lable.length())
maxLable = lable;
}
rectChart.right = getMeasuredWidth() - getPaddingRight() - yRightAxisMark.textSpace -
FontUtil.getFontlength(paintLabel, maxLable);
}
}
Log.v(TAG, "计算图表矩形框:" + rectChart);
/**2. 计算X轴绘制点坐标*/
float markSpace;
if (xAxisMark != null) {
markSpace = (rectChart.right - rectChart.left) * 1.0f / (xAxisMark.lableNum - 1);
xAxisMark.markPointList.clear();
paintLabel.setTextSize(xAxisMark.textSize);
lableHeight = FontUtil.getFontHeight(paintLabel);
lableLead = FontUtil.getFontHeight(paintLabel);
float left = rectChart.left;
for (int i = 0; i < xAxisMark.lableNum; i++) {
if (i >= 1 && i < xAxisMark.lableNum - 1) {
left = rectChart.left + i * markSpace - FontUtil.getFontlength(paintLabel, xAxisMark.lables[i]) / 2;
} else if (i == xAxisMark.lableNum - 1) {
left = rectChart.right - FontUtil.getFontlength(paintLabel, xAxisMark.lables[i]);
}
/**x轴刻度位置确定竖直方向的轴线*/
yAxisLines[i].pointStart = new PointF(rectChart.left + i * markSpace, rectChart.bottom);
yAxisLines[i].pointEnd = new PointF(rectChart.left + i * markSpace, rectChart.top);
xAxisMark.markPointList.add(new AxisMark.MarkPoint(xAxisMark.lables[i],
new PointF(
left,
rectChart.bottom + xAxisMark.textSpace + lableLead
)));
}
}
/**3. 计算Y轴绘制点坐标*/
if (yLeftAxisMark != null) {
markSpace = (rectChart.bottom - rectChart.top) / (yLeftAxisMark.lableNum - 1);
yLeftAxisMark.markPointList.clear();
paintLabel.setTextSize(yLeftAxisMark.textSize);
lableHeight = FontUtil.getFontHeight(paintLabel);
lableLead = FontUtil.getFontHeight(paintLabel);
for (int i = 0; i < yLeftAxisMark.lableNum; i++) {
/**y轴刻度位置确定水平方向的轴线*/
xAxisLines[i].pointStart = new PointF(rectChart.left, rectChart.bottom - i * markSpace);
xAxisLines[i].pointEnd = new PointF(rectChart.right, rectChart.bottom - i * markSpace);
switch (yLeftAxisMark.lableOrientation) {
case LEFT: //左外居中
yLeftAxisMark.markPointList.add(new AxisMark.MarkPoint(yLeftAxisMark.lables[i],
new PointF(
rectChart.left - FontUtil.getFontlength(paintLabel, yLeftAxisMark.lables[i]) - yLeftAxisMark.textSpace,
rectChart.bottom - i * markSpace - lableHeight / 2 + lableLead
)));
break;
case TOP: //左内靠上
yLeftAxisMark.markPointList.add(new AxisMark.MarkPoint(yLeftAxisMark.lables[i],
new PointF(
rectChart.left + yLeftAxisMark.textSpace,
rectChart.bottom - i * markSpace - lableHeight - yLeftAxisMark.textSpace + lableLead
)));
break;
}
}
}
if (yRightAxisMark != null) {
markSpace = (rectChart.bottom - rectChart.top) / (yLeftAxisMark.lableNum - 1);
yRightAxisMark.markPointList.clear();
paintLabel.setTextSize(yRightAxisMark.textSize);
lableHeight = FontUtil.getFontHeight(paintLabel);
lableLead = FontUtil.getFontHeight(paintLabel);
for (int i = 0; i < yRightAxisMark.lableNum; i++) {
/**y轴刻度位置确定水平方向的轴线*/
xAxisLines[i].pointStart = new PointF(rectChart.left, rectChart.bottom - i * markSpace);
xAxisLines[i].pointEnd = new PointF(rectChart.right, rectChart.bottom - i * markSpace);
switch (yRightAxisMark.lableOrientation) {
case RIGHT: //右外居中
yRightAxisMark.markPointList.add(new AxisMark.MarkPoint(yRightAxisMark.lables[i],
new PointF(
rectChart.right + yRightAxisMark.textSpace,
rectChart.bottom - i * markSpace - lableHeight / 2 + lableLead
)));
break;
case TOP: //右内靠上
yRightAxisMark.markPointList.add(new AxisMark.MarkPoint(yRightAxisMark.lables[i],
new PointF(
rectChart.right + yRightAxisMark.textSpace,
rectChart.bottom - i * markSpace - lableHeight - yRightAxisMark.textSpace + lableLead
)));
break;
}
}
}
/**4. 计算曲线*/
if (lineList != null && lineList.size() > 0) {
for (Line line : lineList) {
line.linePointList.clear();
AxisMark axis = line.orientation == Orientation.LEFT ? yLeftAxisMark : yRightAxisMark;
//该折线上点的总数,不能直接取datas.size(),有些图表可能规定有数量,比如股票分时图,如果datas.size()<line.dataNumCount,则这个折线图不能绘制到终点
line.dataNumCount = line.dataNumCount == 0 ? line.datas.size() : line.dataNumCount;
float oneSpace = (rectChart.right - rectChart.left) / (line.dataNumCount - 1);
for (int i = 0; i < line.datas.size(); i++) {
String str_y = ReflectUtil.getField(line.datas.get(i), line.field_y).toString();
String str_x = ReflectUtil.getField(line.datas.get(i), line.field_x).toString();
float valueY;
if (str_y.contains("%"))
valueY = Float.parseFloat(str_y.substring(0, str_y.indexOf("%"))) / 100.0f;
else
valueY = Float.parseFloat(str_y);
line.linePointList.add(new DataPoint(str_x, str_y, new PointF(
rectChart.left + i * oneSpace,
//根据最大值和最小值,计算当前数据在图表上Y轴的坐标
rectChart.bottom -
(rectChart.bottom - rectChart.top) / (axis.cal_mark_max - axis.cal_mark_min)
* (valueY - axis.cal_mark_min)
)));
}
}
}
}
/**绘制图表基本框架*/
@Override
public void drawDefult(Canvas canvas) {
drawGrid(canvas);
drawXYLable(canvas);
}
/**绘制图表*/
@Override
public void drawChart(Canvas canvas) {
drawDataPath(canvas);
drawFocus(canvas);
}
/**1. 绘制坐标轴网格*/
private void drawGrid(Canvas canvas){
//绘制水平轴线
if(xAxisLines==null || xAxisLines.length==0)
return;
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.STROKE);
paintEffect.setStyle(Paint.Style.STROKE);
for (AxisLine axisLine : xAxisLines) {
if(axisLine.type == AxisLineType.NONE)
continue;
drawAxisLine(canvas, axisLine);
}
//绘制Y轴方向辅助网格 上下
for (AxisLine axisLine : yAxisLines) {
if(axisLine.type == AxisLineType.NONE)
continue;
drawAxisLine(canvas, axisLine);
}
}
private void drawAxisLine(Canvas canvas, AxisLine axisLine){
if(axisLine.type == AxisLineType.SOLID){ //实线
paint.setStrokeWidth(axisLine.lineWidth);
paint.setColor(axisLine.lineColor);
canvas.drawLine(axisLine.pointStart.x, axisLine.pointStart.y,
axisLine.pointEnd.x, axisLine.pointEnd.y, paint);
}
if(axisLine.type == AxisLineType.DASHE){ //虚线
paintEffect.setStrokeWidth(axisLine.lineWidth);
paintEffect.setColor(axisLine.lineColor);
Path path = new Path();
path.moveTo(axisLine.pointStart.x, axisLine.pointStart.y);
path.lineTo(axisLine.pointEnd.x,axisLine.pointEnd.y);
PathEffect effects = new DashPathEffect(new float[]{15,8,15,8},0);
paintEffect.setPathEffect(effects);
canvas.drawPath(path, paintEffect);
}
}
/**绘制X轴刻度*/
private void drawXYLable(Canvas canvas){
drawAxisLable(canvas, xAxisMark);
drawAxisLable(canvas, yLeftAxisMark);
drawAxisLable(canvas, yRightAxisMark);
}
private void drawAxisLable(Canvas canvas, AxisMark axisMark){
if(axisMark==null)
return;
if(axisMark.showLable){
paintLabel.setTextSize(axisMark.textSize);
paintLabel.setColor(axisMark.textColor);
for(AxisMark.MarkPoint point : axisMark.markPointList){
canvas.drawText(point.value, point.point.x, point.point.y, paintLabel);
}
}
}
/**绘制曲线*/
private float smoothness=0.15f;
private void drawDataPath(Canvas canvas) {
if(lineList==null || lineList.size()==0)
return;
// animPro = .5f;
paint.setAntiAlias(true);
// paint.setStyle(Paint.Style.STROKE);
// paint.setStrokeCap(Paint.Cap.ROUND);
// paint.setStrokeJoin(Paint.Join.ROUND);
if(null==lineList || lineList.size()<=0)
return;
PointF prePoint = null;
int diffx, diffy;
for(int lineNum = 0; lineNum < lineList.size(); lineNum ++){
Line line = lineList.get(lineNum);
//一条一条的绘制
paint.setStrokeWidth(line.lineWidth);
paint.setColor(line.lineColor);
Path path = new Path();
for(int i = 0; i <line.dataNumCount; i++){
PointF point = ((DataPoint)line.linePointList.get(i)).point;
//下一个点
// nextPoint = i+1<line.dataNumCount?((DataPoint)line.linePointList.get(i+1)).point:point;
if(i == 0){
if(line.animType == AnimType.LEFT_TO_RIGHT){
path.moveTo(rectChart.left+(point.x-rectChart.left)*animPro, point.y);
}else if(line.animType == AnimType.BOTTOM_TO_TOP){
path.moveTo(point.x, rectChart.bottom-(rectChart.bottom - point.y)* animPro);
}else{
path.moveTo(point.x, point.y);
}
// canvas.drawCircle(point.x, point.y, 5.0f, paint);
// path.moveTo(point.x, point.y);
// Log.w(TAG, lineNum+"起点:"+point);
}else{
if(line.lineType == Line.LineType.CURVE) {
//quadTo:二阶贝塞尔曲线连接前后两点,这样使得曲线更加平滑
// * @param x1 The x-coordinate of the control point on a quadratic curve
// * @param y1 The y-coordinate of the control point on a quadratic curve
// * @param x2 The x-coordinate of the end point on a quadratic curve
// * @param y2 The y-coordinate of the end point on a quadratic curve
//cubicTo : 三阶
diffx = (int) ((point.x - prePoint.x) * smoothness);
if (line.animType == AnimType.LEFT_TO_RIGHT) {
path.cubicTo(rectChart.left + (prePoint.x + diffx - rectChart.left) * animPro,
prePoint.y,
rectChart.left + (point.x - diffx - rectChart.left) * animPro,
point.y,
rectChart.left + (point.x - rectChart.left) * animPro,
point.y);
} else if (line.animType == AnimType.BOTTOM_TO_TOP) {
path.cubicTo(prePoint.x + diffx, rectChart.bottom - (rectChart.bottom - prePoint.y) * animPro,
point.x - diffx, rectChart.bottom - (rectChart.bottom - point.y) * animPro,
point.x, rectChart.bottom - (rectChart.bottom - point.y) * animPro);
} else if (line.animType == AnimType.SLOW_DRAW) {
if (i > lineList.size() * animPro)
break;
path.cubicTo(prePoint.x + diffx, prePoint.y,
point.x - diffx, point.y,
point.x, point.y);
} else if (line.animType == AnimType.NONE) {
path.cubicTo(prePoint.x + diffx, prePoint.y,
point.x - diffx, point.y,
point.x, point.y);
}
// path.cubicTo(prePoint.x+diffx, prePoint.y,
// point.x-diffx, point.y,
// point.x, point.y);
}else if(line.lineType == Line.LineType.BROKEN){
if (line.animType == AnimType.LEFT_TO_RIGHT) {
path.lineTo(rectChart.left + (point.x - rectChart.left) * animPro,
point.y);
} else if (line.animType == AnimType.BOTTOM_TO_TOP) {
path.lineTo(point.x, rectChart.bottom - (rectChart.bottom - point.y) * animPro);
} else if (line.animType == AnimType.SLOW_DRAW) {
if (i > lineList.size() * animPro)
break;
path.lineTo(point.x, point.y);
} else if (line.animType == AnimType.NONE) {
path.lineTo(point.x, point.y);
}
}
// canvas.drawCircle(prePoint.x+diffx, prePoint.y, 10.0f, paint);
// canvas.drawCircle(point.x-diffx, point.y, 10.0f, paint);
// canvas.drawCircle(point.x, point.y, 5.0f, paint);
}
prePoint = point;
}
canvas.drawPath(path, paint);
}
}
private ChartFocus focusInfo;
@Override
protected void onTouchMoved(PointF point) {
if(null==lineList || lineList.size()<=0)
return;
onFocus = (null != point);
if(null != point) {
//获取焦点对应的数据的索引
int index = (int) ((point.x - rectChart.left) * lineList.get(0).dataNumCount
/ (rectChart.right - rectChart.left));
//避免滑出
List<DataPoint> dataPoints = lineList.get(0).linePointList;
if(point.x > dataPoints.get(dataPoints.size()-1).point.x)
point.x = dataPoints.get(dataPoints.size()-1).point.x;
if(point.x < dataPoints.get(0).point.x)
point.x = dataPoints.get(0).point.x;
index = Math.max(0, Math.min(index, dataPoints.size() - 1));
focusInfo = new ChartFocus();
List<Object> objs = new ArrayList<>();
List<PointF> points = new ArrayList<>();
for(Line line : lineList){
objs.add(line.datas.get(index));
points.add(((DataPoint)line.linePointList.get(index)).point);
}
focusInfo.setFocusData(objs);
focusInfo.setPoints(points);
if(null!=onFocusChangeListener)
onFocusChangeListener.onfocus(focusInfo);
}
invalidate();
}
private OnFocusChangeListener onFocusChangeListener;
public void setOnFocusChangeListener(OnFocusChangeListener onFocusChangeListener) {
this.onFocusChangeListener = onFocusChangeListener;
}
public interface OnFocusChangeListener{
public void onfocus(ChartFocus focusInfo);
}
//设置焦点线颜色 及 粗细
private int focusLineColor = getResources().getColor(R.color.tc_chart_focus_line);
private int focusLineSize = DensityUtil.dip2px(getContext(), 0.8f);
/**绘制焦点*/
private void drawFocus(Canvas canvas){
if(!onFocus || null==focusInfo)
return;
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.FILL);
paint.setStrokeWidth(focusLineSize);
paint.setColor(focusLineColor);
//竖直线
canvas.drawLine(focusInfo.getPoints().get(0).x, rectChart.bottom,
focusInfo.getPoints().get(0).x, rectChart.top, paint);
// paintLabel.setTextSize(focusTextSize);
// paintLabel.setColor(focusTextColor);
// float focusTextLead = FontUtil.getFontLeading(paintLabel);
// float focusTextHeight = FontUtil.getFontHeight(paintLabel);
try {
for (PointF point : focusInfo.getPoints()) {
//水平参考线
canvas.drawLine(rectChart.left, point.y, rectChart.right, point.y, paint);
//中心点
canvas.drawCircle(point.x, point.y, 10.0f, paint);
}
}catch (Exception e){
e.printStackTrace();
}
}
private float animPro; //动画计算的占比数量
/**创建动画*/
@Override
protected ValueAnimator initAnim() {
if(lineList!=null && lineList.size()>0) {
ValueAnimator anim = ValueAnimator.ofObject(new AngleEvaluator(), 0f, 1f);
anim.setInterpolator(new AccelerateDecelerateInterpolator());
return anim;
}
return null;
}
/**动画值变化之后计算数据*/
@Override
protected void evaluatorData(ValueAnimator animation) {
animPro = (float)animation.getAnimatedValue();
}
}
@@ -0,0 +1,616 @@
package com.openxu.cview.xmstock20201030;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathEffect;
import android.graphics.PointF;
import android.graphics.RectF;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.util.Log;
import android.view.animation.AccelerateDecelerateInterpolator;
import com.openxu.hkchart.config.YAxisMark;
import com.openxu.cview.R;
import com.openxu.cview.chart.anim.AngleEvaluator;
import com.openxu.cview.xmstock.BaseChart;
import com.openxu.cview.xmstock.bean.DataPoint;
import com.openxu.utils.DensityUtil;
import com.openxu.utils.FontUtil;
import com.openxu.utils.LogUtil;
import com.openxu.utils.NumberFormatUtil;
import java.util.ArrayList;
import java.util.List;
/**
* autour : xiami
* date : 2020/11/13 14:26
* className : SyzsLinesChart
* version : 1.0
* description : 4. 收益走势图,贝塞尔曲线
*/
public class SyzsLinesChart extends BaseChart {
//设置数据 每个元素表示:时间、实际累计跌涨浮、历史累计跌涨幅 ["20201016","0.51","0.55"]
private List<List<String>> dataList;
private String[] lableXList;
//计算后的数据
private List<DataPoint> lableXPointList;
private List<List<DataPoint>> linePointList;
//y轴刻度设置
private YAxisMark yAxisMark;
/**可以设置的属性*/
//设置线条颜色
private int[] lineColor = new int[]{Color.parseColor("#fb4051"),
Color.parseColor("#ffcecb")};
//设置曲线粗细
private int lineSize = DensityUtil.dip2px(getContext(), 1.5f);
//设置坐标文字大小
private int textSizeX = (int)getResources().getDimension(R.dimen.ts_chart_xy);
//设置坐标文字颜色
private int textColorX = getResources().getColor(R.color.tc_chart_xy);
//设置X坐标字体与横轴的距离
private int textSpaceX = DensityUtil.dip2px(getContext(), 5);
//设置动画类型
private AnimType animType = AnimType.SLOW_DRAW;
//设置焦点线颜色 及 粗细
private int focusLineColor = Color.parseColor("#5E5E5E");
private int focusLineSize = DensityUtil.dip2px(getContext(), 1f);
//焦点面板矩形,只记录矩形宽高
private int foucsRectTextSpace = DensityUtil.dip2px(getContext(), 3);
private int foucsRectSpace = DensityUtil.dip2px(getContext(), 6);
private float dotRadius = DensityUtil.dip2px(getContext(), 3); //焦点面板小圆点半径
private float dotRadius1 = DensityUtil.dip2px(getContext(), 5); //最高收益小圆点半径
private float foucsRectWidth;
private float foucsRectHeight;
public enum AnimType{
// LEFT_TO_RIGHT, //动画从左往右
// BOTTOM_TO_TOP, //动画从下网上上升
SLOW_DRAW //动画缓慢绘制
}
public SyzsLinesChart(Context context) {
this(context, null);
}
public SyzsLinesChart(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SyzsLinesChart(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public void init(Context context, AttributeSet attrs, int defStyleAttr) {
dataList = new ArrayList<>();
touchEnable = true;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(widthSize, heightSize);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
evaluatorByData();
startDraw = false;
isLoading = false;
invalidate();
}
//20201028 --> 2020-10-28
private String formatDate(String date){
String str = date.substring(0, 4) + "-"+date.substring(4, 6)+"-"+date.substring(6);
Log.v(TAG, date+ "---->"+str);
return str;
}
/**
* 设置数据
*/
public void setData(List<List<String>> dataList){
if(null==dataList)
return;
this.dataList.clear();
this.dataList.addAll(dataList);
//[20200803,"1582.08","30.00"],
Log.v(TAG, "---->"+dataList.get(0));
//重组X轴刻度, 取其中5个日期
lableXList = new String[5];
int part = dataList.size()/4;
lableXList[0] = dataList.get(0).get(0).toString(); //第一天
// Log.v(TAG, 0+ "---->"+Integer.parseInt(dataList.get(0).get(0).toString()));
lableXList[1] = dataList.get(part).get(0).toString(); //
lableXList[2] = dataList.get(dataList.size()/2).get(0).toString(); //中间一天
lableXList[3] = dataList.get(part*3).get(0).toString(); //
lableXList[4] = dataList.get(dataList.size()-1).get(0).toString(); //最后一天
for(int i = 0; i<lableXList.length; i++){
lableXList[i] = formatDate(lableXList[i]);
}
yAxisMark = new YAxisMark.Builder(getContext())
.lableNum(5)
.textSize(textSizeX)
.textColor(textColorX)
.build();
if(getMeasuredWidth()>0) {
evaluatorByData();
startDraw = false;
isLoading = false;
invalidate();
}
}
/**设置数据后,计算相关值*/
private void evaluatorByData(){
if(dataList.size()<=0)
return;
/**③、计算Y刻度最大值和最小值以及幅度*/
//时间、实际累计跌涨浮、历史累计跌涨幅 ["20201016","0.51","0.55"]
calYLable(yAxisMark);
/**①、计算字体相关以及图表原点坐标*/
paintLabel.setTextSize(textSizeX);
float xlableHeight = FontUtil.getFontHeight(paintLabel);
float xlableLead = FontUtil.getFontLeading(paintLabel);
paintLabel.setTextSize(yAxisMark.textSize);
float ylableHeight = FontUtil.getFontHeight(paintLabel);
float ylableMaxLength = Math.max(FontUtil.getFontlength(paintLabel, (int)yAxisMark.cal_mark_max+"%"),
FontUtil.getFontlength(paintLabel, (int)yAxisMark.cal_mark_min+"%"));
float yFoucsLenght = FontUtil.getFontlength(paintLabel, "实际累计涨跌幅:-10.54%");
//图表主体矩形
rectChart = new RectF(getPaddingLeft() + yAxisMark.textSpace + ylableMaxLength,
getPaddingTop() + ylableMaxLength/2,
getMeasuredWidth()-getPaddingRight(),
getMeasuredHeight()-getPaddingBottom() - xlableHeight - textSpaceX);
//焦点面板
//2020-10-16
//实际累计涨跌幅:-10.54%
//历史累计涨跌幅:-10.54%
foucsRectWidth = dotRadius*2 + foucsRectTextSpace +yFoucsLenght + foucsRectSpace*2;
foucsRectHeight = xlableHeight + ylableHeight * 2 + foucsRectTextSpace*2 + foucsRectSpace*2;
/**②、计算X标签绘制坐标*/
float lableXSpace = 0;
for(String lableX : lableXList){
lableXSpace += FontUtil.getFontlength(paintLabel, lableX);
}
lableXSpace = (rectChart.right - rectChart.left - lableXSpace)/(lableXList.length -1);
lableXPointList = new ArrayList<>();
if(lableXSpace>0){
float left = rectChart.left;
for(int i = 0; i<lableXList.length; i++){
String lableX = lableXList[i];
lableXPointList.add(new DataPoint(lableX, 0, new PointF(
left,rectChart.bottom + textSpaceX + xlableLead)));
left += (FontUtil.getFontlength(paintLabel, lableXList[i])+lableXSpace);
}
}else{
//如果X轴标签字体过长的情况需要特殊处理
float oneWidth = (rectChart.right - rectChart.left)/lableXList.length;
for(int i = 0; i<lableXList.length; i++){
String lableX = lableXList[i];
float xLen = FontUtil.getFontlength(paintLabel, lableX);
lableXPointList.add(new DataPoint(lableX, 0, new PointF(
rectChart.left+i*oneWidth+(oneWidth-xLen)/2,rectChart.bottom + textSpaceX + xlableLead)));
}
}
/**④、计算点的坐标,如果有动画的情况下,边绘制边计算会耗费性能,所以先计算*/
linePointList = new ArrayList<>();
//两条线
linePointList.add(new ArrayList<>());
linePointList.add(new ArrayList<>());
float oneSpace = (rectChart.right - rectChart.left) / (dataList.size()-1);
for(int i = 0; i < dataList.size(); i++){
//时间、实际累计跌涨浮、历史累计跌涨幅 ["20201016","0.51","0.55"]
List<String> onePart = dataList.get(i);
try {
//第一条线的数据
if(onePart!=null && onePart.size()>=2&& onePart.get(1)!=null && onePart.get(1).length()>0) {
float valueY = Float.parseFloat(onePart.get(1));
PointF point = new PointF(rectChart.left + i * oneSpace,
rectChart.bottom - (rectChart.bottom - rectChart.top) / (yAxisMark.cal_mark_max - yAxisMark.cal_mark_min) * (valueY - yAxisMark.cal_mark_min));
linePointList.get(0).add(new DataPoint(onePart.get(0), valueY, point));
}
//第二条线的数据
if(onePart!=null && onePart.size()>=3&& onePart.get(2)!=null && onePart.get(2).length()>0) {
float valueY = Float.parseFloat(onePart.get(2));
// Log.w(TAG, i+"计算右侧y值"+valueY);
PointF point = new PointF(rectChart.left + i * oneSpace,
rectChart.bottom - (rectChart.bottom - rectChart.top) / (yAxisMark.cal_mark_max - yAxisMark.cal_mark_min) * (valueY - yAxisMark.cal_mark_min));
linePointList.get(1).add(new DataPoint(onePart.get(0), valueY, point));
}
}catch (Exception e){
e.printStackTrace();
}
}
}
private FocusData focusData;
@Override
protected void onTouchMoved(PointF point) {
if(null==dataList)
return;
onFocus = (null != point);
if(null != point && null!=dataList && dataList.size()>0) {
//避免滑出
// if(point.x > linePointList.get(0).get(dataList.size()-1).getPoint().x)
// point.x = linePointList.get(0).get(dataList.size()-1).getPoint().x;
// if(point.x < linePointList.get(0).get(0).getPoint().x)
// point.x = linePointList.get(0).get(0).getPoint().x;
point.x = Math.max(point.x, rectChart.left);
point.x = Math.min(point.x, rectChart.right);
//获取焦点对应的数据的索引
int index = (int) ((point.x - rectChart.left) * dataList.size() / (rectChart.right - rectChart.left));
LogUtil.e(getClass().getSimpleName(), "========焦点索引:"+index+" 数据总数:"+dataList.size()+" 线条数量:"+linePointList.size());
index = Math.max(0, Math.min(index, dataList.size() - 1));
focusData = new FocusData();
focusData.setData(dataList.get(index));
focusData.setPoints(new ArrayList<>());
if(index<linePointList.get(0).size()) {
focusData.getPoints().add(linePointList.get(0).get(index));
}else{
focusData.getPoints().add(new DataPoint(null, 0, null));
}
if(index<linePointList.get(1).size()) {
focusData.getPoints().add(linePointList.get(1).get(index));
}else{
focusData.getPoints().add(new DataPoint(null, 0, null));
}
if(null!=onFocusChangeListener)
onFocusChangeListener.onfocus(focusData);
}
invalidate();
}
private OnFocusChangeListener onFocusChangeListener;
public void setOnFocusChangeListener(OnFocusChangeListener onFocusChangeListener) {
this.onFocusChangeListener = onFocusChangeListener;
}
public interface OnFocusChangeListener{
public void onfocus(FocusData focusData);
}
/**绘制图表基本框架*/
@Override
public void drawDefult(Canvas canvas) {
if(null==linePointList || linePointList.size()<=0)
return;
paint.setStyle(Paint.Style.FILL);
paint.setStrokeWidth(lineWidth);
paint.setColor(defColor);
drawGrid(canvas);
drawXLable(canvas);
drawYLable(canvas);
}
/**绘制debug辅助线*/
@Override
public void drawDebug(Canvas canvas) {
super.drawDebug(canvas);
}
/**绘制图表*/
@Override
public void drawChart(Canvas canvas) {
if(null==linePointList || linePointList.size()<=0)
return;
drawDataPath(canvas);
drawFocus(canvas);
}
/**绘制X轴方向辅助网格*/
private float yMark0; //y轴0对应的y坐标
private void drawGrid(Canvas canvas){
float yMarkSpace = (rectChart.bottom - rectChart.top)/(yAxisMark.lableNum-1);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(lineWidth);
//竖直黑色实线
paint.setColor(Color.parseColor("#3d3d3d"));
canvas.drawLine(rectChart.left, rectChart.top, rectChart.left, rectChart.bottom, paint);
paintEffect.setStyle(Paint.Style.STROKE);
paintEffect.setStrokeWidth(lineWidth);
paintEffect.setColor(defColor);
// canvas.drawLine(rectChart.left, rectChart.top, rectChart.left, rectChart.bottom, paint);
// canvas.drawLine(rectChart.right, rectChart.top, rectChart.right, rectChart.bottom, paint);
PathEffect effects = new DashPathEffect(new float[]{15,6,15,6},0);
Path path = new Path();
for (int i = 0; i < yAxisMark.lableNum; i++) {
if(yAxisMark.cal_mark_min+i*yAxisMark.cal_mark == 0){
//实线
yMark0 = rectChart.bottom-yMarkSpace*i;
canvas.drawLine(rectChart.left, yMark0, rectChart.right, yMark0, paint);
}else{
path.reset();
path.moveTo(rectChart.left, rectChart.bottom-yMarkSpace*i);
path.lineTo(rectChart.right,rectChart.bottom-yMarkSpace*i);
paintEffect.setPathEffect(effects);
canvas.drawPath(path, paintEffect);
}
}
}
/**绘制X轴刻度*/
private void drawXLable(Canvas canvas){
paintLabel.setTextSize(textSizeX);
paintLabel.setColor(textColorX);
for(DataPoint lable : lableXPointList){
canvas.drawText(lable.getValueX(), lable.getPoint().x, lable.getPoint().y, paintLabel);
}
}
/**绘制Y轴刻度*/
private void drawYLable(Canvas canvas){
float yMarkSpace = (rectChart.bottom - rectChart.top)/(yAxisMark.lableNum-1);
paintLabel.setTextSize(yAxisMark.textSize);
paintLabel.setColor(yAxisMark.textColor);
float lableHeight = FontUtil.getFontHeight(paintLabel);
float lableLead = FontUtil.getFontLeading(paintLabel);
for (int i = 0; i < yAxisMark.lableNum; i++) {
String text = (int) (yAxisMark.cal_mark_min + i * yAxisMark.cal_mark) + "%";
canvas.drawText(text,
rectChart.left - yAxisMark.textSpace - FontUtil.getFontlength(paintLabel, text),
rectChart.bottom - yMarkSpace * i - lableHeight/2 + lableLead, paintLabel);
}
}
/**绘制曲线*/
private void drawDataPath(Canvas canvas) {
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setStrokeJoin(Paint.Join.ROUND);
paint.setStrokeWidth(lineSize);
PointF pointMax = null; //最高收益点
for(int j = 0; j < linePointList.size(); j++){
List<DataPoint> lineList = linePointList.get(j);
if(lineList==null || lineList.size()<=0) //有可能只有一条???
continue;
if(j == 0) {
Log.w(TAG, "当前绘制的是第1条实际跌涨幅,需要标注最高收益");
pointMax = new PointF(0, Integer.MAX_VALUE);
}
//一条一条的绘制
Path path = new Path();
PointF lastPoint = null;
float minYValue = 0;
for(int i = 0 ; i<lineList.size()-1; i++){
if(i>lineList.size()*animPro) //动画
break;
lastPoint = lineList.get(i+1).getPoint();
minYValue = Math.min(lastPoint.y,minYValue);
if(j == 0 && lastPoint.y<pointMax.y){
pointMax.x = lastPoint.x;
pointMax.y = lastPoint.y;
}
if(i == 0){ //二阶
path.moveTo(lineList.get(i).getPoint().x, lineList.get(i).getPoint().y);
float x = lineList.get(i).getPoint().x + (lineList.get(i+1).getPoint().x - lineList.get(i).getPoint().x)/2;
float y = lineList.get(i+1).getPoint().y;
path.quadTo(x,y,lastPoint.x,lastPoint.y);
}else if(i<lineList.size()-2){ //三阶
float x1 = lineList.get(i).getPoint().x + (lineList.get(i+1).getPoint().x - lineList.get(i).getPoint().x)/2;
float y1 = lineList.get(i).getPoint().y;
float x2 = lineList.get(i).getPoint().x + (lineList.get(i+1).getPoint().x - lineList.get(i).getPoint().x)/2;
float y2 = lineList.get(i+1).getPoint().y;
path.cubicTo(x1, y1, x2, y2, lastPoint.x,lastPoint.y);
}else if(i == lineList.size()-2){ //最后一个 二阶
float x = lineList.get(i).getPoint().x + (lineList.get(i+1).getPoint().x - lineList.get(i).getPoint().x)/2;
float y = lineList.get(i).getPoint().y;
path.quadTo(x,y,lastPoint.x,lastPoint.y);
}
}
paint.setColor(lineColor[j]);
canvas.drawPath(path, paint);
//绘制阴影
path.lineTo(lastPoint.x, yMark0);
path.close();
//线性渐变:前两个参数是渐变开始的点坐标,第三四个参数是渐变结束的点的坐标;渐变的颜色,渐变颜色的分布,模式
Shader mShader = new LinearGradient(rectChart.left, rectChart.top ,rectChart.left, rectChart.bottom,
new int[] {lineColor[j],Color.TRANSPARENT},null,Shader.TileMode.CLAMP);
paintEffect.setStyle(Paint.Style.FILL);
paintEffect.setShader(mShader);
canvas.drawPath(path, paintEffect);
paintEffect.setShader(null);
}
Log.w(TAG, "绘制最高收益:"+pointMax);
//绘制最高收益
if(pointMax!=null){
paintLabel.setTextSize(yAxisMark.textSize);
paintLabel.setColor(lineColor[0]);
paint.setStyle(Paint.Style.FILL);
paint.setColor(lineColor[0]);
canvas.drawCircle(pointMax.x , pointMax.y, dotRadius1, paint);
float textlenght = FontUtil.getFontlength(paintLabel, "最高收益");
float x = pointMax.x - textlenght;
x = Math.max(x, rectChart.left);
x = Math.min(x, rectChart.right - textlenght);
canvas.drawText("最高收益", x,
pointMax.y - dotRadius1 - foucsRectTextSpace - FontUtil.getFontHeight(paintLabel) + FontUtil.getFontLeading(paintLabel), paintLabel);
}
}
/**绘制焦点*/
private void drawFocus(Canvas canvas){
if(!onFocus || null==focusData)
return;
PointF point1 = focusData.getPoints().get(0).getPoint();
PointF point2 = focusData.getPoints().get(1).getPoint();
//绘制竖直虚线
PathEffect effects = new DashPathEffect(new float[]{15,10,15,10},0);
paintEffect.setStyle(Paint.Style.STROKE);
paintEffect.setStrokeWidth(focusLineSize);
paintEffect.setColor(focusLineColor);
paintEffect.setPathEffect(effects);
Path path = new Path();
path.moveTo(point1.x, rectChart.bottom);
path.lineTo(point1.x, rectChart.top);
canvas.drawPath(path , paintEffect);
//绘制焦点
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(lineSize);
paint.setColor(lineColor[0]);
canvas.drawCircle(point1.x, point1.y, dotRadius, paint);
paint.setColor(lineColor[1]);
canvas.drawCircle(point2.x, point2.y, dotRadius, paint);
//面板
boolean showLeft = point1.x-rectChart.left > (rectChart.right - rectChart.left)/2;
RectF rect = new RectF(
showLeft?point1.x - foucsRectWidth - 30:point1.x + 30,
rectChart.top + (rectChart.bottom - rectChart.top)/2 - foucsRectHeight/2,
showLeft? point1.x - 30 : point1.x + foucsRectWidth + 30,
rectChart.top + (rectChart.bottom - rectChart.top)/2 + foucsRectHeight/2
);
paint.setStyle(Paint.Style.FILL);
paint.setColor(Color.WHITE);
paint.setAlpha(230);
canvas.drawRect(rect , paint);
//面板中的文字
//2020-10-16
//实际累计涨跌幅:+10.54%
//历史累计涨跌幅:-10.54%
paintLabel.setTextSize(textSizeX);
paintLabel.setColor(textColorX);
float lableHeight = FontUtil.getFontHeight(paintLabel);
float lableLead = FontUtil.getFontLeading(paintLabel);
float top = rect.top + foucsRectSpace;
float left = rect.left + foucsRectSpace;
canvas.drawText(formatDate(focusData.getData().get(0)), left, top + lableLead, paintLabel);
top += (lableHeight+foucsRectTextSpace);
paintLabel.setTextSize(yAxisMark.textSize);
paintLabel.setColor(lineColor[0]);
paint.setColor(lineColor[0]);
lableHeight = FontUtil.getFontHeight(paintLabel);
lableLead = FontUtil.getFontLeading(paintLabel);
canvas.drawCircle(left + dotRadius, top + lableHeight/2, dotRadius, paint);
if(focusData.getPoints().get(0).getValueX() == null){
canvas.drawText("实际累计涨跌幅:",
left+dotRadius*2+foucsRectTextSpace, top + lableLead, paintLabel);
}else{
String dzf = NumberFormatUtil.formattedDecimal(focusData.getPoints().get(0).getValueY());
canvas.drawText("实际累计涨跌幅:"+(focusData.getPoints().get(0).getValueY()>0?"+":"")+dzf+"%",
left+dotRadius*2+foucsRectTextSpace, top + lableLead, paintLabel);
}
top += (lableHeight+foucsRectTextSpace);
paintLabel.setTextSize(yAxisMark.textSize);
paintLabel.setColor(lineColor[1]);
paint.setColor(lineColor[1]);
lableLead = FontUtil.getFontLeading(paintLabel);
canvas.drawCircle(left + dotRadius, top + lableHeight/2, dotRadius, paint);
if(focusData.getPoints().get(1).getValueX() == null){
canvas.drawText("历史累计涨跌幅:",
left+dotRadius*2+foucsRectTextSpace, top + lableLead, paintLabel);
}else{
String dzf = NumberFormatUtil.formattedDecimal(focusData.getPoints().get(1).getValueY());
canvas.drawText("历史累计涨跌幅:"+(focusData.getPoints().get(1).getValueY()>0?"+":"")+dzf+"%",
left+dotRadius*2+foucsRectTextSpace, top + lableLead, paintLabel);
}
}
private float animPro; //动画计算的占比数量
/**创建动画*/
@Override
protected ValueAnimator initAnim() {
if(dataList.size()>0) {
ValueAnimator anim = ValueAnimator.ofObject(new AngleEvaluator(), 0f, 1f);
anim.setInterpolator(new AccelerateDecelerateInterpolator());
return anim;
}
return null;
}
/**动画值变化之后计算数据*/
@Override
protected void evaluatorData(ValueAnimator animation) {
animPro = (float)animation.getAnimatedValue();
}
/**焦点数据*/
public static class FocusData {
private List<DataPoint> points;
private List<String> data;
public List<DataPoint> getPoints() {
return points;
}
public void setPoints(List<DataPoint> points) {
this.points = points;
}
public List<String> getData() {
return data;
}
public void setData(List<String> data) {
this.data = data;
}
}
/**
* 根据传入的Y轴信息,从数据中获取最小最大值,并获取到Y刻度点值
*/
private void calYLable(YAxisMark axisMark){
//时间、实际累计跌涨浮、历史累计跌涨幅 ["20201016","0.51","0.55"]
axisMark.cal_mark_max = Float.MIN_VALUE; //Y轴刻度最大值
axisMark.cal_mark_min = Float.MAX_VALUE; //Y轴刻度最小值
for(List<String> data : dataList){
try {
if(data!=null && data.size()>=2&& data.get(1)!=null && data.get(1).length()>0) {
axisMark.cal_mark_max = Math.max(axisMark.cal_mark_max, Float.parseFloat(data.get(1)));
axisMark.cal_mark_min = Math.min(axisMark.cal_mark_min, Float.parseFloat(data.get(1)));
}
if(data!=null && data.size()>=3 && data.get(2)!=null && data.get(2).length()>0) {
axisMark.cal_mark_max = Math.max(axisMark.cal_mark_max, Float.parseFloat(data.get(2)));
axisMark.cal_mark_min = Math.min(axisMark.cal_mark_min, Float.parseFloat(data.get(2)));
}
}catch (Exception e){
}
}
LogUtil.i(TAG, "Y轴真实axisMark.cal_mark_min="+axisMark.cal_mark_min+" axisMark.cal_mark_max="+axisMark.cal_mark_max);
//保证有一个0刻度
if(axisMark.cal_mark_max > 0 && axisMark.cal_mark_min>0){
axisMark.cal_mark_min = 0;
axisMark.cal_mark = (int)(axisMark.cal_mark_max/axisMark.lableNum);
if(axisMark.cal_mark_max%axisMark.lableNum!=0)
axisMark.lableNum+=1;
}
if(axisMark.cal_mark_max < 0 && axisMark.cal_mark_min<0){
axisMark.cal_mark_max = 0;
axisMark.cal_mark = (int)(-axisMark.cal_mark_min/axisMark.lableNum);
if(axisMark.cal_mark_min%axisMark.lableNum!=0)
axisMark.lableNum+=1;
}
if(axisMark.cal_mark_max > 0 && axisMark.cal_mark_min<0){
axisMark.cal_mark = (int)((axisMark.cal_mark_max-axisMark.cal_mark_min)/axisMark.lableNum);
axisMark.lableNum = 1; //从0刻度开始
int a = 0;
while(true){
a += axisMark.cal_mark;
axisMark.lableNum ++;
if(a>=axisMark.cal_mark_max){
axisMark.cal_mark_max = a;
break;
}
}
a = 0;
while(true){
a -= axisMark.cal_mark;
axisMark.lableNum ++;
if(a<=axisMark.cal_mark_min){
axisMark.cal_mark_min = a;
break;
}
}
}
LogUtil.i(TAG, "Y轴axisMark.cal_mark_min="+axisMark.cal_mark_min+" axisMark.cal_mark_max="+axisMark.cal_mark_max+" axisMark.cal_mark="+axisMark.cal_mark+" 总共"+axisMark.lableNum+"条横线");
}
}
@@ -0,0 +1,535 @@
package com.openxu.cview.xmstock20201030;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathEffect;
import android.graphics.PointF;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.animation.AccelerateDecelerateInterpolator;
import com.openxu.cview.R;
import com.openxu.cview.chart.anim.AngleEvaluator;
import com.openxu.cview.xmstock.BaseChart;
import com.openxu.cview.xmstock.bean.DataPoint;
import com.openxu.cview.xmstock.bean.FocusInfo;
import com.openxu.utils.DensityUtil;
import com.openxu.utils.FontUtil;
import com.openxu.utils.LogUtil;
import com.openxu.utils.NumberFormatUtil;
import java.util.ArrayList;
import java.util.List;
/**
* autour : xiami
* date : 2020/11/13 14:26
* className : QsrdLinesChart
* version : 1.0
* description : 券商热点走势对比图
*/
public class aaLinesChart extends BaseChart {
//设置数据
private List<List<String>> dataList;
private String[] lableXList = new String[]{"09:30", "10:30", "11:30/13:00", "14:00", "15:00"};
private int dataNumCount = 60 * 4; //60*4 一分钟一个,四小时
//计算后的数据
private List<DataPoint> lableXPointList;
private List<ArrayList<DataPoint>> linePointList;
/**可以设置的属性*/
//设置Y轴刻度数量
private int YMARK_NUM = 5;
private YMARK_TYPE yMarkType = YMARK_TYPE.INTEGER;
//设置线条颜色
private int[] lineColor = new int[]{Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW};
//设置曲线粗细
private int lineSize = DensityUtil.dip2px(getContext(), 1.5f);
//曲线数量
private int LINE_NUM = 0;
//设置坐标文字大小
private int textSize = (int)getResources().getDimension(R.dimen.ts_chart_xy);
//设置坐标文字颜色
private int textColor = getResources().getColor(R.color.tc_chart_xy);
//设置X坐标字体与横轴的距离
private int textSpaceX = DensityUtil.dip2px(getContext(), 5);
//设置Y坐标字体与横轴的距离
private int textSpaceY = DensityUtil.dip2px(getContext(), 3);
//设置动画类型
private AnimType animType = AnimType.SLOW_DRAW;
//设置焦点线颜色 及 粗细
private int focusLineColor = getResources().getColor(R.color.tc_chart_focus_line);
private int focusLineSize = DensityUtil.dip2px(getContext(), 0.8f);
public enum YMARK_TYPE{
PERCENTAGE, /*百分比*/
INTEGER
}
public enum AnimType{
LEFT_TO_RIGHT, //动画从左往右
BOTTOM_TO_TOP, //动画从下网上上升
SLOW_DRAW //动画缓慢绘制
}
/**需要计算相关值*/
private float lableLead, lableHeight;
/*字体绘制相关*/
private float YMARK = 1; //Y轴刻度间隔
private float YMARK_MAX = Float.MIN_VALUE; //Y轴刻度最大值
private float YMARK_MIN = Float.MAX_VALUE; //Y轴刻度最小值
public aaLinesChart(Context context) {
this(context, null);
}
public aaLinesChart(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public aaLinesChart(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public void init(Context context, AttributeSet attrs, int defStyleAttr) {
dataList = new ArrayList<>();
touchEnable = true;
}
public void setYMARK_NUM(int YMARK_NUM) {
this.YMARK_NUM = YMARK_NUM;
}
public void setyMarkType(YMARK_TYPE yMarkType) {
this.yMarkType = yMarkType;
}
/***********************************设置属性set方法**********************************/
/**设置数据的数量,默认是60*4*/
public void setDataNumCount(int dataNumCount) {
this.dataNumCount = dataNumCount;
}
public void setLineColor(int[] lineColor) {
this.lineColor = lineColor;
}
public void setAnimType(AnimType animType) {
this.animType = animType;
}
public void setLINE_NUM(int LINE_NUM) {
this.LINE_NUM = LINE_NUM;
}
/***********************************设置属性set方法over**********************************/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(widthSize, heightSize);
evaluatorByData();
invalidate();
}
/***************************************************/
/***************************************************/
/**
* 设置数据
* @param dataList 折线数据
* @param lableXList X轴显示的刻度,默认是9:30-15:00(传null)
* 如果需要显示其他的数据,需要重新设置
*/
public void setData(List<List<String>> dataList, String[] lableXList){
if(null==dataList)
return;
this.dataList.clear();
this.dataList.addAll(dataList);
if(null!=lableXList && lableXList.length>0)
this.lableXList = lableXList;
if(getMeasuredWidth()>0) {
evaluatorByData();
startDraw = false;
invalidate();
}
}
/**设置数据后,计算相关值*/
private void evaluatorByData(){
if(dataList.size()<=0)
return;
dataNumCount = dataList.size();
LogUtil.w(TAG, "总共"+dataNumCount+"条数据");
/**①、计算字体相关以及图表原点坐标*/
paintLabel.setTextSize(textSize);
lableHeight = FontUtil.getFontHeight(paintLabel);
lableLead = FontUtil.getFontLeading(paintLabel);
//图表主体矩形
rectChart = new RectF(getPaddingLeft(),getPaddingTop() + lableHeight + textSpaceY,
getMeasuredWidth()-getPaddingRight(),
getMeasuredHeight()-getPaddingBottom() - lableHeight - textSpaceX);
/**②、计算X标签绘制坐标*/
float lableXSpace = 0;
for(String lableX : lableXList){
lableXSpace += FontUtil.getFontlength(paintLabel, lableX);
}
lableXSpace = (rectChart.right - rectChart.left - lableXSpace)/(lableXList.length -1);
lableXPointList = new ArrayList<>();
if(lableXSpace>0){
float left = rectChart.left;
for(int i = 0; i<lableXList.length; i++){
String lableX = lableXList[i];
lableXPointList.add(new DataPoint(lableX, 0, new PointF(
left,rectChart.bottom + textSpaceX + lableLead)));
left += (FontUtil.getFontlength(paintLabel, lableXList[i])+lableXSpace);
}
}else{
//如果X轴标签字体过长的情况需要特殊处理
float oneWidth = (rectChart.right - rectChart.left)/lableXList.length;
for(int i = 0; i<lableXList.length; i++){
String lableX = lableXList[i];
float xLen = FontUtil.getFontlength(paintLabel, lableX);
lableXPointList.add(new DataPoint(lableX, 0, new PointF(
rectChart.left+i*oneWidth+(oneWidth-xLen)/2,rectChart.bottom + textSpaceX + lableLead)));
}
}
/**③、计算Y刻度最大值和最小值以及幅度*/
int lineNum = LINE_NUM==0?(dataList.get(0).size()-1):LINE_NUM;
YMARK_MAX = Float.MIN_VALUE; //Y轴刻度最大值
YMARK_MIN = Float.MAX_VALUE; //Y轴刻度最小值
for(List<String> list : dataList){
for(int i = 1; i< lineNum+1 ; i++){
String str = list.get(i);
try {
if(str.contains("%")){
str = str.substring(0,str.indexOf("%"));//百分数不能直接强转
YMARK = Float.parseFloat(str) /100.0f;
}else{
YMARK = Float.parseFloat(str);
}
// LogUtil.d(TAG, str+" Y轴 被识别为Float = "+YMARK);
if(YMARK>YMARK_MAX)
YMARK_MAX = YMARK;
if(YMARK<YMARK_MIN)
YMARK_MIN = YMARK;
}catch (Exception e){
}
}
}
LogUtil.w(TAG, "Y轴真实YMARK_MIN="+YMARK_MIN+" YMARK_MAX="+YMARK_MAX);
if(YMARK_MAX>0)
YMARK_MAX *= 1.1f;
else
YMARK_MAX /= 1.1f;
if(YMARK_MIN>0)
YMARK_MIN /= 1.1f;
else
YMARK_MIN *= 1.1f;
if(YMARK_MIN>0)
YMARK_MIN = 0;
YMARK = (YMARK_MAX-YMARK_MIN)/(YMARK_NUM - 1);
if(yMarkType == YMARK_TYPE.INTEGER){
YMARK = (int)YMARK + 1;
YMARK_MIN = (int)YMARK_MIN;
YMARK_MAX = YMARK_MIN+YMARK*(YMARK_NUM-1);
}else if(yMarkType == YMARK_TYPE.PERCENTAGE){
}
LogUtil.i(TAG, "Y轴YMARK_MIN="+YMARK_MIN+" YMARK_MAX="+YMARK_MAX+" YMARK="+YMARK);
/**④、计算点的坐标,如果有动画的情况下,边绘制边计算会耗费性能,所以先计算*/
//创建集合,用于存放每条线上每个点的坐标数据
List<String> group = dataList.get(0);
linePointList = new ArrayList<>();
for(int i = 0; i<group.size()-1; i++)
linePointList.add(new ArrayList<DataPoint>());
float oneSpace = (rectChart.right - rectChart.left) / (dataNumCount-1);
List<DataPoint> lastDataGroup = null; //最后一组数据
for(int i = 0; i < dataList.size(); i++){
//一组数据,包含每条线的一个item数据
group = dataList.get(i);
for(int j = 1; j < group.size(); j++){
try {
float valueY;
if(group.get(j).contains("%"))
valueY = Float.parseFloat(group.get(j).substring(0,group.get(j).indexOf("%"))) /100.0f;
else
valueY = Float.parseFloat(group.get(j));
PointF point = new PointF();
//只有需要绘制的线才计算坐标
if(j<lineNum+1){
point.x = rectChart.left + i * oneSpace;
//根据最高价和最低价,计算当前数据在图表上Y轴的坐标
point.y = rectChart.bottom -
(rectChart.bottom-rectChart.top)/(YMARK_MAX - YMARK_MIN) * (valueY-YMARK_MIN);
}
linePointList.get(j-1).add(new DataPoint(group.get(0), valueY, point));
// LogUtil.w(TAG, "绘制"+j+"曲线"+(int)point.x+", "+(int)point.y);
/**默认最后一组数据为显示的lable数据*/
if(null!=onFocusChangeListener && i==dataList.size()-1){
if(null==focusInfo) {
focusInfo = new FocusInfo();
lastDataGroup = new ArrayList<>();
focusInfo.setPoint(point);
focusInfo.setFocusData(lastDataGroup);
}
lastDataGroup.add(new DataPoint(group.get(0), valueY, point));
}
}catch (Exception e){
e.printStackTrace();
}
}
}
if(null!=onFocusChangeListener)
onFocusChangeListener.onfocus(focusInfo);
}
private FocusInfo focusInfo;
@Override
protected void onTouchMoved(PointF point) {
if(null==dataList)
return;
onFocus = (null != point);
if(null != point && null!=dataList && dataList.size()>0) {
//获取焦点对应的数据的索引
int index = (int) ((point.x - rectChart.left) * dataNumCount / (rectChart.right - rectChart.left));
//避免滑出
List<DataPoint> dataPoints = linePointList.get(0);
if(point.x > dataPoints.get(dataPoints.size()-1).getPoint().x)
point.x = dataPoints.get(dataPoints.size()-1).getPoint().x;
if(point.x < dataPoints.get(0).getPoint().x)
point.x = dataPoints.get(0).getPoint().x;
index = Math.max(0, Math.min(index, dataPoints.size() - 1));
focusInfo = new FocusInfo();
focusInfo.setPoint(point);
List<DataPoint> objs = new ArrayList<>();
for(List<DataPoint> points : linePointList){
objs.add(points.get(index));
}
focusInfo.setFocusData(objs);
if(null!=onFocusChangeListener)
onFocusChangeListener.onfocus(focusInfo);
}
invalidate();
}
private OnFocusChangeListener onFocusChangeListener;
public void setOnFocusChangeListener(OnFocusChangeListener onFocusChangeListener) {
this.onFocusChangeListener = onFocusChangeListener;
}
public interface OnFocusChangeListener{
public void onfocus(FocusInfo focusInfo);
}
/**绘制图表基本框架*/
@Override
public void drawDefult(Canvas canvas) {
if(null==linePointList || linePointList.size()<=0)
return;
paint.setStyle(Paint.Style.FILL);
paint.setStrokeWidth(lineWidth);
paint.setColor(defColor);
drawGrid(canvas);
drawXLable(canvas);
drawYLable(canvas);
}
/**绘制debug辅助线*/
@Override
public void drawDebug(Canvas canvas) {
super.drawDebug(canvas);
}
/**绘制图表*/
@Override
public void drawChart(Canvas canvas) {
if(null==linePointList || linePointList.size()<=0)
return;
drawDataPath(canvas);
drawFocus(canvas);
}
/**绘制X轴方向辅助网格*/
private void drawGrid(Canvas canvas){
float yMarkSpace = (rectChart.bottom - rectChart.top)/(YMARK_NUM-1);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(lineWidth);
paint.setColor(defColor);
paintEffect.setStyle(Paint.Style.STROKE);
paintEffect.setStrokeWidth(lineWidth);
paintEffect.setColor(defColor);
canvas.drawLine(rectChart.left, rectChart.top, rectChart.left, rectChart.bottom, paint);
canvas.drawLine(rectChart.right, rectChart.top, rectChart.right, rectChart.bottom, paint);
for (int i = 0; i < YMARK_NUM; i++) {
if(i==0||i ==YMARK_NUM-1) { //实线
canvas.drawLine(rectChart.left, rectChart.bottom-yMarkSpace*i,
rectChart.right, rectChart.bottom-yMarkSpace*i, paint);
} else { //虚线
Path path = new Path();
path.moveTo(rectChart.left, rectChart.bottom-yMarkSpace*i);
path.lineTo(rectChart.right,rectChart.bottom-yMarkSpace*i);
PathEffect effects = new DashPathEffect(new float[]{15,8,15,8},0);
paintEffect.setPathEffect(effects);
canvas.drawPath(path, paintEffect);
}
}
}
/**绘制X轴刻度*/
private void drawXLable(Canvas canvas){
paintLabel.setTextSize(textSize);
paintLabel.setColor(textColor);
for(DataPoint lable : lableXPointList){
canvas.drawText(lable.getValueX(), lable.getPoint().x, lable.getPoint().y, paintLabel);
}
}
/**绘制Y轴刻度*/
private void drawYLable(Canvas canvas){
float yMarkSpace = (rectChart.bottom - rectChart.top)/(YMARK_NUM-1);
paintLabel.setTextSize(textSize);
paintLabel.setColor(textColor);
for (int i = 0; i < YMARK_NUM; i++) {
if(yMarkType == YMARK_TYPE.INTEGER) {
canvas.drawText((int) (YMARK_MIN + i * YMARK) + "", rectChart.left,
rectChart.bottom - yMarkSpace * i - lableHeight - textSpaceY + lableLead, paintLabel);
}else if(yMarkType == YMARK_TYPE.PERCENTAGE){
canvas.drawText(NumberFormatUtil.formattedDecimalToPercentage((YMARK_MIN + i * YMARK), 2), rectChart.left,
rectChart.bottom - yMarkSpace * i - lableHeight - textSpaceY + lableLead, paintLabel);
}
}
}
/**绘制曲线*/
private void drawDataPath(Canvas canvas) {
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setStrokeJoin(Paint.Join.ROUND);
paint.setStrokeWidth(lineSize);
int lineNum = LINE_NUM==0?(dataList.get(0).size()-1):LINE_NUM;
for(int j = 0; j < lineNum; j++){
// for(int j = 0; j < linePointList.size(); j++){
List<DataPoint> lineList = linePointList.get(j);
paint.setColor(lineColor[j]);
//一条一条的绘制
Path path = new Path();
PointF lastPoint = null;
for(int i = 0 ; i<lineList.size(); i++){
DataPoint dataPoint = lineList.get(i);
if(i == 0){
if(animType == AnimType.LEFT_TO_RIGHT){
path.moveTo(rectChart.left+(dataPoint.getPoint().x-rectChart.left)*animPro, dataPoint.getPoint().y);
}else if(animType == AnimType.BOTTOM_TO_TOP){
path.moveTo(dataPoint.getPoint().x, rectChart.bottom-(rectChart.bottom- dataPoint.getPoint().y)* animPro);
}else{
path.moveTo(dataPoint.getPoint().x, dataPoint.getPoint().y);
}
}else{
//quadTo:二阶贝塞尔曲线连接前后两点,这样使得曲线更加平滑
if(animType == AnimType.LEFT_TO_RIGHT){
path.quadTo(rectChart.left+(lastPoint.x-rectChart.left)*animPro, lastPoint.y,
rectChart.left+(dataPoint.getPoint().x-rectChart.left)*animPro , dataPoint.getPoint().y);
}else if(animType == AnimType.BOTTOM_TO_TOP){
path.quadTo(lastPoint.x, rectChart.bottom-(rectChart.bottom-lastPoint.y)* animPro,
dataPoint.getPoint().x, rectChart.bottom-(rectChart.bottom- dataPoint.getPoint().y)* animPro);
}else if(animType == AnimType.SLOW_DRAW){
if(i>lineList.size()*animPro)
break;
path.quadTo(lastPoint.x, lastPoint.y, dataPoint.getPoint().x, dataPoint.getPoint().y);
}else{
path.quadTo(lastPoint.x, lastPoint.y, dataPoint.getPoint().x, dataPoint.getPoint().y);
}
}
lastPoint = dataPoint.getPoint();
}
canvas.drawPath(path, paint);
}
}
/**绘制焦点*/
private void drawFocus(Canvas canvas){
if(!onFocus || null==focusInfo)
return;
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.FILL);
paint.setStrokeWidth(focusLineSize);
paint.setColor(focusLineColor);
//竖直线
canvas.drawLine(focusInfo.getPoint().x, rectChart.bottom, focusInfo.getPoint().x, rectChart.top, paint);
/*
paintLabel.setTextSize(focusTextSize);
paintLabel.setColor(focusTextColor);
float focusTextLead = FontUtil.getFontLeading(paintLabel);
float focusTextHeight = FontUtil.getFontHeight(paintLabel);
try {
for (Object obj : focusInfo.getObjs()) {
DataPoint dataPoint = (DataPoint)obj;
//水平参考线
canvas.drawLine(rectChart.left, dataPoint.getPoint().y, rectChart.right, dataPoint.getPoint().y, paint);
//中心点
canvas.drawCircle(dataPoint.getPoint().x, dataPoint.getPoint().y, 2.0f, paint);
//绘制刻度值
String str = "("+ dataPoint.getValueX()+",";
if(yMarkType == YMARK_TYPE.INTEGER) {
str += dataPoint.getValueY()+")";
}else if(yMarkType == YMARK_TYPE.PERCENTAGE){
str += formattedDecimalToPercentage(dataPoint.getValueY())+")";
}
float textLength = FontUtil.getFontlength(paintLabel,str);
if((rectChart.right- dataPoint.getPoint().x)>textLength){
canvas.drawText(str, dataPoint.getPoint().x + 10,
dataPoint.getPoint().y-focusTextHeight+focusTextLead - 10, paintLabel);
}else{
canvas.drawText(str, dataPoint.getPoint().x - 10 - textLength,
dataPoint.getPoint().y-focusTextHeight+focusTextLead - 10, paintLabel);
}
}
}catch (Exception e){
e.printStackTrace();
}
*/
}
private float animPro; //动画计算的占比数量
/**创建动画*/
@Override
protected ValueAnimator initAnim() {
if(dataList.size()>0) {
ValueAnimator anim = ValueAnimator.ofObject(new AngleEvaluator(), 0f, 1f);
anim.setInterpolator(new AccelerateDecelerateInterpolator());
return anim;
}
return null;
}
/**动画值变化之后计算数据*/
@Override
protected void evaluatorData(ValueAnimator animation) {
animPro = (float)animation.getAnimatedValue();
}
}
@@ -0,0 +1,40 @@
package com.openxu.cview.xmstock20201030.bean;
/**
* autour : xiami
* date : 2020/11/13 14:59
* className : CalandarList
* version : 1.0
* description : 日历数据
*/
public class CalandarList {
private int code;
private String err_info;
private CalendarListData data;
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public CalendarListData getData() {
return data;
}
public void setData(CalendarListData data) {
this.data = data;
}
public String getErr_info() {
return err_info;
}
public void setErr_info(String err_info) {
this.err_info = err_info;
}
}
@@ -0,0 +1,49 @@
package com.openxu.cview.xmstock20201030.bean;
import java.util.List;
/**
* autour : xiami
* date : 2020/11/13 14:59
* className : HotDetailData
* version : 1.0
* description : 日历数据
*/
public class CalendarData {
private String date;
private String week;
private List<CalendarDataStock> stock_list;
@Override
public String toString() {
return "CalendarData{" +
"date='" + date + '\'' +
", week='" + week + '\'' +
'}';
}
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
public String getWeek() {
return week;
}
public void setWeek(String week) {
this.week = week;
}
public List<CalendarDataStock> getStock_list() {
return stock_list;
}
public void setStock_list(List<CalendarDataStock> stock_list) {
this.stock_list = stock_list;
}
}
@@ -0,0 +1,134 @@
package com.openxu.cview.xmstock20201030.bean;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.ArrayList;
import java.util.List;
/**
* autour : xiami
* date : 2020/11/13 14:59
* className : HotDetailData
* version : 1.0
* description : 日历数据
*/
public class CalendarDataStock implements Parcelable {
private String name;
private String code;
private String symbol;
private int flag;
private String chg;
private String count;
private String max_real_chg;
private List<List<String>> trend_line;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getSymbol() {
return symbol;
}
public void setSymbol(String symbol) {
this.symbol = symbol;
}
public int getFlag() {
return flag;
}
public void setFlag(int flag) {
this.flag = flag;
}
public String getChg() {
return chg;
}
public void setChg(String chg) {
this.chg = chg;
}
public String getCount() {
return count;
}
public void setCount(String count) {
this.count = count;
}
public String getMax_real_chg() {
return max_real_chg;
}
public void setMax_real_chg(String max_real_chg) {
this.max_real_chg = max_real_chg;
}
public List<List<String>> getTrend_line() {
return trend_line;
}
public void setTrend_line(List<List<String>> trend_line) {
this.trend_line = trend_line;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(this.name);
dest.writeString(this.code);
dest.writeString(this.symbol);
dest.writeInt(this.flag);
dest.writeString(this.chg);
dest.writeString(this.count);
dest.writeString(this.max_real_chg);
dest.writeList(this.trend_line);
}
public CalendarDataStock() {
}
protected CalendarDataStock(Parcel in) {
this.name = in.readString();
this.code = in.readString();
this.symbol = in.readString();
this.flag = in.readInt();
this.chg = in.readString();
this.count = in.readString();
this.max_real_chg = in.readString();
this.trend_line = new ArrayList<List<String>>();
in.readList(this.trend_line, List.class.getClassLoader());
}
public static final Parcelable.Creator<CalendarDataStock> CREATOR = new Parcelable.Creator<CalendarDataStock>() {
@Override
public CalendarDataStock createFromParcel(Parcel source) {
return new CalendarDataStock(source);
}
@Override
public CalendarDataStock[] newArray(int size) {
return new CalendarDataStock[size];
}
};
}
@@ -0,0 +1,97 @@
package com.openxu.cview.xmstock20201030.bean;
import java.util.List;
/**
* autour : xiami
* date : 2020/11/13 14:59
* className : HotDetailData
* version : 1.0
* description : 日历数据
*/
public class CalendarDetail {
private String id;
private String date;
private String week;
private String title;
private String importance;
private List<String> relative_sectors;
private String start_date;
private String end_date;
private List<CalendarDataStock> stock_list;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
public String getWeek() {
return week;
}
public void setWeek(String week) {
this.week = week;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getImportance() {
return importance;
}
public void setImportance(String importance) {
this.importance = importance;
}
public List<String> getRelative_sectors() {
return relative_sectors;
}
public void setRelative_sectors(List<String> relative_sectors) {
this.relative_sectors = relative_sectors;
}
public String getStart_date() {
return start_date;
}
public void setStart_date(String start_date) {
this.start_date = start_date;
}
public String getEnd_date() {
return end_date;
}
public void setEnd_date(String end_date) {
this.end_date = end_date;
}
public List<CalendarDataStock> getStock_list() {
return stock_list;
}
public void setStock_list(List<CalendarDataStock> stock_list) {
this.stock_list = stock_list;
}
}
@@ -0,0 +1,32 @@
package com.openxu.cview.xmstock20201030.bean;
import java.util.List;
/**
* autour : xiami
* date : 2020/11/13 14:59
* className : HotDetailData
* version : 1.0
* description : 日历数据
*/
public class CalendarListData {
private List<CalendarData> calendar;
private List<CalendarDetail> calendar_detail;
public List<CalendarData> getCalendar() {
return calendar;
}
public void setCalendar(List<CalendarData> calendar) {
this.calendar = calendar;
}
public List<CalendarDetail> getCalendar_detail() {
return calendar_detail;
}
public void setCalendar_detail(List<CalendarDetail> calendar_detail) {
this.calendar_detail = calendar_detail;
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,18 @@
package com.openxu.cview.xmstock20201030.bean;
/**
* Author: openXu
* Time: 2020/11/4 12:04
* class: DayFinish
* Description:
*/
public class DayFinish {
public int day;
public int all;
public int finish;
public DayFinish(int day, int finish, int all) {
this.day = day;
this.all = all;
this.finish = finish;
}
}
@@ -0,0 +1,77 @@
package com.openxu.cview.xmstock20201030.bean;
import java.util.List;
/**
* autour : xiami
* date : 2020/11/13 14:59
* className : TopDetailStock
* version : 1.0
* description : 券商热点与走势对比
*/
public class DetailStock {
private String name;
private String code;
private String symbol;
private String remark;
private String trade;
private String updownrate;
private String circ_mv;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getSymbol() {
return symbol;
}
public void setSymbol(String symbol) {
this.symbol = symbol;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
public String getTrade() {
return trade;
}
public void setTrade(String trade) {
this.trade = trade;
}
public String getUpdownrate() {
return updownrate;
}
public void setUpdownrate(String updownrate) {
this.updownrate = updownrate;
}
public String getCirc_mv() {
return circ_mv;
}
public void setCirc_mv(String circ_mv) {
this.circ_mv = circ_mv;
}
}
@@ -0,0 +1,40 @@
package com.openxu.cview.xmstock20201030.bean;
/**
* autour : xiami
* date : 2020/11/13 14:59
* className : HotDetail
* version : 1.0
* description : 概念走势
*/
public class HotDetail {
private int code;
private String err_info;
private HotDetailData data;
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public HotDetailData getData() {
return data;
}
public void setData(HotDetailData data) {
this.data = data;
}
public String getErr_info() {
return err_info;
}
public void setErr_info(String err_info) {
this.err_info = err_info;
}
}
@@ -0,0 +1,97 @@
package com.openxu.cview.xmstock20201030.bean;
import java.util.List;
/**
* autour : xiami
* date : 2020/11/13 14:59
* className : HotDetailData
* version : 1.0
* description : 概念走势
*/
public class HotDetailData {
private String concept_id; //529683333575
private String concept_name;
private String intro;
private String pub_time;
private List<HotDetailNew> news;
//实时行情走势图,每个元素字段分别是:时间、现价、均价 、跌涨浮["0930",1639.83,1625.58,"0.10"]
private List<List<Object>> real_trend_line;
private HotDetailHotInfo hot_info; //实时行情数据
//概念走势图, 每个元素表示:时间、概念、热度 [20200803,"1582.08","30.00"],
private List<List<Object>> trend_line;
private List<DetailStock> stock_list;
public String getConcept_id() {
return concept_id;
}
public void setConcept_id(String concept_id) {
this.concept_id = concept_id;
}
public String getConcept_name() {
return concept_name;
}
public void setConcept_name(String concept_name) {
this.concept_name = concept_name;
}
public String getIntro() {
return intro;
}
public void setIntro(String intro) {
this.intro = intro;
}
public String getPub_time() {
return pub_time;
}
public void setPub_time(String pub_time) {
this.pub_time = pub_time;
}
public List<HotDetailNew> getNews() {
return news;
}
public void setNews(List<HotDetailNew> news) {
this.news = news;
}
public List<List<Object>> getReal_trend_line() {
return real_trend_line;
}
public void setReal_trend_line(List<List<Object>> real_trend_line) {
this.real_trend_line = real_trend_line;
}
public HotDetailHotInfo getHot_info() {
return hot_info;
}
public void setHot_info(HotDetailHotInfo hot_info) {
this.hot_info = hot_info;
}
public List<List<Object>> getTrend_line() {
return trend_line;
}
public void setTrend_line(List<List<Object>> trend_line) {
this.trend_line = trend_line;
}
public List<DetailStock> getStock_list() {
return stock_list;
}
public void setStock_list(List<DetailStock> stock_list) {
this.stock_list = stock_list;
}
}
@@ -0,0 +1,102 @@
package com.openxu.cview.xmstock20201030.bean;
/**
* autour : xiami
* date : 2020/11/13 14:59
* className : TopDetailNew
* version : 1.0
* description : 概念走势
*/
public class HotDetailHotInfo {
private String uptime;
private String concept_name;
private String concept_summary;
private float trade;
private float updownvalue;
private float updownrate;
private float up_count;
private float zhangting_count;
private float down_count;
private float dieting_count;
public String getUptime() {
return uptime;
}
public void setUptime(String uptime) {
this.uptime = uptime;
}
public String getConcept_name() {
return concept_name;
}
public void setConcept_name(String concept_name) {
this.concept_name = concept_name;
}
public String getConcept_summary() {
return concept_summary;
}
public void setConcept_summary(String concept_summary) {
this.concept_summary = concept_summary;
}
public float getTrade() {
return trade;
}
public void setTrade(float trade) {
this.trade = trade;
}
public float getUpdownvalue() {
return updownvalue;
}
public void setUpdownvalue(float updownvalue) {
this.updownvalue = updownvalue;
}
public float getUpdownrate() {
return updownrate;
}
public void setUpdownrate(float updownrate) {
this.updownrate = updownrate;
}
public float getUp_count() {
return up_count;
}
public void setUp_count(float up_count) {
this.up_count = up_count;
}
public float getZhangting_count() {
return zhangting_count;
}
public void setZhangting_count(float zhangting_count) {
this.zhangting_count = zhangting_count;
}
public float getDown_count() {
return down_count;
}
public void setDown_count(float down_count) {
this.down_count = down_count;
}
public float getDieting_count() {
return dieting_count;
}
public void setDieting_count(float dieting_count) {
this.dieting_count = dieting_count;
}
}
@@ -0,0 +1,39 @@
package com.openxu.cview.xmstock20201030.bean;
/**
* autour : xiami
* date : 2020/11/13 14:59
* className : TopDetailNew
* version : 1.0
* description : 概念走势
*/
public class HotDetailNew {
private String id;
private String title;
private String ctime;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getCtime() {
return ctime;
}
public void setCtime(String ctime) {
this.ctime = ctime;
}
}
@@ -0,0 +1,40 @@
package com.openxu.cview.xmstock20201030.bean;
/**
* autour : xiami
* date : 2020/11/13 14:59
* className : TopDetail
* version : 1.0
* description : 券商热点与走势对比
*/
public class TopDetail {
private int code;
private String err_info;
private TopDetailData data;
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public TopDetailData getData() {
return data;
}
public void setData(TopDetailData data) {
this.data = data;
}
public String getErr_info() {
return err_info;
}
public void setErr_info(String err_info) {
this.err_info = err_info;
}
}
@@ -0,0 +1,86 @@
package com.openxu.cview.xmstock20201030.bean;
import java.util.List;
/**
* autour : xiami
* date : 2020/11/13 14:59
* className : TopDetailData
* version : 1.0
* description : 券商热点与走势对比
*/
public class TopDetailData {
private String concept_id;
private String concept_name;
private String title;
private String content;
private String opentime;
private List<DetailStock> stock_list;
private List<List<Object>> trend_line;
private List<TopDetailNew> news;
public String getConcept_id() {
return concept_id;
}
public void setConcept_id(String concept_id) {
this.concept_id = concept_id;
}
public String getConcept_name() {
return concept_name;
}
public void setConcept_name(String concept_name) {
this.concept_name = concept_name;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getOpentime() {
return opentime;
}
public void setOpentime(String opentime) {
this.opentime = opentime;
}
public List<DetailStock> getStock_list() {
return stock_list;
}
public void setStock_list(List<DetailStock> stock_list) {
this.stock_list = stock_list;
}
public List<List<Object>> getTrend_line() {
return trend_line;
}
public void setTrend_line(List<List<Object>> trend_line) {
this.trend_line = trend_line;
}
public List<TopDetailNew> getNews() {
return news;
}
public void setNews(List<TopDetailNew> news) {
this.news = news;
}
}
@@ -0,0 +1,30 @@
package com.openxu.cview.xmstock20201030.bean;
/**
* autour : xiami
* date : 2020/11/13 14:59
* className : TopDetailNew
* version : 1.0
* description : 券商热点与走势对比
*/
public class TopDetailNew {
private String title;
private String publishdate;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getPublishdate() {
return publishdate;
}
public void setPublishdate(String publishdate) {
this.publishdate = publishdate;
}
}
@@ -0,0 +1,351 @@
package com.openxu.hkchart
import android.animation.ValueAnimator
import android.animation.ValueAnimator.AnimatorUpdateListener
import android.content.Context
import android.graphics.*
import android.graphics.drawable.Drawable
import android.text.TextUtils
import android.util.AttributeSet
import android.util.Log
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.ScaleGestureDetector
import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.Scroller
import com.openxu.cview.chart.anim.AngleEvaluator
import com.openxu.hkchart.config.ChartConfigBase
import com.openxu.hkchart.config.MultipartBarConfig
import com.openxu.hkchart.config.MultipartBarData
import com.openxu.hkchart.data.FocusData
import com.openxu.hkchart.loading.BallPulseIndicator
import com.openxu.hkchart.loading.LoadingIndicator
import com.openxu.utils.DensityUtil
import com.openxu.utils.LogUtil
import com.openxu.utils.SharedData
import kotlin.math.abs
/**
* Author: openXu
* Time: 2021/5/11 11:01
* class: BaseChart
* Description:
*/
open abstract class BaseChart<T> : View, View.OnTouchListener {
protected var TAG = javaClass.simpleName
protected var debug = SharedData.getInstance().sp.getBoolean(SharedData.KEY_DEBUG, false)
//画笔
protected var paint: Paint
protected var paintText: Paint
protected var paintEffect: Paint
//屏幕宽高
protected var screenWidth = 0
protected var screenHeight = 0
protected lateinit var rectDrawBounds : RectF //图表绘制矩形区域(每次刷新数据固定不变)
protected lateinit var rectChart : RectF //图表主体绘制矩形(刷新数据时会重新计算)
protected lateinit var centerPoint : Point //chart中心点坐标
//坐标轴辅助线宽度
protected var axisLineWidth = DensityUtil.dip2px(context, 0.8f)
/**设置是否正在加载*/
var loading = true
set(value){
field = value
if (value) {
loadingIndicator.start()
} else {
loadingIndicator.stop()
postInvalidate()
}
}
//动画
private lateinit var chartAnim: ValueAnimator
protected var chartAnimValue = 1f //动画值
protected var chartAnimStarted = false
private lateinit var loadingIndicator: LoadingIndicator
constructor(context: Context) :this(context, null)
constructor(context: Context, attrs: AttributeSet?) :this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyle: Int):super(context, attrs, defStyle){
mGestureDetector = GestureDetector(getContext(), MyOnGestureListener())
mScaleGestureDetector = ScaleGestureDetector(context, MyOnScaleGestureListener())
mScroller = Scroller(context)
setOnTouchListener(this)
val dm = resources.displayMetrics
screenHeight = dm.heightPixels
screenWidth = dm.widthPixels
paint = Paint()
paintText = Paint()
paintEffect = Paint()
paint.isAntiAlias = true
paintText.isAntiAlias = true
paintEffect.isAntiAlias = true
//加载动画
setLoadingIndicator("BallPulseIndicator")
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
centerPoint = Point(measuredWidth / 2, measuredHeight / 2)
rectDrawBounds = RectF(paddingLeft.toFloat(), paddingTop.toFloat(), (measuredWidth - paddingRight).toFloat(),
(measuredHeight - paddingBottom).toFloat())
rectChart = RectF(rectDrawBounds.left, rectDrawBounds.top, rectDrawBounds.right,rectDrawBounds.bottom)
loadingIndicator.setBounds(rectDrawBounds.left.toInt(), rectDrawBounds.top.toInt(),
rectDrawBounds.right.toInt(), rectDrawBounds.bottom.toInt())
initial()
}
override fun onDraw(canvas: Canvas) {
// Log.e(TAG, "=================绘制图表");
if (debug) drawDebug(canvas)
if (loading) {
val saveCount = canvas.save()
// canvas.translate(getPaddingLeft(), getPaddingTop());
loadingIndicator!!.draw(canvas)
canvas.restoreToCount(saveCount)
return
}
if(chartConfig==null){
drawChart(canvas)
}else{
if (chartConfig!!.showAnim && !chartAnimStarted) {
chartAnimStarted = true
startChartAnimation()
} else {
drawChart(canvas)
}
}
}
private fun drawDebug(canvas: Canvas) {
//绘制debug辅助线
paint.style = Paint.Style.STROKE //设置空心
paint.strokeWidth = axisLineWidth.toFloat()
//绘制边界--chart区域
paint.color = Color.BLACK
var r = RectF(0f, 0f, measuredWidth.toFloat(), measuredHeight.toFloat())
canvas.drawRect(r, paint)
paint.color = Color.RED
r = RectF(paddingLeft.toFloat(), paddingTop.toFloat(), (measuredWidth - paddingRight).toFloat(), (measuredHeight - paddingBottom).toFloat())
canvas.drawRect(r, paint)
paint.color = Color.GREEN
canvas.drawRect(rectDrawBounds, paint)
}
/***************************1. API👇👇👇***************************/
var chartConfig : ChartConfigBase? = null
/***************************1. API👆👆👆***************************/
/***************************2. 子类重写👇👇👇***************************/
/**初步计算,当设置数据 & size发生变化时调用*/
open fun initial() :Boolean{
if(!this::rectDrawBounds.isInitialized)
return true
return false
}
/**绘制图表 */
open abstract fun drawChart(canvas: Canvas?)
/**焦点坐标*/
open abstract fun onFocusTouch(point: PointF?)
/**缩放开始*/
open abstract fun onScaleBegin(detector: ScaleGestureDetector)
/**缩放*/
open abstract fun onScale(detector: ScaleGestureDetector, beginScrollx: Float)
/**缩放结束*/
open fun onScaleEnd(detector: ScaleGestureDetector){}
/***************************2. 子类重写👆👆👆***************************/
/***************************3. 事件👇👇👇***************************/
private var mGestureDetector : GestureDetector //手势监听,处理滑动
private var mScroller : Scroller //滚动帮助类,处理快速滚动(惯性)
private var mScaleGestureDetector : ScaleGestureDetector //缩放手势监听,处理缩放
/*计算*/
protected var scrollXMax = 0f //最大滚动距离,是一个负值
protected var scrollx = 0f //当前滚动距离,范围:scrollXMax ~ 0 (scrollXMax会自动计算)。从第一条数据绘制(scrollx==0),如果从最后一条数据绘制(scrollx==scrollXMax)
protected var scalex = 1f //x方向缩放系数(范围默认1~2,可根据需求在MyOnScaleGestureListener类中修改)
protected var focusData: FocusData<T>? = null //焦点数据
protected var focusIndex = 0 //焦点落在数据集合的索引值
/**
* 重写dispatchTouchEvent,并调用requestDisallowInterceptTouchEvent申请父控件不要拦截事件,将事件处理权交给图表
*
* 这对图表来说是非常重要的,比如图表放在ScrollerView里面时,如果不调用requestDisallowInterceptTouchEvent(true),
* 图表接受的事件将由ScrollerView决定,一旦ScrollerView发现竖直滚动则会拦截事件,导致图表不能再接受到事件
*
* 此处首先申请父控件不要拦截事件,所有事件都将传到图表中,由图表决定自己是否处理事件,如果不需要处理(竖直方向滑动距离大于水平方向)则让父控件处理
* 需要注意的是一旦放弃处理,剩下的事件将不会被收到
*/
private var mDownX = 0f //dispatchTouchEvent方法中手指按下时的坐标,用于决定什么时候拦截事件
private var mDownY = 0f
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
mDownX = event.x
mDownY = event.y
parent.requestDisallowInterceptTouchEvent(true) //ACTION_DOWN的时候,赶紧把事件hold住
}
//不处于缩放状态,竖直滑动的距离大于阈值,将事件还给父控件
MotionEvent.ACTION_MOVE -> if (!scaleing && abs(event.y - mDownY) > abs(event.x - mDownX) * 1.5) {
parent.requestDisallowInterceptTouchEvent(false)
}
}
return super.dispatchTouchEvent(event)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
return super.onTouchEvent(event)
}
protected val focusPoint = PointF() //焦点坐标
override fun onTouch(v: View?, event: MotionEvent): Boolean {
mScaleGestureDetector.onTouchEvent(event)
mGestureDetector.onTouchEvent(event)
when (event.action) {
MotionEvent.ACTION_DOWN -> {
focusPoint.x = event.x
focusPoint.y = event.y
onFocusTouch(focusPoint)
}
MotionEvent.ACTION_MOVE -> {
focusPoint.x = event.x
focusPoint.y = event.y
onFocusTouch(focusPoint)
}
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> onFocusTouch(null)
}
postInvalidate()
return true
}
inner class MyOnGestureListener : GestureDetector.SimpleOnGestureListener() {
override fun onDown(e: MotionEvent): Boolean {
if (!mScroller.isFinished) mScroller.forceFinished(true)
return true //事件被消费,下次才能继续收到事件
}
override fun onScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
if (!scaleing) {
scrollx -= distanceX //distanceX左正右负
scrollx = Math.min(scrollx, 0f)
scrollx = Math.max(scrollXMax, scrollx)
LogUtil.i(TAG, "---------滚动:$distanceX $scrollx")
postInvalidate()
}
return false
}
override fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
// LogUtil.e(TAG,"onFling------------>velocityX="+velocityX+" velocityY="+velocityY);
/**
* 从当前位置scrollx开始滚动,
* 最小值为scrollXMax -- 滚动到最后
* 最大值为0 -- 滚动到开始
*/
mScroller.fling(scrollx.toInt(), 0, velocityX.toInt(), 0,
scrollXMax.toInt(), 0, 0, 0
)
return false
}
}
override fun computeScroll() {
super.computeScroll()
if (mScroller.isFinished) return
if (mScroller.computeScrollOffset()) {
// Log.d(TAG, "滚动后计算:"+mScroller.getCurrX());
scrollx = mScroller.currX.toFloat()
invalidate()
}
}
/**缩放监听*/
private var scaleing = false //是否正在缩放
private inner class MyOnScaleGestureListener : ScaleGestureDetector.OnScaleGestureListener {
private var beginScrollx = 0f
override fun onScale(detector: ScaleGestureDetector): Boolean {
this@BaseChart.onScale(detector, beginScrollx)
postInvalidate()
return true
}
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
LogUtil.i(TAG, ">>>>>>>>>>>>>>>>>>>缩放开始了")
beginScrollx = scrollx
scaleing = true
this@BaseChart.onScaleBegin(detector)
return true
}
override fun onScaleEnd(detector: ScaleGestureDetector) {
LogUtil.i(TAG, "<<<<<<<<<<<<<<<<<<<<缩放结束了")
scaleing = false
this@BaseChart.onScaleEnd(detector)
}
}
/***************************3. 事件👆👆👆***************************/
/***************************4. 动画👇👇👇***************************/
private fun setLoadingIndicator(indicatorName: String) {
var indicatorName = indicatorName
if (TextUtils.isEmpty(indicatorName)) return
loadingIndicator = BallPulseIndicator(context)
indicatorName = "com.openxu.hkchart.loading.$indicatorName"
try {
loadingIndicator = Class.forName(indicatorName).getConstructor(Context::class.java).newInstance(context) as LoadingIndicator
loadingIndicator.callback = this
} catch (e: Exception) {
Log.e(TAG, "Didn't find your class , check the name again !")
}
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
if(loading && this::loadingIndicator.isInitialized)
loadingIndicator.start()
}
override fun onDetachedFromWindow() {
if(this::loadingIndicator.isInitialized)
loadingIndicator.stop()
if(this::chartAnim.isInitialized)
chartAnim.cancel()
super.onDetachedFromWindow()
}
override fun verifyDrawable(who: Drawable): Boolean {
return who === loadingIndicator || super.verifyDrawable(who)
}
override fun invalidateDrawable(dr: Drawable) {
if (verifyDrawable(dr)) {
val dirty = dr.bounds
invalidate(dirty)
} else {
super.invalidateDrawable(dr)
}
}
private fun startChartAnimation() {
if(!this::chartAnim.isInitialized){
chartAnim = ValueAnimator.ofObject(AngleEvaluator(), 0f, 1f)
chartAnim.duration = 1000
chartAnim.interpolator = AccelerateDecelerateInterpolator()
chartAnim.addUpdateListener { animation ->
chartAnimValue = animation.animatedValue as Float
postInvalidate()
}
}
chartAnim.reverse()
chartAnim.start()
LogUtil.i(TAG, "-----------------开始动画")
}
/***************************4. 动画👆👆👆***************************/
}
@@ -0,0 +1,251 @@
package com.openxu.hkchart.bar
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.util.Log
import android.view.ScaleGestureDetector
import com.openxu.hkchart.BaseChart
import com.openxu.hkchart.config.*
import com.openxu.utils.DensityUtil
import com.openxu.utils.FontUtil
import com.openxu.utils.LogUtil
/**
* Author: openXu
* Time: 2021/5/11 13:47
* class: BarChart
* Description:
*/
class BarChart : BaseChart<Bar> {
constructor(context: Context) :this(context, null)
constructor(context: Context, attrs: AttributeSet?) :this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyle: Int):super(context, attrs, defStyle){
}
/**设置 */
private var barData = mutableListOf<Bar>()
/**计算 */
fun setData(barData: List<Bar>?) {
this.barData.clear()
barData?.let {
this.barData.addAll(barData)
}
initial()
chartConfig?.let {
if (it.showAnim) chartAnimStarted = false
}
loading = false
}
private var groupWidth = 0f
private lateinit var yAxisMark : YAxisMark
private lateinit var xAxisMark : XAxisMark
private var barColor = intArrayOf(
Color.parseColor("#f46763"),
Color.parseColor("#3cd595"),
Color.parseColor("#4d7bff")) //柱颜色
private var barWidth = DensityUtil.dip2px(context, 15f).toFloat() //柱宽度
private var barSpace = DensityUtil.dip2px(context, 1f).toFloat() //双柱间的间距
private var groupSpace = DensityUtil.dip2px(context, 25f).toFloat() //一组柱之间的间距(只有scrollAble==true时才生效)
override fun initial(): Boolean {
if(super.initial()) return true
if(barData.isNullOrEmpty()) return true
if(chartConfig==null)
throw RuntimeException("---------请配置图表")
val config = chartConfig as BarChartConfig
if(null==config.xAxisMark)
throw RuntimeException("---------请设置x坐标")
if(null==config.yAxisMark)
throw RuntimeException("---------请设置y坐标")
xAxisMark = config.xAxisMark!!
yAxisMark = config.yAxisMark!!
barColor = config.barColor
barWidth = config.barWidth
barSpace = config.barSpace
groupSpace = config.groupSpace
paintText.textSize = xAxisMark.textSize.toFloat()
xAxisMark.textHeight = FontUtil.getFontHeight(paintText)
xAxisMark.textLead = FontUtil.getFontLeading(paintText)
//确定图表最下放绘制位置
rectChart.bottom = measuredHeight - paddingBottom - xAxisMark.textHeight - xAxisMark.textSpace
xAxisMark.drawPointY = rectChart.bottom + xAxisMark.textSpace + xAxisMark.textLead
calculateYMark()
paintText.textSize = yAxisMark.textSize.toFloat()
yAxisMark.textHeight = FontUtil.getFontHeight(paintText)
yAxisMark.textLead = FontUtil.getFontLeading(paintText)
val maxLable = yAxisMark.getMarkText(yAxisMark.cal_mark_max)
rectChart.left = (paddingLeft + yAxisMark.textSpace + FontUtil.getFontlength(paintText, maxLable))
rectChart.top = rectChart.top + yAxisMark.textHeight / 2
val barNum: Int = barData[0].valuey.size
if (config.scrollAble) {
groupWidth = (config.barWidth * barNum + config.barSpace * (barNum - 1) + config.groupSpace).toFloat()
val allWidth: Float = groupWidth * barData.size //总宽度
scrollXMax = -(allWidth - rectChart.width())
scrollx = if (config.displayScheme==DisplayScheme.SHOW_BEGIN) 0f else scrollXMax
} else {
config.groupSpace = (rectChart.width() - barData.size * (config.barWidth * barNum + config.barSpace * (barNum - 1))) / barData.size
groupWidth = (config.barWidth * barNum + config.barSpace * (barNum - 1) + config.groupSpace).toFloat()
scrollXMax = 0f
scrollx = scrollXMax
}
Log.w(TAG, "计算groupWidth=$groupWidth config.barWidth=$config.barWidth scrollx=$scrollx")
return true
}
override fun drawChart(canvas: Canvas?) {
val yMarkSpace = (rectChart.bottom - rectChart.top) / (yAxisMark.lableNum - 1)
paint.style = Paint.Style.STROKE
paint.strokeWidth = yAxisMark.lineColor.toFloat()
paint.color = yAxisMark.lineColor
paintEffect.style = Paint.Style.STROKE
paintEffect.strokeWidth = yAxisMark.lineWidth.toFloat()
paintEffect.color = yAxisMark.lineColor
paintText.textSize = yAxisMark.textSize.toFloat()
paintText.color = yAxisMark.textColor
// canvas.drawLine(rectChart.left, rectChart.top, rectChart.left, rectChart.bottom, paint);
// canvas.drawLine(rectChart.left, rectChart.top, rectChart.left, rectChart.bottom, paint);
val effects: PathEffect = DashPathEffect(floatArrayOf(15f, 6f, 15f, 6f), 0f)
paintEffect.pathEffect = effects
for (i in 0 until yAxisMark.lableNum) {
/**绘制横向线 */
canvas!!.drawLine(rectChart.left, rectChart.bottom - yMarkSpace * i,
rectChart.right, rectChart.bottom - yMarkSpace * i, paint)
/**绘制y刻度 */
val text = yAxisMark.getMarkText(yAxisMark.cal_mark_min + i * yAxisMark.cal_mark)
canvas!!.drawText(text,
rectChart.left - yAxisMark.textSpace - FontUtil.getFontlength(paintText, text),
rectChart.bottom - yMarkSpace * i - yAxisMark.textHeight / 2 + yAxisMark.textLead, paintText)
}
/**绘制柱状 */
paint.style = Paint.Style.FILL
val rect = RectF()
val rectArc = RectF()
val path = Path()
//预算需要绘制的组的开始和结尾index,避免不必要的计算浪费性能
val startIndex = (-scrollx / groupWidth).toInt()
var endIndex = ((-scrollx + rectChart.width()) / groupWidth).toInt()
endIndex = Math.min(endIndex, barData.size - 1)
// for(int i = 0; i<barData.size(); i++){
// for(int i = 0; i<barData.size(); i++){
for (i in startIndex..endIndex) {
val group = barData[i]
//一组
/**绘制X刻度 */
paintText.textSize = xAxisMark.textSize.toFloat()
paintText.color = xAxisMark.textColor
rect.left = scrollx + rectChart.left + i * groupWidth
rect.right = rect.left + groupWidth
//过滤掉绘制区域外的组
// if(rect.right < rectChart.left || rect.left > rectChart.right) {
// Log.w(TAG, "第"+i+"组超界了 "+rect.left+" "+rect.right);
// continue;
// }
//裁剪画布,避免x刻度超出
var restoreCount = canvas!!.save()
canvas!!.clipRect(RectF(rectChart.left, rectChart.bottom,
rectChart.right, rectChart.bottom + xAxisMark.textSpace + xAxisMark.textHeight))
canvas!!.drawText(group.valuex,
rect.left + groupWidth / 2 - FontUtil.getFontlength(paintText, group.valuex) / 2,
xAxisMark.drawPointY, paintText)
canvas!!.restoreToCount(restoreCount)
/**绘制柱状 */
// 记录当前画布信息
restoreCount = canvas!!.save()
/**使用Canvas的clipRect和clipPath方法限制View的绘制区域 */
canvas!!.clipRect(rectChart) //裁剪画布,只绘制rectChart的范围
for (j in group.valuey.indices) {
paint.color = barColor[j]
// float top = (zeroPoint.y - YMARK_ALL_H * (bean.getNum() / YMARK_MAX) * animPro);
rect.left = rectChart.left + i * groupWidth + groupSpace / 2 + j * (barSpace + barWidth) + scrollx
rect.right = rect.left + barWidth
//过滤掉绘制区域外的柱
if (rect.right < rectChart.left || rect.left > rectChart.right) continue
if (group.valuey[j] == null) {
rect.top = rectChart.bottom
} else {
rect.top = (rectChart.bottom - rectChart.height() / (yAxisMark.cal_mark_max - yAxisMark.cal_mark_min) *
(group.valuey[j] - yAxisMark.cal_mark_min) * chartAnimValue)
}
rect.bottom = rectChart.bottom
rectArc.left = rect.left
rectArc.top = rect.top
rectArc.right = rect.right
rectArc.bottom = rect.top + barWidth
path.reset()
path.moveTo(rect.left, rectChart.bottom)
path.lineTo(rectArc.left, rectArc.bottom - rectArc.height() / 2)
path.arcTo(rectArc, 180f, 180f)
path.lineTo(rect.right, rect.bottom)
path.close()
// Log.w(TAG, "---绘制"+i+"*"+j+" = "+group.get(j).getValuey()+" " +rect.top +" "+rectChart.bottom);
canvas!!.drawPath(path, paint)
/**绘制y值 */
if (group.valuey[j] != null) {
canvas!!.drawText(yAxisMark.getMarkText(group.valuey[j]),
rectArc.left + barWidth / 2 - FontUtil.getFontlength(paintText, yAxisMark.getMarkText(group.valuey[j])) / 2,
rectArc.top - yAxisMark.textSpace - yAxisMark.textHeight + yAxisMark.textLead, paintText)
}
}
//恢复到裁切之前的画布
canvas!!.restoreToCount(restoreCount)
}
}
override fun onFocusTouch(point: PointF?) {
}
override fun onScaleBegin(detector: ScaleGestureDetector) {
}
override fun onScale(detector: ScaleGestureDetector, beginScrollx: Float) {
}
private fun calculateYMark() {
val redundance = 1.01f //y轴最大和最小值冗余
yAxisMark.cal_mark_max = -Float.MIN_VALUE //Y轴刻度最大值
yAxisMark.cal_mark_min = Float.MAX_VALUE //Y轴刻度最小值
for (data in barData) {
for (valuey in data.valuey) {
yAxisMark.cal_mark_max = Math.max(yAxisMark.cal_mark_max, valuey)
yAxisMark.cal_mark_min = Math.min(yAxisMark.cal_mark_min, valuey)
}
}
LogUtil.i(TAG, "Y轴真实cal_mark_min=" + yAxisMark.cal_mark_min.toString() + " cal_mark_max=" + yAxisMark.cal_mark_max)
if (yAxisMark.markType === MarkType.Integer) {
val min = 0
val max = yAxisMark.cal_mark_max.toInt()
var mark = (max - min) / (yAxisMark.lableNum - 1) + if ((max - min) % (yAxisMark.lableNum - 1) > 0) 1 else 0
val first = (mark.toString() + "").substring(0, 1).toInt() + 1
LogUtil.i(TAG, "mark=$mark first=$first")
if ((mark.toString() + "").length == 1) {
//YMARK = 1、2、5、10
mark = if (mark == 3 || mark == 4 || mark == 6 || mark == 7 || mark == 8 || mark == 9) if (mark == 3 || mark == 4) 5 else 10 else mark
} else if ((mark.toString() + "").length == 2) {
mark = first * 10
} else if ((mark.toString() + "").length == 3) {
mark = first * 100
} else if ((mark.toString() + "").length == 4) {
mark = first * 1000
} else if ((mark.toString() + "").length == 5) {
mark = first * 10000
} else if ((mark.toString() + "").length == 6) {
mark = first * 100000
}
yAxisMark.cal_mark_min = 0f
yAxisMark.cal_mark_max = (mark * (yAxisMark.lableNum - 1)).toFloat()
yAxisMark.cal_mark = mark.toFloat()
} else { //Float //Percent
yAxisMark.cal_mark_max *= redundance
yAxisMark.cal_mark_min /= redundance
yAxisMark.cal_mark = (yAxisMark.cal_mark_max - yAxisMark.cal_mark_min) / (yAxisMark.lableNum - 1)
}
LogUtil.i(TAG, " cal_mark_min=" + yAxisMark.cal_mark_min.toString() + " cal_mark_max=" + yAxisMark.cal_mark_max.toString() + " yAxisMark.cal_mark=" + yAxisMark.cal_mark)
}
}
@@ -0,0 +1,330 @@
package com.openxu.hkchart.bar
import android.content.Context
import android.graphics.*
import android.text.TextUtils
import android.util.AttributeSet
import android.view.ScaleGestureDetector
import com.openxu.hkchart.BaseChart
import com.openxu.hkchart.config.*
import com.openxu.utils.DensityUtil
import com.openxu.utils.FontUtil
import com.openxu.utils.LogUtil
/**
* Author: openXu
* Time: 2021/5/11 14:07
* class: HorizontalBarChart
* Description:
*/
class HorizontalBarChart : BaseChart<HBar> {
/**计算 */
private val chartRect = RectF() //图表矩形
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle) {
}
/**设置 */
private val barData = mutableListOf<HBar>()
private lateinit var yAxisMark: YAxisMark
private lateinit var xAxisMark: XAxisMark
private var barWidth = DensityUtil.dip2px(context, 26f)
private var barSpace = DensityUtil.dip2px(context, 10f)
private var barColor = intArrayOf(
Color.parseColor("#F46863"),
Color.parseColor("#2DD08A"),
Color.parseColor("#567CF6"),
Color.parseColor("#F5B802"),
Color.parseColor("#CC71F7"))
fun setData(barData: List<HBar>?) {
if(chartConfig==null)
throw RuntimeException("---------请配置图表")
LogUtil.w(TAG, "设置数据:$barData")
this.barData.clear()
barData?.let {
this.barData.addAll(barData)
}
var config = chartConfig as HorizontalBarConfig
if(null==config.xAxisMark)
throw RuntimeException("---------请设置x坐标")
if(null==config.yAxisMark)
throw RuntimeException("---------请设置y坐标")
xAxisMark = config.xAxisMark!!
yAxisMark = config.yAxisMark!!
barWidth = config.barWidth
barSpace = config.barSpace
barColor = config.barColor
if (config.showAnim) chartAnimStarted = false
calculateYMark()
loading = false
requestLayout()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
LogUtil.v(TAG, "测量建议:" + MeasureSpec.UNSPECIFIED + " " + MeasureSpec.EXACTLY + " " + MeasureSpec.AT_MOST)
LogUtil.v(TAG, "宽测量建议:$widthSize*$widthMode")
LogUtil.v(TAG, "高度测量建议:$heightSize*$heightMode")
scrollXMax = 0f
scrollx = 0f
var height = 0
when (heightMode) {
MeasureSpec.EXACTLY -> height = heightSize
MeasureSpec.UNSPECIFIED, MeasureSpec.AT_MOST -> //计算需要的高度
if (!barData.isNullOrEmpty()) {
height += paddingTop
height += paddingBottom
paintText.textSize = yAxisMark.textSize.toFloat()
paintText.typeface = yAxisMark.numberTypeface
yAxisMark.textHeight = FontUtil.getFontHeight(paintText)
yAxisMark.textLead = FontUtil.getFontLeading(paintText)
height += yAxisMark.textSpace
height += yAxisMark.textHeight.toInt()
height += (barWidth + barSpace) * barData.size
//
/* if(height>heightSize){
}
scrollAble = false;
scrollXMax = 0;
scrollx = 0;*/LogUtil.v(TAG, "实际需要高度:$heightSize")
} else {
//默认最小高度用于显示正在加载中
height = DensityUtil.dip2px(context, 150f)
}
}
LogUtil.v(TAG, "测量:$widthSize*$height")
setMeasuredDimension(widthSize, height)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
if (barData.isNullOrEmpty())
return
paintText.textSize = xAxisMark.textSize.toFloat()
paintText.typeface = Typeface.DEFAULT
xAxisMark.textHeight = FontUtil.getFontHeight(paintText)
xAxisMark.textLead = FontUtil.getFontLeading(paintText)
var lableMax = 0f
for (hbar in barData) {
lableMax = Math.max(lableMax, FontUtil.getFontlength(paintText, hbar.lable))
}
chartRect.left = paddingLeft + lableMax + xAxisMark.textSpace
chartRect.top = yAxisMark.textSpace + yAxisMark.textHeight
paintText.textSize = yAxisMark.textSize.toFloat()
paintText.typeface = yAxisMark.numberTypeface
chartRect.right = rectChart.right - FontUtil.getFontlength(paintText, yAxisMark.getMarkText(yAxisMark.cal_mark_max)) / 2
chartRect.bottom = rectChart.bottom
LogUtil.w(TAG, "重新计算绘制范围:$chartRect")
}
private var rect = RectF() //备用,后面绘制矩形
private var path = Path()
override fun drawChart(canvas: Canvas?) {
if (barData.isNullOrEmpty()) return
var axis0x = 0f //y轴0刻度的x坐标
var top: Float = chartRect.top + barSpace / 2 //绘制柱状开始的最上方位置
paintText.textSize = xAxisMark.textSize.toFloat()
paintText.color = xAxisMark.textColor
paintText.typeface = Typeface.DEFAULT
xAxisMark.textHeight = FontUtil.getFontHeight(paintText)
xAxisMark.textLead = FontUtil.getFontLeading(paintText)
paint.style = Paint.Style.FILL
for (i in 0 until barData.size) {
val bar = barData[i]
//绘制x标签
val lable: String = getXValue(bar.lable)
val textWidth = FontUtil.getFontlength(paintText, lable)
canvas!!.drawText(lable,
chartRect.left - xAxisMark.textSpace - textWidth,
top + barWidth / 2 - xAxisMark.textHeight / 2 + xAxisMark.textLead, paintText)
//绘制底色
paint.color = Color.parseColor("#f0f0f0")
rect.left = chartRect.left
rect.top = top
rect.right = chartRect.right
rect.bottom = top + barWidth
canvas!!.drawRect(rect, paint)
top += barWidth + barSpace
}
paint.style = Paint.Style.STROKE
paint.strokeWidth = yAxisMark.lineWidth.toFloat()
paint.color = yAxisMark.lineColor
paintEffect.style = Paint.Style.STROKE
paintEffect.strokeWidth = yAxisMark.lineWidth.toFloat()
paintEffect.color = yAxisMark.lineColor
val effects: PathEffect = DashPathEffect(floatArrayOf(15f, 6f, 15f, 6f), 0f)
paintEffect.pathEffect = effects
paintText.textSize = yAxisMark.textSize.toFloat()
paintText.color = yAxisMark.textColor
paintText.typeface = yAxisMark.numberTypeface
val yMarkSpace = chartRect.width() / (yAxisMark.lableNum - 1)
for (i in 0 until yAxisMark.lableNum) {
/**竖直线 */
if (index0 === i) axis0x = chartRect.left + yMarkSpace * i
//canvas.drawLine()给paint设置DashPathEffect(虚线)无效。后面发现是硬件加速的锅。 解决方法就是,在view层关闭硬件加速
// setLayerType(View.LAYER_TYPE_SOFTWARE, null);
// canvas.drawLine(chartRect.left + yMarkSpace*i, chartRect.top,
// chartRect.left + yMarkSpace*i,chartRect.bottom, index0 == i?paint:paintEffect);
//canvas.drawPath()可以绘制虚线,不用关闭硬件加速
path.reset()
path.moveTo(chartRect.left + yMarkSpace * i, chartRect.top)
path.lineTo(chartRect.left + yMarkSpace * i, chartRect.bottom)
canvas!!.drawPath(path, if (index0 === i) paint else paintEffect)
/**绘制y刻度 */
val text = yAxisMark.getMarkText(yAxisMark.cal_mark_min + i * yAxisMark.cal_mark)
canvas!!.drawText(text,
chartRect.left + yMarkSpace * i - FontUtil.getFontlength(paintText, text) / 2,
rectChart.top + yAxisMark.textLead, paintText)
}
/**绘制数据*/
/**绘制数据 */
paint.style = Paint.Style.FILL
paintText.textSize = xAxisMark.textSize.toFloat()
paintText.color = xAxisMark.textColor
paintText.typeface = yAxisMark.numberTypeface
//两种不同的字体设置后要重新计算 文字高度和基线,否则会出现意想不到的效果
//两种不同的字体设置后要重新计算 文字高度和基线,否则会出现意想不到的效果
xAxisMark.textHeight = FontUtil.getFontHeight(paintText)
xAxisMark.textLead = FontUtil.getFontLeading(paintText)
//y值每一份对应的宽度
//y值每一份对应的宽度
val once = chartRect.width() / (yAxisMark.cal_mark_max - yAxisMark.cal_mark_min)
top = chartRect.top + barSpace / 2
for (i in 0 until barData.size) {
val bar = barData[i]
//绘制颜色柱子
rect.top = top
rect.bottom = top +barWidth
if (bar.value >= 0) {
rect.left = axis0x
rect.right = rect.left + once * bar.value * chartAnimValue
} else {
rect.right = axis0x
rect.left = rect.right + once * bar.value * chartAnimValue
}
paint.color = barColor.get(i % barColor.size)
canvas!!.drawRect(rect, paint)
//绘制文字
val lable = yAxisMark.getMarkText(bar.value) + yAxisMark.unit
val textWidth = FontUtil.getFontlength(paintText, lable)
var x = if (rect.width() < textWidth) if (bar.value >= 0) rect.left else rect.right - textWidth else rect.left + rect.width() / 2 - textWidth / 2
if (x < chartRect.left + xAxisMark.textSpace) x = chartRect.left + xAxisMark.textSpace
if (x + textWidth > chartRect.right) x = chartRect.right - textWidth - xAxisMark.textSpace
paintText.typeface = yAxisMark.numberTypeface
canvas!!.drawText(lable,
x,
top + barWidth / 2 - xAxisMark.textHeight / 2 + xAxisMark.textLead, paintText)
top += barWidth + barSpace
}
}
override fun onFocusTouch(point: PointF?) {
}
override fun onScaleBegin(detector: ScaleGestureDetector) {
}
override fun onScale(detector: ScaleGestureDetector, beginScrollx: Float) {
}
//截取
private fun getXValue(value: String): String {
var value = value
if (TextUtils.isEmpty(value)) return ""
if (xAxisMark.splitSubLen > 0 && value.length > xAxisMark.splitSubLen) {
value = value.substring(0, xAxisMark.splitSubLen)
}
return value
}
private var index0 = 0 //0刻度对应的标签索引
private fun calculateYMark() {
if (barData == null || barData.size <= 0) return
yAxisMark.cal_mark_max = barData[0].value
yAxisMark.cal_mark_min = barData[0].value
for (data in barData) {
yAxisMark.cal_mark_max = Math.max(yAxisMark.cal_mark_max, data.value)
yAxisMark.cal_mark_min = Math.min(yAxisMark.cal_mark_min, data.value)
}
LogUtil.w(TAG, "真实的最小值=" + yAxisMark.cal_mark_min + " 最大值=" + yAxisMark.cal_mark_max + " y刻度数量:" + yAxisMark.lableNum)
if (yAxisMark.cal_mark_min > 0 && yAxisMark.cal_mark_max > 0) {
yAxisMark.cal_mark_min = 0f
} else if (yAxisMark.cal_mark_min < 0 && yAxisMark.cal_mark_max < 0) {
yAxisMark.cal_mark_max = 0f
}
val z = ((yAxisMark.cal_mark_max - yAxisMark.cal_mark_min) / (yAxisMark.lableNum - 1)).toInt()
val y = ((yAxisMark.cal_mark_max - yAxisMark.cal_mark_min) % (yAxisMark.lableNum - 1)).toInt()
var mark = z + if (y > 0) 1 else 0
LogUtil.w(TAG, "取整 $z 余 $y 计算mark=$mark")
mark = if (mark == 0) 1 else mark //最大值和最小值都为0的情况
LogUtil.w(TAG, yAxisMark.cal_mark_min.toString() + "~" + yAxisMark.cal_mark_max + "计算mark=" + mark)
if (mark <= 10) {
//YMARK = 1、2、5、10
mark = if (mark == 3 || mark == 4 || mark == 6 || mark == 7 || mark == 8 || mark == 9) if (mark == 3 || mark == 4) 5 else 10 else mark
} else {
//mark前两位,比如 4549 取mark1=4 mark2=5
var mark1 = (mark.toString() + "").substring(0, 1).toInt()
var mark2 = (mark.toString() + "").substring(1, 2).toInt()
LogUtil.w(TAG, "mark前两位=$mark1 $mark2")
if (mark2 < 5) {
mark2 = 5
} else {
mark2 = 0
mark1 += 1
}
val ws = (mark.toString() + "").length
LogUtil.w(TAG, "mark前两位=$mark1 $mark2 位数:$ws")
mark = mark1 * getWs(ws) + mark2 * getWs(ws - 1)
}
LogUtil.w(TAG, "取值mark=$mark")
if (yAxisMark.cal_mark_min < 0 && yAxisMark.cal_mark_max > 0) {
//需要显示0
index0 = (-yAxisMark.cal_mark_min / mark).toInt()+ if (-yAxisMark.cal_mark_min % mark != 0f) 1 else 0
while (checkMark(index0, mark, yAxisMark.cal_mark_max)) {
yAxisMark.lableNum++
LogUtil.w(TAG, "检测到正值可能越界,增加标签数量=" + yAxisMark.lableNum)
}
LogUtil.w(TAG, "一正一负的情况mark=" + mark + " index0=" + index0 + " lableNum=" + yAxisMark.lableNum)
yAxisMark.cal_mark_min = -mark * index0.toFloat()
yAxisMark.cal_mark_max = yAxisMark.cal_mark_min + mark * (yAxisMark.lableNum - 1)
} else if (yAxisMark.cal_mark_min == 0f) {
index0 = 0
yAxisMark.cal_mark_max = mark * (yAxisMark.lableNum - 1).toFloat()
} else if (yAxisMark.cal_mark_max == 0f) {
index0 = yAxisMark.lableNum - 1
yAxisMark.cal_mark_min = -mark * (yAxisMark.lableNum - 1).toFloat()
}
yAxisMark.cal_mark = mark.toFloat()
LogUtil.w(TAG, "最终取值=" + yAxisMark.cal_mark_min + "~" + yAxisMark.cal_mark_max + " mark=" + mark)
}
private fun getWs(ws: Int): Int {
return if (ws == 1) 1 else if (ws == 2) 10 else if (ws == 3) 100 else if (ws == 4) 1000 else if (ws == 5) 10000 else if (ws == 6) 100000 else if (ws == 7) 1000000 else if (ws == 8) 10000000 else if (ws == 9) 100000000 else 1
}
private fun checkMark(index0: Int, mark: Int, max: Float): Boolean {
return (yAxisMark.lableNum - 1 - index0) * mark < max
}
}
@@ -0,0 +1,590 @@
package com.openxu.hkchart.bar
import android.content.Context
import android.graphics.*
import android.text.TextUtils
import android.util.AttributeSet
import android.view.ScaleGestureDetector
import com.openxu.hkchart.BaseChart
import com.openxu.hkchart.config.*
import com.openxu.hkchart.data.FocusData
import com.openxu.hkchart.data.FocusPanelText
import com.openxu.utils.DensityUtil
import com.openxu.utils.FontUtil
import com.openxu.utils.LogUtil
import java.util.regex.Pattern
/**
* Author: openXu
* Time: 2021/5/9 12:00
* class: MultipartBarChart
* Description: 一个柱子分为多种颜色多部份的柱状图
*
* 特色:支持 缩放、滚动、惯性滚动、y坐标缩放变化、x坐标滚动变化
*
* 缩放因子:scalex 范围默认1~2
* 滑动距离:scrollx 范围:scrollXMax ~ 0 (scrollXMax会自动计算)
*
*/
class MultipartBarChart : BaseChart<MultipartBarData>{
constructor(context: Context) :this(context, null)
constructor(context: Context, attrs: AttributeSet?) :this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyle: Int):super(context, attrs, defStyle){
}
/***************************1. API👇👇👇***************************/
/**设置数据*/
private var _datas = mutableListOf<MultipartBarData>()
var datas: MutableList<MultipartBarData>
get() {return _datas}
set(value) {
_datas.clear()
_datas.addAll(value)
initial()
chartConfig?.let {
if (it.showAnim) chartAnimStarted = false
}
loading = false
}
/***************************1. API👆👆👆***************************/
/***************************2. 子类重写👇👇👇***************************/
/**配置*/
private lateinit var yAxisMark : YAxisMark
private lateinit var xAxisMark : XAxisMark
private lateinit var barColor : IntArray
private var focusPanelText: Array<FocusPanelText>? = null //焦点面板显示内容
private var dataTotalCount : Int = -1
/**初步计算*/
private var barWidth : Float = 0f //柱宽度
private var spacingRatio = 1f
private var barSpace : Float = 0f //柱间的间距
private var oneDataWidth : Float = 0f //单个柱子+间距 的宽度 ...( | |)
private var allDataWidth : Float = 0f //所有柱子+间距 的宽度 ( | | | |...| | )
private var foucsRectWidth = 0f //焦点面板矩形宽高
private var foucsRectHeight = 0f
override fun initial():Boolean{
if(super.initial()) return true
if(_datas.isNullOrEmpty()) return true
if(chartConfig==null)
throw RuntimeException("---------请配置图表")
var config = chartConfig as MultipartBarConfig
if(null==config.xAxisMark)
throw RuntimeException("---------请设置x坐标")
if(null==config.yAxisMark)
throw RuntimeException("---------请设置y坐标")
xAxisMark = config.xAxisMark!!
yAxisMark = config.yAxisMark!!
barColor = config.barColor
barWidth = config.barWidth
spacingRatio = config.spacingRatio
barSpace = barWidth * spacingRatio
dataTotalCount = config.dataTotalCount
if(dataTotalCount<0)
dataTotalCount = datas.size
focusPanelText = config.focusPanelText
/**重新确定表体矩形rectChart*/
paintText.textSize = xAxisMark.textSize.toFloat()
xAxisMark.textHeight = FontUtil.getFontHeight(paintText)
xAxisMark.textLead = FontUtil.getFontLeading(paintText)
//确定图表最下放绘制位置
rectChart.bottom = rectDrawBounds.bottom - (xAxisMark.textHeight + xAxisMark.textSpace)
xAxisMark.drawPointY = rectChart.bottom + xAxisMark.textSpace + xAxisMark.textLead
LogUtil.e(TAG, "--------------设置数据后第一次计算所有数据y轴刻度,以确定图标左侧位置")
calculateYMark(true)
paintText.textSize = yAxisMark.textSize.toFloat()
yAxisMark.textHeight = FontUtil.getFontHeight(paintText)
yAxisMark.textLead = FontUtil.getFontLeading(paintText)
var maxLable: String = yAxisMark.getMarkText(yAxisMark.cal_mark_max)
val minLable: String = yAxisMark.getMarkText(yAxisMark.cal_mark_min)
val maxYWidth = FontUtil.getFontlength(paintText, if(maxLable.length>minLable.length) maxLable else minLable)
rectChart.left = rectDrawBounds.left + yAxisMark.textSpace + maxYWidth
LogUtil.w(TAG, "原始顶部:${rectChart.top} 单位高度${if (TextUtils.isEmpty(yAxisMark.unit)) 0f else (yAxisMark.textHeight + yAxisMark.textSpace)} y一半:${yAxisMark.textHeight / 2}")
rectChart.top = rectDrawBounds.top + yAxisMark.textHeight / 2 + (if (TextUtils.isEmpty(yAxisMark.unit)) 0f else (yAxisMark.textHeight + yAxisMark.textSpace))
rectChart.right = rectDrawBounds.right
LogUtil.v(TAG, "确定表格矩形 $rectChart 宽度 ${rectChart.width()} 高度${rectChart.height()}")
/**重新计算柱子宽度 和 间距*/
LogUtil.e(TAG, "--------------根据显示配置和数据,计算柱子宽度和间距")
//根据设置的柱子宽度和间距,计算所有数据宽度
allDataWidth = dataTotalCount * barWidth + (dataTotalCount+1) * barSpace
when(config.displayScheme){
DisplayScheme.SHOW_ALL->{ //全部显示
if(allDataWidth > rectChart.width()){ //超出时,重新计算barWidth
// barWidth * dataTotalCount + barWidth*config.spacingRatio*(dataTotalCount+1) = rectChart.width()
barWidth = rectChart.width()/(dataTotalCount + spacingRatio*(dataTotalCount+1))
barSpace = barWidth * spacingRatio
LogUtil.w(TAG, "全部展示时宽度超过,重新计算柱子宽度$barWidth 间距 $barSpace")
}
}
DisplayScheme.SHOW_BEGIN->{}//从第一条数据开始展示,柱子宽度就是设置的宽度
DisplayScheme.SHOW_END->{} //从最后一条数据开始展示,柱子宽度就是设置的宽度
}
//最大放大倍数 = 设置的柱子宽度的3倍 / 当前柱子宽度
scaleXMax = config.barWidth * 3f / barWidth
LogUtil.v(TAG, "确定柱子宽度 $barWidth 间距 $barSpace 最大放大倍数$scaleXMax")
/**确定第一条数据的绘制x坐标 计算滚动最大值*/
oneDataWidth = barWidth + barSpace
allDataWidth = dataTotalCount * barWidth + (dataTotalCount+1) * barSpace
scrollx = 0f
scrollXMax = 0f
scalex = 1f
if(allDataWidth>rectChart.width()){
scrollXMax = rectChart.width() -allDataWidth //最大滚动距离,是一个负值
}
when(config.displayScheme){
DisplayScheme.SHOW_ALL->{ }//全部显示
DisplayScheme.SHOW_BEGIN->{
scrollx = 0f
}
DisplayScheme.SHOW_END->{
scrollx = scrollXMax
}
}
LogUtil.v(TAG, "单个柱子+间距 $oneDataWidth 所有数据宽度 $allDataWidth")
focusPanelText?.let {
//计算焦点面板
//2020-10-16 06:00
//零序电流:15.2KW
//A相电流:15.2KW
//A相电流:15.2KW
//A相电流:15.2KW
foucsRectWidth = 0f
foucsRectHeight = foucsRectSpace * 2.toFloat()
var text: String
maxLable = ((if (yAxisMark.getMarkText(yAxisMark.cal_mark_max).length >
yAxisMark.getMarkText(yAxisMark.cal_mark_min).length)
yAxisMark.getMarkText(yAxisMark.cal_mark_max) else
yAxisMark.getMarkText(yAxisMark.cal_mark_min))
+ if (TextUtils.isEmpty(yAxisMark.unit)) "" else yAxisMark.unit)
for (i in it.indices) {
if (it[i].show) {
paintText.textSize = it[i].textSize.toFloat()
if (i == 0) {//x轴数据
foucsRectWidth = Math.max(foucsRectWidth, FontUtil.getFontlength(paintText, it[i].text))
foucsRectHeight += FontUtil.getFontHeight(paintText)
} else {
text = it[i].text + maxLable
foucsRectWidth = Math.max(foucsRectWidth, FontUtil.getFontlength(paintText, text))
LogUtil.w(TAG, "计算面板:$text $foucsRectWidth")
foucsRectHeight += foucsRectTextSpace + FontUtil.getFontHeight(paintText)
}
}
}
foucsRectWidth += foucsRectSpace * 4.toFloat()
}
return true
}
override fun drawChart(canvas: Canvas?) {
LogUtil.e(TAG, "-----------开始绘制,当前缩放系数$scalex 偏移量$scrollx")
if(_datas.isNullOrEmpty())
return
//预算需要绘制的组的开始和结尾index,避免不必要的计算浪费性能
// caculateIndex()
//计算Y轴刻度值
calculateYMark(false)
//计算x轴刻度值
caculateXMark()
val yMarkSpace = (rectChart.bottom - rectChart.top) / (yAxisMark.lableNum - 1)
paintEffect.style = Paint.Style.STROKE
paintEffect.strokeWidth = yAxisMark.lineWidth.toFloat()
paintEffect.color = yAxisMark.lineColor
paintText.textSize = yAxisMark.textSize.toFloat()
paintText.color = yAxisMark.textColor
val effects: PathEffect = DashPathEffect(floatArrayOf(15f, 6f, 15f, 6f), 0f)
paintEffect.pathEffect = effects
paint.style = Paint.Style.STROKE
paint.strokeWidth = yAxisMark.lineWidth.toFloat()
paint.color = yAxisMark.lineColor
for (i in 0 until yAxisMark.lableNum) {
/**绘制横向线 */
canvas!!.drawLine(rectChart.left, rectChart.bottom - yMarkSpace * i,
rectChart.right, rectChart.bottom - yMarkSpace * i, paint)
/**绘制y刻度 */
val text = yAxisMark.getMarkText(yAxisMark.cal_mark_min + i * yAxisMark.cal_mark)
canvas!!.drawText(text,
rectChart.left - yAxisMark.textSpace - FontUtil.getFontlength(paintText, text),
rectChart.bottom - yMarkSpace * i - yAxisMark.textHeight / 2 + yAxisMark.textLead, paintText)
}
//绘制Y轴单位
if (!TextUtils.isEmpty(yAxisMark.unit)) {
canvas!!.drawText(yAxisMark.unit,
rectChart.left - yAxisMark.textSpace - FontUtil.getFontlength(paintText, yAxisMark.unit),
//y = 图表顶部 - 单位文字距离 - 单位文字高度 + 最上方y刻度高度/2
rectChart.top - yAxisMark.textSpace - yAxisMark.textHeight * 3 / 2 + yAxisMark.textLead, paintText)
}
val rect = RectF()
paint.style = Paint.Style.FILL
for(index:Int in startIndex..endIndex){
//计算柱体x,y坐标
if(allDataWidth*scalex<=rectChart.width()){
rect.left = rectChart.left + (rectChart.width() - allDataWidth*scalex)/2 +
index * oneDataWidth*scalex + barSpace*scalex
// LogUtil.v(TAG, "数据不够填充表,居中显示,当前数据x坐标 ${rect.left}")
}else{
rect.left = scrollx + rectChart.left + index * oneDataWidth*scalex + barSpace*scalex
// LogUtil.v(TAG, "数据超过表,当前数据x坐标 ${rect.left}")
}
rect.right = rect.left + barWidth*scalex
rect.bottom = rectChart.bottom
rect.top = rectChart.bottom
//★★★顺便为焦点数据设置x坐标,方便下一步绘制焦点
focusData?.let {
if(focusIndex == index)
it.point.x = rect.left + barWidth*scalex/2
}
// LogUtil.d(TAG, "$i 绘制:"+_datas[i].valuey)
/**绘制柱状 */
//过滤掉绘制区域外的柱
var barLayer:Int? = null
if(index == startIndex || index == endIndex){
/**
* Canvas有两种坐标系:
* 1. Canvas自己的坐标系:(0,0,canvas.width,canvas.height),它是固定不变的
* 2. 绘图坐标系:用于绘制,通过Matrix让Canvas平移translate,旋转rotate,缩放scale 等时实际上操作的是绘图坐标系
* 由于绘图坐标系中Matrix的改变是不可逆的,所以产生了状态栈和Layer栈,它们分别运用于save方法和saveLayer方法,使得绘图坐标系恢复到保存时的状态
* 1. 状态栈:save()、restore()保存和还原变换操作Matrix以及Clip剪裁,也可以restoretoCount()直接还原到对应栈的保存状态
* 2. Layer栈:saveLayer()时会新建一个透明图层(离屏Bitmap-离屏缓冲),并且将saveLayer之前的一些Canvas操作延续过来,
* 后续的绘图操作都在新建的layer上面进行,当调用restore或者restoreToCount时更新到对应的图层和画布上
*
* 需要注意的是saveLayer会造成过渡绘制,可以考虑用 canvas?.save() canvas?.clipRect(rectChart)组合代替
*/
/* barLayer = canvas?.saveLayer(rectChart.left, rectChart.top, rectChart.right,
rectChart.bottom*//* + xAxisMark.textSpace + xAxisMark.textHeight*//*
, paint, Canvas.ALL_SAVE_FLAG)*/
//裁剪画布,避免x刻度超出
barLayer = canvas?.save()
canvas?.clipRect(rectChart)
}
for(vindex : Int in _datas[index].valuey.indices){
paint.color = barColor[vindex]
if (_datas[index].valuey[vindex] != null) {
val vh = rectChart.height() / (yAxisMark.cal_mark_max - yAxisMark.cal_mark_min) *
(_datas[index].valuey[vindex] - yAxisMark.cal_mark_min) * chartAnimValue
rect.top -= vh
canvas?.drawRect(rect, paint)
rect.bottom = rect.top
}
}
if(barLayer!=null)
canvas?.restoreToCount(barLayer)//还原画布,将柱子更新到画布上
/**绘制x坐标 */
//测试:绘制索引
// canvas?.drawText("$i", rect.left + (barWidth*scalex) / 2 - FontUtil.getFontlength(paintText, "$i") / 2, xAxisMark.drawPointY, paintText)
//从第一条数据开始每隔xIndexSpace绘制一个x刻度
if((index - startIndex) % xIndexSpace == 0){
val x = rect.left + (barWidth*scalex) / 2 - FontUtil.getFontlength(paintText, _datas[index].valuex) / 2
//过滤掉超出图表范围的x值绘制,通常是第一条和最后一条
if(x < paddingLeft || x+FontUtil.getFontlength(paintText, _datas[index].valuex) > measuredWidth - paddingRight)
continue
canvas?.drawText(_datas[index].valuex, x,xAxisMark.drawPointY, paintText)
}
}
drawFocus(canvas)
}
/**绘制焦点 */
private val focusLineColor = Color.parseColor("#319A5A")
private val focusLineSize = DensityUtil.dip2px(context, 1f)
private val foucsRectTextSpace = DensityUtil.dip2px(context, 3f)
private val foucsRectSpace = DensityUtil.dip2px(context, 6f)
private fun drawFocus(canvas: Canvas?) {
if (null == focusData || null==canvas) return
if(focusData!!.point.x<rectChart.left ||focusData!!.point.x>rectChart.right) //上次获取的焦点因为滑出矩形,不显示
return
//绘制竖直虚线
val effects: PathEffect = DashPathEffect(floatArrayOf(8f, 5f, 8f, 5f), 0f)
paintEffect.style = Paint.Style.STROKE
paintEffect.strokeWidth = focusLineSize.toFloat()
paintEffect.color = focusLineColor
paintEffect.pathEffect = effects
val path = Path()
path.moveTo(focusData!!.point.x, rectChart.bottom)
path.lineTo(focusData!!.point.x, rectChart.top)
canvas.drawPath(path, paintEffect)
//面板
val showLeft: Boolean = focusData!!.point.x - rectChart.left > (rectChart.right - rectChart.left) / 2
val rect = RectF(
if (showLeft) focusData!!.point.x - foucsRectWidth - 30 else focusData!!.point.x + 30,
rectChart.top /*+ (rectChart.bottom - rectChart.top)/2 - foucsRectHeight/2*/,
if (showLeft) focusData!!.point.x - 30 else focusData!!.point.x + foucsRectWidth + 30,
rectChart.top + foucsRectHeight /*+ (rectChart.bottom - rectChart.top)/2 + foucsRectHeight/2*/
)
paint.style = Paint.Style.FILL
paint.color = Color.WHITE
paint.alpha = 230
canvas.drawRect(rect, paint)
//面板中的文字
//2020-10-16 06:00
//零序电流:15.2KW
//A相电流:15.2KW
//A相电流:15.2KW
//A相电流:15.2KW
var text = ""
var top: Float = rect.top + foucsRectSpace
val currentPoint = PointF()
val radius = DensityUtil.dip2px(context, 2.5f).toFloat()
focusPanelText?.let {
for (i in it.indices) {
if (it[i].show) {
paintText.textSize = it[i].textSize.toFloat()
paintText.color = it[i].textColor
if (i == 0) { //x轴数据
text = focusData!!.data.valuex
} else {
top += foucsRectTextSpace.toFloat()
text = (it[i].text +
(if (focusData!!.data.valuey[i - 1] == null) ""
else YAxisMark.formattedDecimal(focusData!!.data.valuey[i - 1].toDouble(), 2))
+ yAxisMark.unit)
}
canvas.drawText(text,
rect.left + foucsRectSpace,
top + FontUtil.getFontLeading(paintText), paintText)
top += FontUtil.getFontHeight(paintText)
}
}
}
}
/***************************事件👇👇👇***************************/
private var oldScalex = 0f
override fun onScaleBegin(detector: ScaleGestureDetector) {
// val width = -scrollx + (detector.focusX - rectChart.left)
// val zs = (width / (oneDataWidth*scalex)).toInt()
// val ys = width % (oneDataWidth*scalex)
// focusIndex = zs + if(ys>(barWidth/2+barSpace)*scalex)1 else 0
oldScalex = scalex
//设置焦点为两个手指中间
focusPoint.x = detector.focusX
focusPoint.y = detector.focusY
onFocusTouch(focusPoint)
LogUtil.i(TAG, "缩放开始了,焦点索引为$focusIndex") // 缩放因子
}
private var scaleXMax = 3f //X轴方向最大放大倍数,需要根据柱子是否能填充图表计算
override fun onScale(detector: ScaleGestureDetector, beginScrollx: Float) {
scalex *= detector.scaleFactor
LogUtil.e(TAG, "--------------------当前缩放值$scalex 缩放${detector.scaleFactor} 缩放之后${scalex*detector.scaleFactor}")
//缩放范围约束
scalex = scalex.coerceAtMost(scaleXMax)
scalex = scalex.coerceAtLeast(1f)
LogUtil.e(TAG, "--------------------最终值$scalex ")
//重新计算最大偏移量
if(allDataWidth * scalex > rectChart.width()){
scrollXMax = rectChart.width() - allDataWidth * scalex
//为了保证焦点对应的点位置不变,是使用公式: beginScrollx + rectChart.left + focusIndex*beginPointWidth = scrollx + rectChart.left + focusIndex*pointWidth
scrollx = beginScrollx + focusIndex * (oneDataWidth * oldScalex - oneDataWidth*scalex)
scrollx = Math.min(scrollx, 0f)
scrollx = Math.max(scrollXMax, scrollx)
LogUtil.i(TAG, "缩放后偏移:"+scrollx);
}else{
scrollXMax = 0f //数据不能填充时,居中展示
scrollx = 0f
}
}
override fun onFocusTouch(point: PointF?) {
try {
focusData = null
point?.let {
if (!_datas.isNullOrEmpty()) {
val scaleOneWidth = oneDataWidth*scalex
val allWidth = allDataWidth*scalex
//避免滑出
var left = rectChart.left
var right = rectChart.right
if(allWidth<rectChart.width()){
left = rectChart.left + (rectChart.width()-allWidth)/2
right = left + allWidth
}
LogUtil.e(TAG, "========焦点位置${point.x}")
point.x = Math.max(point.x, left)
point.x = Math.min(point.x, right)
LogUtil.e(TAG, "========左右范围:${left}*${right} 焦点纠正后$point.x")
val width = if(allWidth<rectChart.width()){
point.x - left
}else{
-scrollx + (point.x - left)
}
//获取焦点对应的数据的索引
focusIndex = (width / scaleOneWidth).toInt() - 1 //计算的是索引(从0开始),所以-1
LogUtil.e(TAG, "========单个宽度$scaleOneWidth 宽度$width 整数索引$focusIndex")
if(width % scaleOneWidth > barSpace*scalex/2) {
focusIndex += 1
LogUtil.e(TAG, "========焦点在下一个点范围了:$focusIndex")
}
focusIndex = Math.max(0, Math.min(focusIndex, _datas.size - 1))
LogUtil.e(TAG, "========焦点索引:$focusIndex")
focusData = FocusData(_datas[focusIndex], it)
}
}
// postInvalidate()
} catch (e: Exception) {
e.printStackTrace()
}
}
/***************************事件👆👆👆***************************/
/***************************2. 子类重写👆👆👆***************************/
/***************************3. 特殊👇👇👇***************************/
private var startIndex = 0
private var endIndex = 0
/**计算当前缩放、移动状态下,需要绘制的数据的起始和结束索引*/
private fun caculateIndex(){
//预算需要绘制的组的开始和结尾index,避免不必要的计算浪费性能
val scaleOneWidth = oneDataWidth*scalex
if(allDataWidth*scalex<=rectChart.width()){
startIndex = 0
endIndex = _datas.size-1
}else{
startIndex = (-scrollx / scaleOneWidth).toInt()
endIndex = ((-scrollx + rectChart.width()) / scaleOneWidth).toInt() - 1
// LogUtil.w(TAG, "总宽度:${-scrollx + rectChart.width()} 当前状态下一个柱子及间隙宽度$scaleOneWidth 最后一条数据索引取整$endIndex")
val nextVisible = (-scrollx + rectChart.width()) % scaleOneWidth>=barSpace*scalex
endIndex += if(nextVisible)1 else 0
// LogUtil.w(TAG, "取余:${(-scrollx + rectChart.width()) % scaleOneWidth} 柱子宽度${barSpace*scalex} 是否可见$nextVisible 结束索引$endIndex")
endIndex = endIndex.coerceAtMost(_datas.size - 1)
}
}
/**y值累加*/
private fun getTotalValuey(data : MultipartBarData) : Float{
var valuey = 0f
for(v in data.valuey)
valuey+=v
return valuey
}
/**获取startIndex~endIndex的数据最大最小值,并根据需要显示几个y刻度计算出递增值*/
private fun calculateYMark(all:Boolean) {
val redundance = 1.1f //y轴最大和最小值冗余
yAxisMark.cal_mark_max = -Float.MAX_VALUE //Y轴刻度最大值
yAxisMark.cal_mark_min = Float.MAX_VALUE //Y轴刻度最小值
var startIdx = 0
var endIdx = _datas.size-1
if(!all){
caculateIndex()
startIdx = startIndex
endIdx = endIndex
}
for(index in startIdx..endIdx){
val valuey = getTotalValuey(_datas[index])
yAxisMark.cal_mark_max = Math.max(yAxisMark.cal_mark_max, valuey)
yAxisMark.cal_mark_min = Math.min(yAxisMark.cal_mark_min, valuey)
}
LogUtil.w(TAG, "$startIdx ~ $endIdx 真实最小最大值:" + yAxisMark.cal_mark_min + " " + yAxisMark.cal_mark_max)
//只有一个点的时候
if (yAxisMark.cal_mark_min == yAxisMark.cal_mark_max) {
when {
yAxisMark.cal_mark_min > 0 -> {
yAxisMark.cal_mark_min = 0f
}
yAxisMark.cal_mark_min == 0f -> {
yAxisMark.cal_mark_max = 1f
}
yAxisMark.cal_mark_min < 0 -> {
yAxisMark.cal_mark_max = 0f
}
}
}
if (yAxisMark.markType == MarkType.Integer) {
val min = if (yAxisMark.cal_mark_min > 0) 0 else yAxisMark.cal_mark_min.toInt()
val max = yAxisMark.cal_mark_max.toInt()
var mark = (max - min) / (yAxisMark.lableNum - 1) + if ((max - min) % (yAxisMark.lableNum - 1) > 0) 1 else 0
mark = if (mark == 0) 1 else mark //最大值和最小值都为0的情况
val first = (mark.toString() + "").substring(0, 1).toInt() + 1
if ((mark.toString() + "").length == 1) {
//YMARK = 1、2、5、10
mark = if (mark == 3 || mark == 4 || mark == 6 || mark == 7 || mark == 8 || mark == 9) if (mark == 3 || mark == 4) 5 else 10 else mark
} else if ((mark.toString() + "").length == 2) {
mark = first * 10
} else if ((mark.toString() + "").length == 3) {
mark = first * 100
} else if ((mark.toString() + "").length == 4) {
mark = first * 1000
} else if ((mark.toString() + "").length == 5) {
mark = first * 10000
} else if ((mark.toString() + "").length == 6) {
mark = first * 100000
}
yAxisMark.cal_mark_min = 0f
yAxisMark.cal_mark_max = mark * (yAxisMark.lableNum - 1).toFloat()
yAxisMark.cal_mark = mark.toFloat()
} else { //Float //Percent
yAxisMark.cal_mark_max = if (yAxisMark.cal_mark_max < 0) yAxisMark.cal_mark_max / redundance else yAxisMark.cal_mark_max * redundance
// yAxisMark.cal_mark_min = if (yAxisMark.cal_mark_min < 0) yAxisMark.cal_mark_min * redundance else yAxisMark.cal_mark_min / redundance
yAxisMark.cal_mark_min = 0f
yAxisMark.cal_mark = (yAxisMark.cal_mark_max - yAxisMark.cal_mark_min) / (yAxisMark.lableNum - 1)
}
//小数点位
if (yAxisMark.digits == 0) {
val mark = (yAxisMark.cal_mark_max - yAxisMark.cal_mark_min) / (yAxisMark.lableNum - 1)
if (mark < 1) {
val pattern = "[1-9]"
val p = Pattern.compile(pattern)
val m = p.matcher(mark.toString() + "") // 获取 matcher 对象
m.find()
val index = m.start()
yAxisMark.digits = index - 1
LogUtil.w(TAG, mark.toString() + "第一个大于0的数字位置:" + index + " 保留小数位数:" + yAxisMark.digits)
}
}
// LogUtil.w(TAG, "最终最小最大值:" + yAxisMark.cal_mark_min + " " + yAxisMark.cal_mark_max + " " + yAxisMark.cal_mark)
}
/**根据startIndex~endIndex计算x标签间隔数量*/
//从当前绘制的第一条数据开始,每隔多少展示一个x标签
private var xIndexSpace: Int = 0
private fun caculateXMark() {
caculateIndex()
paintText.textSize = xAxisMark.textSize.toFloat()
//计算当前显示的数据的x轴文字长度最大值
var xTextMaxLength = 0f
for(index in startIndex..endIndex){
xTextMaxLength = xTextMaxLength.coerceAtLeast(FontUtil.getFontlength(paintText, _datas[index].valuex))
}
//当前绘制宽度 / x刻度最长
var xNumber = ((allDataWidth * scalex).coerceAtMost(rectChart.width()) / xTextMaxLength).toInt()
val dataNumber = endIndex - startIndex + 1
LogUtil.e(TAG, "绘制的数据条数${endIndex-startIndex+1} X轴文字最长长度$xTextMaxLength 理论最多可显示$xNumber 个")
xNumber = Math.min(xNumber, xAxisMark.lableNum)
when(xNumber){
1->xIndexSpace = endIndex - startIndex + 10 //只显示第一个
2->xIndexSpace = endIndex - startIndex //显示第一个和最后一个
3->{ //取中点
when(dataNumber % 2){
0->xIndexSpace = (dataNumber-1)/2 //数据条数为偶数 变为奇数取中点
1->xIndexSpace = dataNumber/2 //数据条数为奇数取中点
}
}
else->{
xIndexSpace = when(dataNumber%xNumber){
0-> dataNumber/xNumber //数据条数 整除 lable数 时,取除数
else-> dataNumber/xNumber + 1 //不能整除时 +1
}
}
}
}
/***************************3. 特殊👆👆👆***************************/
}
@@ -0,0 +1,24 @@
package com.openxu.hkchart.config
import android.content.Context
import android.graphics.Color
import com.openxu.utils.DensityUtil
/**
* Author: openXu
* Time: 2021/5/9 12:14
* class: MultipartBarConfig
* Description:
*/
class BarChartConfig(context: Context) : ChartConfigBase(){
var scrollAble = true //是否支持滚动
var barWidth = DensityUtil.dip2px(context, 15f).toFloat() //柱宽度
var barSpace = DensityUtil.dip2px(context, 1f).toFloat() //双柱间的间距
var groupSpace = DensityUtil.dip2px(context, 25f).toFloat() //一组柱之间的间距(只有scrollAble==true时才生效)
var barColor = intArrayOf(
Color.parseColor("#f46763"),
Color.parseColor("#3cd595"),
Color.parseColor("#4d7bff")) //柱颜色
}
class Bar(val valuex: String,val valuey: List<Float>)
@@ -0,0 +1,38 @@
package com.openxu.hkchart.config
/**
* Author: openXu
* Time: 2021/5/9 11:58
* class: DisplayScheme
* Description: 首次展示模式
*/
enum class DisplayScheme {
SHOW_ALL, //第一次全部显示
SHOW_BEGIN, //从第一条数据开始展示固定条数
SHOW_END //从最后一条数据开始展示固定条数
}
/**
* Author: openXu
* Time: 2021/5/9 11:58
* class: ChartConfigBase
* Description: 图表配置基类
*/
open class ChartConfigBase{
//默认第一次展示全部数据
open var displayScheme:DisplayScheme = DisplayScheme.SHOW_ALL
//柱颜色
open var xAxisMark: XAxisMark?=null
open var yAxisMark: YAxisMark?=null
//是否展示动画
open var showAnim = true
//设置数据总数,可选并且设置的数据总数可以大于data集合总数,如果没有设置默认dataTotalCount=_data.size
//有些图表的数据总数和设置的数据可能不同,比如股票分时图:总数据量 = 一天交易时间分钟数,当前返回的数据量 = 开盘时间到当前时间分钟的数据
open var dataTotalCount : Int = -1
}
@@ -0,0 +1,28 @@
package com.openxu.hkchart.config
import android.content.Context
import android.graphics.Color
import android.graphics.Region
import com.openxu.utils.DensityUtil
/**
* Author: openXu
* Time: 2021/5/9 12:14
* class: MultipartBarConfig
* Description:
*/
class HorizontalBarConfig(context: Context) : ChartConfigBase(){
var barWidth = DensityUtil.dip2px(context, 26f)
var barSpace = DensityUtil.dip2px(context, 10f)
var barColor = intArrayOf(
Color.parseColor("#F46863"),
Color.parseColor("#2DD08A"),
Color.parseColor("#567CF6"),
Color.parseColor("#F5B802"),
Color.parseColor("#CC71F7"))
}
class HBar(val lable: String,val value: Float){
}
@@ -0,0 +1,30 @@
package com.openxu.hkchart.config
import android.content.Context
import android.graphics.Color
import com.openxu.hkchart.data.FocusPanelText
import com.openxu.utils.DensityUtil
/**
* Author: openXu
* Time: 2021/5/9 12:14
* class: MultipartBarConfig
* Description:
*/
class LineChartConfig(context: Context) : ChartConfigBase(){
var lineType : LineType = LineType.BROKEN
var lineWidth = DensityUtil.dip2px(context, 1f).toFloat()
var lineColor = intArrayOf(
Color.parseColor("#f46763"),
Color.parseColor("#3cd595"),
Color.parseColor("#4d7bff"),
Color.parseColor("#4d7bff"))
var pageShowNum = 0 //第一次页面总数据量 没有设置展示数据量,则默认为全部展示
/**设置焦点面板显示内容*/
var focusPanelText: Array<FocusPanelText>? = null
}
enum class LineType {
CURVE, //曲线
BROKEN //折线
}
@@ -0,0 +1,13 @@
package com.openxu.hkchart.config;
/**
* Author: openXu
* Time: 2020/11/20 11:25
* class: MarkType
* Description:
*/
public enum MarkType {
Integer,
Float,
Percent
}
@@ -0,0 +1,37 @@
package com.openxu.hkchart.config
import android.content.Context
import android.graphics.Color
import com.openxu.hkchart.data.FocusPanelText
import com.openxu.utils.DensityUtil
/**
* Author: openXu
* Time: 2021/5/9 12:14
* class: MultipartBarConfig
* Description:
*/
class MultipartBarConfig(context: Context) : ChartConfigBase(){
//默认柱宽度15dp
var barWidth = DensityUtil.dip2px(context, 15f).toFloat()
//默认柱间的间距占比,间距 = barWidth*spacingRatio
var spacingRatio = 1f
//柱颜色
var barColor: IntArray = intArrayOf(
Color.parseColor("#f46763"),
Color.parseColor("#3cd595"),
Color.parseColor("#4d7bff"))
/**设置焦点面板显示内容*/
var focusPanelText: Array<FocusPanelText>? = null
}
/**
* Author: openXu
* Time: 2021/5/9 12:14
* class: MultipartBarData
* Description: 数据
*/
class MultipartBarData(val valuey: List<Float>, val valuex: String)
@@ -0,0 +1,64 @@
package com.openxu.hkchart.config;
import android.content.Context;
import android.graphics.Color;
import com.openxu.utils.DensityUtil;
/**
* 坐标轴刻度标签
*/
public class XAxisMark {
/**可设置*/
public int textSize; //设置坐标文字大小
public int textColor; //设置坐标文字颜色
public int textSpace; //设置坐标字体与横轴的距离
//2选1设置
public int lableNum = 5;
public int splitSubLen = -1;
public String[] lables;
// public int lineWidth;
// public int lineColor;
/**计算*/
public float textHeight, textLead, drawPointY;
private XAxisMark() {
}
public static class Builder{
private XAxisMark axisMark;
public Builder(Context context) {
axisMark = new XAxisMark();
axisMark.textSize = DensityUtil.sp2px(context, 10);
axisMark.textColor = Color.parseColor("#333333");
axisMark.textSpace = DensityUtil.dip2px(context, 5);
// axisMark.lineWidth = DensityUtil.dip2px(context, 0.7f);
// axisMark.lineColor = Color.parseColor("#5E5E5E");
}
public Builder splitSubLen(int splitSubLen) {
axisMark.splitSubLen = splitSubLen;
return this;
}
public Builder textSize(int textSize) {
axisMark.textSize = textSize;
return this;
}
public Builder textColor(int textColor) {
axisMark.textColor = textColor;
return this;
}
public Builder lables(String[] lables) {
axisMark.lables = lables;
axisMark.lableNum = lables.length;
return this;
}
public Builder lableNum(int lableNum) {
axisMark.lableNum = lableNum;
return this;
}
public XAxisMark build(){
return axisMark;
}
}
}
@@ -0,0 +1,158 @@
package com.openxu.hkchart.config;
import android.content.Context;
import android.graphics.Color;
import android.graphics.PointF;
import android.graphics.Typeface;
import com.openxu.utils.DensityUtil;
import java.math.RoundingMode;
import java.text.NumberFormat;
/**
* 坐标轴刻度标签
*/
public class YAxisMark {
private String TAG = "YAxisMark";
/**可设置*/
public int textSize; //设置坐标文字大小
public int textColor; //设置坐标文字颜色
public int textSpace; //设置坐标字体与横轴的距离
public Typeface numberTypeface = Typeface.DEFAULT;
public int lineWidth;
public int lineColor;
public int lableNum;
public MarkType markType;
public int digits; //小数点保留位数
public String unit = ""; //单位 KW
/**自动计算*/
// public List<MarkPoint> markPointList = new ArrayList<>();
public float textHeight, textLead;
public float cal_mark; //轴刻度间隔
public float cal_mark_max = Float.MIN_VALUE; //Y轴刻度最大值
public float cal_mark_min = Float.MAX_VALUE; //Y轴刻度最小值
private YAxisMark() {
}
public static class Builder{
private YAxisMark axisMark;
public Builder(Context context) {
axisMark = new YAxisMark();
axisMark.textSize = DensityUtil.sp2px(context, 10);
axisMark.textColor = Color.parseColor("#BCBCBC");
axisMark.textSpace = DensityUtil.dip2px(context, 5);
axisMark.lineWidth = DensityUtil.dip2px(context, 0.7f);
axisMark.lineColor = Color.parseColor("#e7eaef");
axisMark.lableNum = 5;
axisMark.markType = MarkType.Integer;
}
public Builder numberTypeface(Typeface numberTypeface) {
axisMark.numberTypeface = numberTypeface;
return this;
}
public Builder textSize(int textSize) {
axisMark.textSize = textSize;
return this;
}
public Builder textColor(int textColor) {
axisMark.textColor = textColor;
return this;
}
public Builder lineWidth(int lineWidth) {
axisMark.lineWidth = lineWidth;
return this;
}
public Builder lableNum(int lableNum) {
axisMark.lableNum = lableNum;
return this;
}
public Builder markType(MarkType markType) {
axisMark.markType = markType;
return this;
}
public Builder digits(int digits) {
axisMark.digits = digits;
return this;
}
public Builder unit(String unit) {
axisMark.unit = unit;
return this;
}
public YAxisMark build(){
return axisMark;
}
}
public String getMarkText(float value){
switch (markType){
case Integer:
return (long)value+"";
case Float:
return formattedDecimal(value, digits);
case Percent:
return formattedDecimalToPercentage(value, digits);
}
return value+"";
}
public static class MarkPoint {
public float value; //对应轴上的值
public PointF point; //绘制的点的坐标
public MarkPoint(float value, PointF point) {
this.value = value;
this.point = point;
}
}
/**
* double 类型转换成保留2位小数百分数 0.1 -> 10.00%
* @param decimal
* @return
*/
public static String formattedDecimalToPercentage(double decimal, int digits){
//获取格式化对象
NumberFormat format = NumberFormat.getPercentInstance();
// format.setGroupingUsed(true);
//设置百分数精确度2即保留两位小数
format.setMinimumFractionDigits(digits);
return format.format(decimal);
}
/**
* double 类型转换成保留2位小数 0.1 -> 0.10
* @param decimal
* @return
*/
public static String formattedDecimal(double decimal, int digits){
NumberFormat nf = NumberFormat.getNumberInstance();
nf.setMaximumFractionDigits(digits);
/*
* setMinimumFractionDigits设置成2
* 如果不这么做,那么当value的值是100.00的时候返回100
* 而不是100.00
*/
// nf.setMinimumFractionDigits(digits);
nf.setRoundingMode(RoundingMode.HALF_UP);
/*
* 如果想输出的格式用逗号隔开,可以设置成true
*/
nf.setGroupingUsed(false);
return nf.format(decimal);
}
}
@@ -0,0 +1,12 @@
package com.openxu.hkchart.data;
/**
* (共用)数据转换器,将服务器返回的数据转换为对应图表需要的数据格式
* @param <T>
* @param <R>
*/
public interface DataTransform <T, R>{
R transform(T t);
}
@@ -0,0 +1,12 @@
package com.openxu.hkchart.data
import android.graphics.PointF
/**
* Author: openXu
* Time: 2021/5/11 10:50
* class: FocusData
* Description: (共用)焦点数据
*/
class FocusData<T>(val data: T, val point: PointF) {
}
@@ -0,0 +1,21 @@
package com.openxu.hkchart.data;
/**
* Author: openXu
* Time: 2020/11/11 15:56
* class: FocusPanelText
* Description: (共用)焦点文字设置
*/
public class FocusPanelText {
public boolean show;
public int textSize; //设置坐标文字大小
public int textColor; //设置坐标文字颜色
public String text;
public FocusPanelText(boolean show, int textSize, int textColor, String text) {
this.show = show;
this.textSize = textSize;
this.textColor = textColor;
this.text = text;
}
}
@@ -0,0 +1,55 @@
package com.openxu.hkchart.data;
/**
* LineChart 的数据
*/
public class LinePoint {
private float valuey ; //y轴值
private String valuex; //x轴刻度值
private String valuexfocus; //x轴焦点值,如果为null,则默认为valuex
// private PointF point;
public LinePoint(String valuex, float valuey) {
this.valuey = valuey;
this.valuex = valuex;
}
public LinePoint(String valuex, String valuexfocus, float valuey) {
this.valuey = valuey;
this.valuex = valuex;
this.valuexfocus = valuexfocus;
}
public String getValuexfocus() {
return valuexfocus;
}
public void setValuexfocus(String valuexfocus) {
this.valuexfocus = valuexfocus;
}
// public PointF getPoint() {
// return point;
// }
//
// public void setPoint(PointF point) {
// this.point = point;
// }
public float getValuey() {
return valuey;
}
public void setValuey(float valuey) {
this.valuey = valuey;
}
public String getValuex() {
return valuex;
}
public void setValuex(String valuex) {
this.valuex = valuex;
}
}
@@ -0,0 +1,641 @@
package com.openxu.hkchart.line
import android.content.Context
import android.graphics.*
import android.text.TextUtils
import android.util.AttributeSet
import android.util.Log
import android.view.ScaleGestureDetector
import com.openxu.hkchart.BaseChart
import com.openxu.hkchart.config.*
import com.openxu.hkchart.data.FocusData
import com.openxu.hkchart.data.FocusPanelText
import com.openxu.hkchart.data.LinePoint
import com.openxu.utils.DensityUtil
import com.openxu.utils.FontUtil
import com.openxu.utils.LogUtil
import com.openxu.utils.NumberFormatUtil
import java.util.*
import java.util.regex.Pattern
/**
* Author: openXu
* Time: 2021/5/11 12:43
* class: LineChart
* Description:
*/
class LineChart : BaseChart<MutableList<LinePoint?>> {
constructor(context: Context) :this(context, null)
constructor(context: Context, attrs: AttributeSet?) :this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyle: Int):super(context, attrs, defStyle){
}
/***************************1. API👇👇👇***************************/
/**设置数据*/
private var _datas = mutableListOf<MutableList<LinePoint>>()
var datas: MutableList<MutableList<LinePoint>>
get() {return _datas}
set(value) {
_datas.clear()
_datas.addAll(value)
initial()
chartConfig?.let {
if (it.showAnim) chartAnimStarted = false
}
loading = false
}
/***************************1. API👆👆👆***************************/
/***************************2. 子类重写👇👇👇***************************/
/**配置*/
private lateinit var yAxisMark : YAxisMark
private lateinit var xAxisMark : XAxisMark
private var lineType : LineType = LineType.BROKEN
private var lineWidth = DensityUtil.dip2px(context, 1f).toFloat()
private var lineColor = intArrayOf(
Color.parseColor("#f46763"),
Color.parseColor("#3cd595"),
Color.parseColor("#4d7bff"),
Color.parseColor("#4d7bff"))
private var pageShowNum = 0 //第一次页面总数据量 没有设置展示数据量,则默认为全部展示
private var dataTotalCount : Int = -1
/**设置焦点面板显示内容*/
private var focusPanelText: Array<FocusPanelText>? = null
/**初步计算*/
private var maxPointNum = 0 //点最多的线的点数量
private var maxPointIndex = 0//点最多的线的索引
private var pointWidthMin= 0f //最初的每个点占据的宽度,最小缩放值
private var pointWidthMax = 0f //最初的每个点占据的宽度,最大放大值
private var pointWidth = 0f //每个点占据的宽度
override fun initial():Boolean{
if(super.initial()) return true
if(_datas.isNullOrEmpty()) return true
if(chartConfig==null)
throw RuntimeException("---------请配置图表")
var config = chartConfig as LineChartConfig
xAxisMark = config.xAxisMark?:XAxisMark.Builder(context)
.lableNum(5)
.build()
yAxisMark = config.yAxisMark?:YAxisMark.Builder(context)
.lableNum(6)
.markType(MarkType.Integer)
.unit("")
.build()
lineType = config.lineType
lineWidth = config.lineWidth
lineColor = config.lineColor
pageShowNum = config.pageShowNum
dataTotalCount = config.dataTotalCount
if(dataTotalCount<0)
dataTotalCount = datas.size
focusPanelText = config.focusPanelText
maxPointNum = 0
maxPointIndex = 0
for (i in 0 until _datas.size) {
if (maxPointNum <= _datas[i].size) {
maxPointNum = _datas[i].size
maxPointIndex = i
}
}
Log.w(TAG, "点最多的线索引为:$maxPointIndex 点数:$maxPointNum")
paintText.textSize = xAxisMark.textSize.toFloat()
xAxisMark.textHeight = FontUtil.getFontHeight(paintText)
xAxisMark.textLead = FontUtil.getFontLeading(paintText)
Log.w(TAG, "x轴字体高度:" + xAxisMark.textHeight.toString() + " textLead:" + xAxisMark.textLead.toString() + " xAxisMark.textSpace" + xAxisMark.textSpace)
//确定图表最下放绘制位置
rectChart.bottom = rectDrawBounds.bottom - xAxisMark.textHeight - xAxisMark.textSpace
Log.w(TAG, "rectDrawBounds.bottom:" + rectDrawBounds.bottom + " rectChart.bottom=" + rectChart.bottom)
xAxisMark.drawPointY = rectChart.bottom + xAxisMark.textSpace + xAxisMark.textLead
calculateYMark()
paintText.textSize = yAxisMark.textSize.toFloat()
yAxisMark.textHeight = FontUtil.getFontHeight(paintText)
yAxisMark.textLead = FontUtil.getFontLeading(paintText)
Log.w(TAG, "y轴字体高度:" + yAxisMark.textHeight.toString() + " textLead:" + yAxisMark.textLead)
var maxLable = if (yAxisMark.getMarkText(yAxisMark.cal_mark_max).length > yAxisMark.getMarkText(yAxisMark.cal_mark_min).length) yAxisMark.getMarkText(yAxisMark.cal_mark_max) else yAxisMark.getMarkText(yAxisMark.cal_mark_min)
LogUtil.w(TAG, "Y刻度最大字符串:$maxLable")
if (!TextUtils.isEmpty(yAxisMark.unit)) maxLable = if (yAxisMark.unit.length > maxLable.length) yAxisMark.unit else maxLable
LogUtil.w(TAG, "Y刻度最大字符串:$maxLable")
rectChart.left = paddingLeft + yAxisMark.textSpace + FontUtil.getFontlength(paintText, maxLable)
rectChart.top = rectDrawBounds.top + yAxisMark.textHeight / 2 +
if (TextUtils.isEmpty(yAxisMark.unit)) 0f else yAxisMark.textHeight + yAxisMark.textSpace
rectChart.right = rectDrawBounds.right
//没有设置展示数据量,则默认为全部展示
if (pageShowNum <= 0) pageShowNum = maxPointNum
if (maxPointNum < pageShowNum) //最多的点小于需要显示的点,则全部展示
pageShowNum = maxPointNum
Log.w(TAG, "pageShowNum=${pageShowNum}")
pointWidthMin = rectChart.width() / (maxPointNum - 1) //缩小到全部显示
pointWidth = rectChart.width() / (pageShowNum - 1)
pointWidthMax = rectChart.width() / 4 //最大只能放大到每个标签显示5个点
// pointWidthMax = rectChart.width() / (xAxisMark.lableNum-1) / 5; //最大只能放大到每个标签显示5个点
// pointWidthMax = rectChart.width() / (xAxisMark.lableNum-1) / 5; //最大只能放大到每个标签显示5个点
Log.w(TAG, "缩放最小最大宽度=$pointWidthMin $pointWidthMax")
//数据没有展示完,说明可以滚动
scrollXMax = 0f
if (pageShowNum < maxPointNum) scrollXMax = -(pointWidth * (maxPointNum - 1) - rectChart.width()) //最大滚动距离,是一个负值
scrollx = if (config.displayScheme==DisplayScheme.SHOW_BEGIN) 0f else scrollXMax
caculateXMark()
focusPanelText?.let {
//计算焦点面板
//2020-10-16 06:00
//零序电流:15.2KW
//A相电流:15.2KW
//A相电流:15.2KW
//A相电流:15.2KW
foucsRectWidth = 0f
foucsRectHeight = foucsRectSpace * 2f
var text: String
maxLable = (if (yAxisMark.getMarkText(yAxisMark.cal_mark_max).length > yAxisMark.getMarkText(yAxisMark.cal_mark_min).length) yAxisMark.getMarkText(yAxisMark.cal_mark_max) else yAxisMark.getMarkText(yAxisMark.cal_mark_min))
.toString() + if (TextUtils.isEmpty(yAxisMark.unit)) "" else yAxisMark.unit
for (i in it.indices) {
if (it[i].show) {
paintText.textSize = it[i].textSize.toFloat()
if (i == 0) {
//x轴数据
foucsRectWidth = Math.max(foucsRectWidth, FontUtil.getFontlength(paintText, it[i].text))
foucsRectHeight += FontUtil.getFontHeight(paintText)
} else {
// text = focusPanelText[i].text+maxLable+ yAxisMark.unit;
text = it[i].text + maxLable
foucsRectWidth = Math.max(foucsRectWidth, FontUtil.getFontlength(paintText, text))
Log.w(TAG, "计算面板:$text $foucsRectWidth")
foucsRectHeight += foucsRectTextSpace + FontUtil.getFontHeight(paintText)
}
}
}
foucsRectWidth += foucsRectSpace * 4
}
/**计算点坐标 */
/* for (int i = 0; i < _datas.size(); i++) {
List<LinePoint> linePoints = _datas.get(i);
for (int j = 0; j < linePoints.size(); j++) {
if (linePoints.get(j).getValuey() == null)
continue;
linePoints.get(j).setPoint(new PointF(
rectChart.left + j * pointWidth,
rectChart.bottom - (rectChart.bottom - rectChart.top) /
(yAxisMark.cal_mark_max - yAxisMark.cal_mark_min) * (linePoints.get(j).getValuey() - yAxisMark.cal_mark_min)
));
}
}*/Log.w(TAG, "计算scrollXMax=$scrollXMax scrollx=$scrollx")
return true
}
override fun drawChart(canvas: Canvas?) {
LogUtil.e(TAG, "-----------开始绘制,当前缩放系数$scalex 偏移量$scrollx")
if(_datas.isNullOrEmpty())
return
val startTime = System.currentTimeMillis()
val yMarkSpace = (rectChart.bottom - rectChart.top) / (yAxisMark.lableNum - 1)
paintEffect.style = Paint.Style.STROKE
paintEffect.strokeWidth = yAxisMark.lineWidth.toFloat()
paintEffect.color = yAxisMark.lineColor
paintText.textSize = yAxisMark.textSize.toFloat()
paintText.color = yAxisMark.textColor
// canvas.drawLine(rectChart.left, rectChart.top, rectChart.left, rectChart.bottom, paint);
// canvas.drawLine(rectChart.left, rectChart.top, rectChart.left, rectChart.bottom, paint);
val effects: PathEffect = DashPathEffect(floatArrayOf(15f, 6f, 15f, 6f), 0f)
paintEffect.pathEffect = effects
paint.style = Paint.Style.STROKE
paint.color = yAxisMark.lineColor
for (i in 0 until yAxisMark.lableNum) {
/**绘制横向线 */
paint.strokeWidth = if (i == 0) yAxisMark.lineWidth * 2.5f else yAxisMark.lineWidth.toFloat()
canvas!!.drawLine(rectChart.left, rectChart.bottom - yMarkSpace * i,
rectChart.right, rectChart.bottom - yMarkSpace * i, paint)
/**绘制y刻度 */
val text = yAxisMark.getMarkText(yAxisMark.cal_mark_min + i * yAxisMark.cal_mark)
canvas!!.drawText(text,
rectChart.left - yAxisMark.textSpace - FontUtil.getFontlength(paintText, text),
rectChart.bottom - yMarkSpace * i - yAxisMark.textHeight / 2 + yAxisMark.textLead, paintText)
}
//绘制Y轴单位
if (!TextUtils.isEmpty(yAxisMark.unit)) {
canvas!!.drawText(yAxisMark.unit,
rectChart.left - yAxisMark.textSpace - FontUtil.getFontlength(paintText, yAxisMark.unit),
rectChart.top - yAxisMark.textSpace - yAxisMark.textHeight * 3 / 2 + yAxisMark.textLead, paintText)
}
/**绘制x轴刻度 */
// if(xAxisMark.lables!=null){
// //绘制固定的
// drawFixedXLable(canvas);
// lables = xAxisMark.lables;
// }else{
// drawXLable(canvas);
// }
/**绘制折线 */
if (maxPointNum <= 0) return
paintText.textSize = xAxisMark.textSize.toFloat()
paintText.color = xAxisMark.textColor
paint.strokeWidth = lineWidth
val radius = DensityUtil.dip2px(context, 3f).toFloat()
val path = Path()
val lastPoint = PointF()
val currentPoint = PointF()
var startIndex = (-scrollx / pointWidth).toInt()
var endIndex = ((-scrollx + rectChart.width()) / pointWidth + 1).toInt()
startIndex = Math.max(startIndex, 0)
endIndex = Math.min(endIndex, maxPointNum - 1)
// Log.w(TAG, "绘制索引:"+startIndex+" 至 "+endIndex+" scrollx="+scrollx);
// Log.w(TAG, "绘制索引:"+startIndex+" 至 "+endIndex+" scrollx="+scrollx);
val clipRect = RectF(rectChart.left - radius - lineWidth / 2, rectChart.top,
rectChart.right + radius + lineWidth / 2,
rectChart.bottom + xAxisMark.textSpace + xAxisMark.textHeight)
canvas!!.saveLayer(clipRect.left, clipRect.top, clipRect.right, clipRect.bottom, paint, Canvas.ALL_SAVE_FLAG)
var drawXLable = false
for (i in _datas.indices) {
val linePoints: List<LinePoint> = _datas[i]
if (linePoints.isNullOrEmpty()) break
path.reset()
for (j in startIndex..endIndex) {
if (j > startIndex + (endIndex - startIndex) * chartAnimValue) break
//每条线的点数量可能不一样
if (j >= linePoints.size /* || linePoints.get(j).getValuey() == null*/) continue
currentPoint.x = scrollx + rectChart.left + j * pointWidth
currentPoint.y = rectChart.bottom - (rectChart.bottom - rectChart.top) /
(yAxisMark.cal_mark_max - yAxisMark.cal_mark_min) * (linePoints[j].valuey - yAxisMark.cal_mark_min)
if (lineType == LineType.BROKEN) {
if (path.isEmpty) {
path.moveTo(currentPoint.x, currentPoint.y)
} else {
path.lineTo(currentPoint.x, currentPoint.y)
}
} else {
if (j == startIndex) {
path.moveTo(currentPoint.x, currentPoint.y)
} else if (j == startIndex + 1) { //二阶
val x = lastPoint.x + (currentPoint.x - lastPoint.x) / 2
val y = currentPoint.y
path.quadTo(x, y, currentPoint.x, currentPoint.y)
} else if (j <= endIndex - 1) { //三阶
val x1 = lastPoint.x + (currentPoint.x - lastPoint.x) / 2
val y1 = lastPoint.y
val y2 = currentPoint.y
path.cubicTo(x1, y1, x1, y2, currentPoint.x, currentPoint.y)
} else if (j == endIndex) { //最后一个 二阶
val x = lastPoint.x + (currentPoint.x - lastPoint.x) / 2
val y = lastPoint.y
path.quadTo(x, y, currentPoint.x, currentPoint.y)
}
lastPoint.x = currentPoint.x
lastPoint.y = currentPoint.y
}
drawXLable = false
if (i == maxPointIndex) {
drawXLable = if (xlables.size > 0 && xlables.contains(linePoints[j].valuex)) {
true
} else {
if (chartConfig!!.displayScheme==DisplayScheme.SHOW_BEGIN) {
j % xindexSpace === 0
} else {
(j - (maxPointNum - 1)) % xindexSpace === 0
}
}
if (drawXLable) {
// FLog.v(xlables.size()+"绘制x轴刻度"+linePoints.get(j).getValuex());
var x: Float
x = if (j == 0) {
currentPoint.x
} else if (j == maxPointNum - 1) {
currentPoint.x - FontUtil.getFontlength(paintText, linePoints[j].valuex)
} else {
currentPoint.x - FontUtil.getFontlength(paintText, linePoints[j].valuex) / 2
}
canvas!!.drawText(linePoints[j].valuex, x,
rectChart.bottom + xAxisMark.textSpace + xAxisMark.textLead, paintText)
}
}
}
paint.style = Paint.Style.STROKE
paint.strokeWidth = lineWidth
paint.color = lineColor[i]
/**
* Xfermode 有三个实现类: AvoidXfermode,PixelXorXfermode,PorterDuffXfermode
*
* 1. AvoidXfermode:对原来的像素进行处理,AvoidXfermode不支持硬件加速,使用它需要关闭硬件加速。其次,最好在新建图层上绘制. 构造方法参数分别代表:
* opColor被处理的像素颜色
* 容差值(原像素在一定范围内与传入的像素相似则处理)
* 模式: TARGET模式判断画布上是否有与opColor相似(容差)的颜色,如果有,则把该区域“染”上一层我们”画笔的颜色“,
* AVOID与TARGET相反,将画布上与传入opColor不相似的染上画笔颜色
* 比如下面的代码中首先绘制一个图片,然后对图片上的白色像素进行处理,染色为画笔的红色
* canvas.drawBitmap(mBmp,null,new Rect(0,0,width,height),mPaint);
* mPaint.setXfermode(new AvoidXfermode(Color.WHITE,100, AvoidXfermode.Mode.TARGET));
* mPaint.setColor(Color.RED)
* canvas.drawRect(0,0,width,height,mPaint);
*
* 2. PixelXofXermode 没设么用,不支持硬件加速
*
* 3. PorterDuffXfermode是最常用的,它用于描述2D图像图像合成的模式,一共有12中模式描述数字图像合成的基本手法,包括
* Clear、Source Only、Destination Only、Source Over、Source In、Source
* Out、Source Atop、Destination Over、Destination In、Destination
* Out、Destination Atop、XOR。通过组合使用 Porter-Duff 操作,可完成任意 2D
* 图像的合成。在绘图时会先检查该画笔Paint对象有没有设置Xfermode,如果没有设置Xfermode,那么直接将绘制的图形覆盖Canvas对应位置原有的像素;
* 如果设置了Xfermode,那么会按照Xfermode具体的规则来更新Canvas中对应位置的像素颜色。
*
* 使用时通常结合canvas.saveLayer(clipRect.left, clipRect.top, clipRect.right, clipRect.bottom, paint, Canvas.ALL_SAVE_FLAG)创建一个全透明的layer层,否则会产生不可预期的结果
*
* 使用它时要搞清楚两个概念,DST表示在画笔设置它之前画布上已经绘制的内容,SRC表示设置之后绘制的内容,PorterDuffXfermode就是将两个部分的像素按照一定的模式进行合并
*/
//这里设置DST_OVER,目的是将绘制path之前已经绘制的线上的点显示的线之上,要不然线会遮住小圆点
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OVER)
canvas!!.drawPath(path, paint)
paint.xfermode = null
}
// canvas.restore();
// Log.w(TAG, "绘制一次需要:"+(System.currentTimeMillis() - startTime)+ " ms");
// canvas.restore();
// Log.w(TAG, "绘制一次需要:"+(System.currentTimeMillis() - startTime)+ " ms");
drawFocus(canvas)
}
/**绘制焦点 */
private val focusLineColor = Color.parseColor("#319A5A")
private val focusLineSize = DensityUtil.dip2px(context, 1f)
private val foucsRectTextSpace = DensityUtil.dip2px(context, 3f)
private val foucsRectSpace = DensityUtil.dip2px(context, 6f)
//焦点面板矩形宽高
private var foucsRectWidth = 0f
private var foucsRectHeight = 0f
private fun drawFocus(canvas: Canvas?) {
try {
if (null == focusData || maxPointNum == 0 || null==canvas) return
//绘制竖直虚线
val effects: PathEffect = DashPathEffect(floatArrayOf(8f, 5f, 8f, 5f), 0f)
paintEffect.style = Paint.Style.STROKE
paintEffect.strokeWidth = focusLineSize.toFloat()
paintEffect.color = focusLineColor
paintEffect.pathEffect = effects
val path = Path()
path.moveTo(focusData!!.point.x, rectChart.bottom)
path.lineTo(focusData!!.point.x, rectChart.top)
canvas.drawPath(path, paintEffect)
//面板
val showLeft: Boolean = focusData!!.point.x - rectChart.left > (rectChart.right - rectChart.left) / 2
val rect = RectF(
if (showLeft) focusData!!.point.x - foucsRectWidth - 30 else focusData!!.point.x + 30,
rectChart.top /*+ (rectChart.bottom - rectChart.top)/2 - foucsRectHeight/2*/,
if (showLeft) focusData!!.point.x - 30 else focusData!!.point.x + foucsRectWidth + 30,
rectChart.top + foucsRectHeight /*+ (rectChart.bottom - rectChart.top)/2 + foucsRectHeight/2*/
)
paint.style = Paint.Style.FILL
paint.color = Color.WHITE
paint.alpha = 230
canvas.drawRect(rect, paint)
//面板中的文字
//2020-10-16 06:00
//零序电流:15.2KW
//A相电流:15.2KW
//A相电流:15.2KW
//A相电流:15.2KW
var text = ""
var top = rect.top + foucsRectSpace
val currentPoint = PointF()
val radius = DensityUtil.dip2px(context, 2.5f).toFloat()
focusPanelText?.let {
for (i in it.indices) {
if (it[i].show) {
paintText.textSize = it[i].textSize.toFloat()
paintText.color = it[i].textColor
if (i == 0) {
//x轴数据
for (point in focusData!!.data) {
if (point != null) {
text = if (TextUtils.isEmpty(point.valuexfocus)) point.valuex else point.valuexfocus
break
}
}
} else {
top += foucsRectTextSpace.toFloat()
/* text = focusPanelText[i].text+
(focusData!!.data.get(i-1)==null?"":getFocusYText(focusData!!.data.get(i-1).getValuey()))
+ yAxisMark.unit;*/
text = (it[i].text +
(if (focusData!!.data[i - 1] == null) "" else YAxisMark.formattedDecimal(focusData!!.data[i - 1]!!.valuey.toDouble(), 2))
+ yAxisMark.unit)
//绘制焦点圆圈
if (focusData!!.data[i - 1] != null) {
currentPoint.x = focusData!!.point.x
currentPoint.y = rectChart.bottom - (rectChart.bottom - rectChart.top) /
(yAxisMark.cal_mark_max - yAxisMark.cal_mark_min) * (focusData!!.data[i - 1]!!.valuey - yAxisMark.cal_mark_min)
paint.style = Paint.Style.STROKE
paint.color = lineColor.get(i - 1)
canvas.drawCircle(currentPoint.x, currentPoint.y, radius, paint)
paint.style = Paint.Style.FILL
paint.color = Color.WHITE
canvas.drawCircle(currentPoint.x, currentPoint.y, radius - lineWidth / 2, paint)
}
}
canvas.drawText(text,
rect.left + foucsRectSpace,
top + FontUtil.getFontLeading(paintText), paintText)
top += FontUtil.getFontHeight(paintText)
}
}
}
} catch (e: java.lang.Exception) {
e.printStackTrace()
}
}
/***************************事件👇👇👇***************************/
private var beginPointWidth = 0f
override fun onScaleBegin(detector: ScaleGestureDetector) {
beginPointWidth = pointWidth
}
override fun onScale(detector: ScaleGestureDetector, beginScrollx: Float) {
pointWidth *= detector.scaleFactor
//缩放范围约束
// pointWidthMin = rectChart.width() / (maxPointNum-1); //缩小到全部显示
// pointWidth = rectChart.width() / (pageShowNum-1);
// pointWidthMax = rectChart.width() / 4; //最大只能放大到每个标签显示5个点
pointWidth = Math.min(pointWidth, pointWidthMax)
pointWidth = Math.max(pointWidth, pointWidthMin)
//重新计算最大偏移量
scrollXMax = -(pointWidth * (maxPointNum - 1) - rectChart.width()) //最大滚动距离,是一个负值
//计算当前偏移量
Log.i(TAG, "=============================当前偏移:$scrollx 两点宽度 = $pointWidth")
//为了保证焦点对应的点位置不变,是使用公式: beginScrollx + rectChart.left + focusIndex*beginPointWidth = scrollx + rectChart.left + focusIndex*pointWidth
scrollx = beginScrollx + focusIndex * (beginPointWidth - pointWidth)
scrollx = Math.min(scrollx, 0f)
scrollx = Math.max(scrollXMax, scrollx)
caculateXMark()
// Log.i(TAG, "缩放后偏移:"+scrollx);
}
override fun onFocusTouch(point: PointF?) {
focusData = null
point?.let {
if (!_datas.isNullOrEmpty() && focusPanelText!=null) {
//避免滑出
point.x = Math.max(point.x, rectChart.left);
point.x = Math.min(point.x, rectChart.right);
//获取焦点对应的数据的索引
focusIndex =((-scrollx + (point.x - rectChart.left)) / pointWidth).toInt()
if ((-scrollx + (point.x - rectChart.left)) - focusIndex * pointWidth > pointWidth / 2) {
LogUtil.e(TAG, "========焦点在下一个点范围了:" + focusIndex);
focusIndex += 1;
}
LogUtil.e(TAG, "========焦点索引:" + focusIndex);
focusIndex = Math.max(0, Math.min(focusIndex, maxPointNum - 1));
point.x = rectChart.left + (focusIndex * pointWidth + scrollx);
val datas = mutableListOf<LinePoint?>()
for (line in _datas) {
if (focusIndex < line.size) datas.add(line[focusIndex])
else datas.add(null)
}
focusData = FocusData(datas, it)
}
}
}
/***************************事件👆👆👆***************************/
/***************************2. 子类重写👆👆👆***************************/
/***************************3. 特殊👇👇👇***************************/
private var xindexSpace: Int = 0
private val xlables: MutableList<String> = ArrayList()
private fun caculateXMark() {
xlables.clear()
if (xAxisMark.lables != null && xAxisMark.lables.size > 0) {
xlables.addAll(Arrays.asList(*xAxisMark.lables))
return
}
val markSpace = rectChart.width() / (xAxisMark.lableNum - 1)
//每隔多少展示一个标签
xindexSpace = (markSpace / pointWidth).toInt()
xindexSpace = Math.max(xindexSpace, 1)
}
private fun calculateYMark() {
if (maxPointNum == 0) {
//没有数据
yAxisMark.cal_mark_min = 0f
yAxisMark.cal_mark_max = yAxisMark.lableNum - 1.toFloat()
yAxisMark.cal_mark = 1f
return
}
val redundance = 1.01f //y轴最大和最小值冗余
yAxisMark.cal_mark_max = -Float.MAX_VALUE //Y轴刻度最大值
yAxisMark.cal_mark_min = Float.MAX_VALUE //Y轴刻度最小值
for (linePoints in _datas) {
for (point in linePoints) {
yAxisMark.cal_mark_max = Math.max(yAxisMark.cal_mark_max, point.valuey)
yAxisMark.cal_mark_min = Math.min(yAxisMark.cal_mark_min, point.valuey)
}
}
LogUtil.w(TAG, "真实最小最大值:" + yAxisMark.cal_mark_min + " " + yAxisMark.cal_mark_max)
//只有一个点的时候
if (yAxisMark.cal_mark_min == yAxisMark.cal_mark_max) {
if (yAxisMark.cal_mark_min > 0) {
yAxisMark.cal_mark_min = 0f
} else if (yAxisMark.cal_mark_min == 0f) {
yAxisMark.cal_mark_max = 1f
} else if (yAxisMark.cal_mark_min < 0) {
yAxisMark.cal_mark_max = 0f
}
}
if (yAxisMark.markType == MarkType.Integer) {
val min = if (yAxisMark.cal_mark_min > 0) 0 else yAxisMark.cal_mark_min.toInt()
val max = yAxisMark.cal_mark_max.toInt()
var mark = (max - min) / (yAxisMark.lableNum - 1) + if ((max - min) % (yAxisMark.lableNum - 1) > 0) 1 else 0
mark = if (mark == 0) 1 else mark //最大值和最小值都为0的情况
val first = (mark.toString() + "").substring(0, 1).toInt() + 1
if ((mark.toString() + "").length == 1) {
//YMARK = 1、2、5、10
mark = if (mark == 3 || mark == 4 || mark == 6 || mark == 7 || mark == 8 || mark == 9) if (mark == 3 || mark == 4) 5 else 10 else mark
} else if ((mark.toString() + "").length == 2) {
mark = first * 10
} else if ((mark.toString() + "").length == 3) {
mark = first * 100
} else if ((mark.toString() + "").length == 4) {
mark = first * 1000
} else if ((mark.toString() + "").length == 5) {
mark = first * 10000
} else if ((mark.toString() + "").length == 6) {
mark = first * 100000
}
yAxisMark.cal_mark_min = 0f
yAxisMark.cal_mark_max = mark * (yAxisMark.lableNum - 1).toFloat()
yAxisMark.cal_mark = mark.toFloat()
} else { //Float //Percent
yAxisMark.cal_mark_max = if (yAxisMark.cal_mark_max < 0) yAxisMark.cal_mark_max / redundance else yAxisMark.cal_mark_max * redundance
yAxisMark.cal_mark_min = if (yAxisMark.cal_mark_min < 0) yAxisMark.cal_mark_min * redundance else yAxisMark.cal_mark_min / redundance
yAxisMark.cal_mark = (yAxisMark.cal_mark_max - yAxisMark.cal_mark_min) / (yAxisMark.lableNum - 1)
}
//小数点位
if (yAxisMark.digits == 0) {
val mark = (yAxisMark.cal_mark_max - yAxisMark.cal_mark_min) / (yAxisMark.lableNum - 1)
if (mark < 1) {
val pattern = "[1-9]"
val p = Pattern.compile(pattern)
val m = p.matcher(mark.toString() + "") // 获取 matcher 对象
m.find()
val index = m.start()
yAxisMark.digits = index - 1
LogUtil.w(TAG, mark.toString() + "第一个大于0的数字位置:" + index + " 保留小数位数:" + yAxisMark.digits)
}
}
LogUtil.w(TAG, "最终最小最大值:" + yAxisMark.cal_mark_min + " " + yAxisMark.cal_mark_max + " " + yAxisMark.cal_mark)
}
/**绘制焦点 */
/**********************************3. 测量和绘制👇 */
/**获取焦点面板上数值展示字符串 */
fun getFocusYText(value: Float): String? {
when (yAxisMark.markType) {
MarkType.Integer -> return (value as Long).toString()
MarkType.Float -> return NumberFormatUtil.formattedDecimal(value.toDouble())
MarkType.Percent -> return NumberFormatUtil.formattedDecimalToPercentage(value.toDouble(), 2)
}
return value.toString() + ""
}
/**绘制 XAxisMark.lables 设置的固定x刻度, */
private fun drawFixedXLable(canvas: Canvas) {
val oneWidth = (-scrollXMax + rectChart.width()) / (xAxisMark.lables.size - 1)
Log.w(TAG, "最大滚动:" + scrollXMax + " 图表宽度" + rectChart.width() + " lable数量" + xAxisMark.lables.size + " 单个跨度:" + oneWidth)
paintText.textSize = xAxisMark.textSize.toFloat()
paintText.color = xAxisMark.textColor
var x: Float
val restoreCount = canvas.save()
canvas.clipRect(RectF(rectChart.left, rectChart.bottom, rectChart.right, rectChart.bottom + xAxisMark.textSpace + xAxisMark.textHeight))
for (i in xAxisMark.lables.indices) {
val text = xAxisMark.lables[i]
x = if (i == 0) {
scrollx + rectChart.left + i * oneWidth
} else if (i == xAxisMark.lables.size - 1) {
scrollx + rectChart.left + i * oneWidth - FontUtil.getFontlength(paintText, text)
} else {
scrollx + rectChart.left + i * oneWidth - FontUtil.getFontlength(paintText, text) / 2
}
canvas.drawText(text, x,
rectChart.bottom + xAxisMark.textSpace + xAxisMark.textLead, paintText)
}
canvas.restoreToCount(restoreCount)
}
/***************************3. 特殊👆👆👆***************************/
}
@@ -0,0 +1,80 @@
package com.openxu.hkchart.loading;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import com.openxu.utils.DensityUtil;
import java.util.ArrayList;
public class BallPulseIndicator extends LoadingIndicator {
private float loadRadius;
private float loadCircleSpace;
private float loadingAnimValueMax = 1;
private float loadingAnimValueMin = 0.4f;
private int[] loadCircleColors = new int[]{
Color.parseColor("#DB4528"),
Color.parseColor("#5f93e7"),
Color.parseColor("#fda33c")};
private float[] scaleFloats = new float[]{loadingAnimValueMax, loadingAnimValueMax, loadingAnimValueMax};
public BallPulseIndicator(Context context) {
super(context);
loadRadius = DensityUtil.dip2px(context, 5);
loadCircleSpace = DensityUtil.dip2px(context, 4);
}
@Override
public void draw(Canvas canvas, Paint paint) {
Rect rect = getBounds();
paint.setStyle(Paint.Style.FILL);
for(int i = 0; i<loadCircleColors.length; i++){
paint.setColor(loadCircleColors[i]);
canvas.drawCircle(rect.left+rect.width()/2 - loadRadius*2 - loadCircleSpace + i*(loadRadius*2 + loadCircleSpace),
rect.top+rect.height()/2,
loadRadius*scaleFloats[i],paint);
}
/* float circleSpacing = 4;
float radius=(rect.width()-circleSpacing*(loadCircleColors.length-1))/(loadCircleColors.length*2);
float x = rect.width()/ 2-(radius*2+circleSpacing);
float y = rect.height() / 2;
for (int i = 0; i < loadCircleColors.length; i++) {
paint.setColor(loadCircleColors[i]);
canvas.save();
float translateX=x+(radius*2)*i+circleSpacing*i;
canvas.translate(translateX, y);
canvas.scale(scaleFloats[i], scaleFloats[i]);
canvas.drawCircle(0, 0, radius, paint);
canvas.restore();
}*/
}
@Override
public ArrayList<ValueAnimator> onCreateAnimators() {
ArrayList<ValueAnimator> animators=new ArrayList<>();
int[] delays=new int[]{120,240,360};
for (int i = 0; i < 3; i++) {
final int index=i;
ValueAnimator scaleAnim=ValueAnimator.ofFloat(loadingAnimValueMax, loadingAnimValueMin,loadingAnimValueMax);
scaleAnim.setDuration(900);
scaleAnim.setRepeatCount(-1);
scaleAnim.setStartDelay(delays[i]);
addUpdateListener(scaleAnim, animation -> {
scaleFloats[index] = (float) animation.getAnimatedValue();
// Log.w(TAG, index + "刷新loading "+scaleFloats[index]);
invalidateSelf();
});
animators.add(scaleAnim);
}
return animators;
}
}
@@ -0,0 +1,131 @@
package com.openxu.hkchart.loading;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.util.Log;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import androidx.annotation.Nullable;
/**
* https://github.com/81813780/AVLoadingIndicatorView/blob/master/library/src/main/java/com/wang/avi/Indicator.java
*
*
*/
public abstract class LoadingIndicator extends Drawable implements Animatable {
protected String TAG = getClass().getSimpleName();
protected List<ValueAnimator> mAnimators;
private HashMap<ValueAnimator,ValueAnimator.AnimatorUpdateListener> mUpdateListeners;
// protected Rect drawBounds;
private Paint mPaint;
public LoadingIndicator(Context context) {
mUpdateListeners = new HashMap<>();
// drawBounds = new Rect();
mPaint=new Paint();
mPaint.setColor(Color.WHITE);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setAntiAlias(true);
}
@Override
public void draw(Canvas canvas) {
draw(canvas,mPaint);
}
/**绘制loading*/
protected abstract void draw(Canvas canvas, Paint paint);
@Override
public void setAlpha(int alpha) {
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter) {
}
@Override
public int getOpacity() {
//没有透明格式
return PixelFormat.OPAQUE;
}
public abstract ArrayList<ValueAnimator> onCreateAnimators();
/**
* Your should use this to add AnimatorUpdateListener when
* create animator , otherwise , animator doesn't work when
* the animation restart .
* @param updateListener
*/
public void addUpdateListener(ValueAnimator animator, ValueAnimator.AnimatorUpdateListener updateListener){
mUpdateListeners.put(animator,updateListener);
}
@Override
public void start() {
if(mAnimators==null || mAnimators.size()<=0)
mAnimators = onCreateAnimators();
if (mAnimators == null)
return;
// If the animators has not ended, do nothing.
if (isStarted()) {
return;
}
Log.w(TAG, "执行加载动画");
for (int i = 0; i < mAnimators.size(); i++) {
ValueAnimator animator = mAnimators.get(i);
//when the animator restart , add the updateListener again because they
// was removed by animator stop .
ValueAnimator.AnimatorUpdateListener updateListener=mUpdateListeners.get(animator);
if (updateListener!=null)
animator.addUpdateListener(updateListener);
animator.start();
}
}
private boolean isStarted() {
for (ValueAnimator animator : mAnimators)
return animator.isStarted();
return false;
}
@Override
public boolean isRunning() {
for (ValueAnimator animator : mAnimators)
return animator.isRunning();
return false;
}
@Override
public void stop() {
if (mAnimators!=null){
for (ValueAnimator animator : mAnimators) {
if (animator != null && animator.isStarted()) {
animator.removeAllUpdateListeners();
animator.end();
}
}
}
}
}
@@ -0,0 +1,113 @@
package com.openxu.utils;
import android.util.Log;
import com.openxu.cview.xmstock20201030.build.AxisMark;
import java.util.Arrays;
/**
* 图表计算工具
*/
public class ChartCalUtil {
private static String TAG = "ChartCalUtil";
/**
* 计算X轴刻度点lable值
* @param axisMark
*/
public static void calXLable(AxisMark axisMark){/*List datas, String field, int num){*/
Object[] newDatas;
if(axisMark.datas==null || axisMark.field==null) {
axisMark.lables = new String[]{};
}
if(axisMark.datas.size()<=axisMark.lableNum){
axisMark.lables = new String[axisMark.datas.size()];
newDatas = axisMark.datas.toArray();
}else{
axisMark.lables = new String[axisMark.lableNum];
newDatas = new Object[axisMark.lableNum];
//取第一个和最后一个
newDatas[0] = axisMark.datas.get(0);
newDatas[axisMark.lableNum-1] = axisMark.datas.get(axisMark.datas.size()-1);
int part = axisMark.datas.size() / axisMark.lableNum;
Log.i(TAG, "均匀取值:"+part);
for(int i = 1; i< axisMark.lableNum-1; i++){
newDatas[i] = axisMark.datas.get(i*part);
}
}
for(int j = 0; j<newDatas.length; j++){
axisMark.lables[j] = ReflectUtil.getField(newDatas[j], axisMark.field).toString();
}
}
/**
* 根据传入的Y轴信息,从数据中获取最小最大值,并获取到Y刻度点值
* @param axisMark
* @return
*/
public static AxisMark calYLable(AxisMark axisMark){
axisMark.cal_mark_max = Float.MIN_VALUE; //Y轴刻度最大值
axisMark.cal_mark_min = Float.MAX_VALUE; //Y轴刻度最小值
for(Object data : axisMark.datas){
String str = ReflectUtil.getField(data, axisMark.field).toString();
try {
if(str.contains("%")){
str = str.substring(0,str.indexOf("%"));//百分数不能直接强转
axisMark.cal_mark = Float.parseFloat(str) /100.0f;
}else{
axisMark.cal_mark = Float.parseFloat(str);
}
// LogUtil.d(TAG, str+" Y轴 被识别为Float = "+YMARK);
if(axisMark.cal_mark>axisMark.cal_mark_max)
axisMark.cal_mark_max = axisMark.cal_mark;
if(axisMark.cal_mark<axisMark.cal_mark_min)
axisMark.cal_mark_min = axisMark.cal_mark;
}catch (Exception e){
}
}
LogUtil.i(TAG, "Y轴真实axisMark.cal_mark_min="+axisMark.cal_mark_min+" axisMark.cal_mark_max="+axisMark.cal_mark_max);
if(axisMark.cal_mark_max>0)
axisMark.cal_mark_max *= 1.1f;
else
axisMark.cal_mark_max /= 1.1f;
if(axisMark.cal_mark_min>0)
axisMark.cal_mark_min /= 1.1f;
else
axisMark.cal_mark_min *= 1.1f;
if(axisMark.cal_mark_min>0)
axisMark.cal_mark_min = 0;
axisMark.cal_mark = (axisMark.cal_mark_max-axisMark.cal_mark_min)/(axisMark.lableNum - 1);
axisMark.lables = new String[axisMark.lableNum];
if(axisMark.lableType == AxisMark.LABLE_TYPE.INTEGER){
axisMark.cal_mark = (int)axisMark.cal_mark + 1;
axisMark.cal_mark_min = (int)axisMark.cal_mark_min;
axisMark.cal_mark_max = axisMark.cal_mark_min+axisMark.cal_mark*(axisMark.lableNum-1);
}else if(axisMark.lableType == AxisMark.LABLE_TYPE.PERCENTAGE){
}else if(axisMark.lableType == AxisMark.LABLE_TYPE.FLOAT){
}
LogUtil.i(TAG, "Y轴axisMark.cal_mark_min="+axisMark.cal_mark_min+" axisMark.cal_mark_max="+axisMark.cal_mark_max+" axisMark.cal_mark="+axisMark.cal_mark);
for(int i = 0; i< axisMark.lableNum; i++){
float num = axisMark.cal_mark_min + i * axisMark.cal_mark;
if(axisMark.lableType == AxisMark.LABLE_TYPE.INTEGER){
axisMark.lables[i] = ((int)num)+"";
}else if(axisMark.lableType == AxisMark.LABLE_TYPE.PERCENTAGE){
axisMark.lables[i] = NumberFormatUtil.formattedDecimalToPercentage(num, 2);
}else if(axisMark.lableType == AxisMark.LABLE_TYPE.FLOAT){
axisMark.lables[i] = NumberFormatUtil.formattedDecimal(num);
}
}
Log.i(TAG, "计算Y轴刻度:"+ Arrays.asList(axisMark.lables));
return axisMark;
}
}
@@ -0,0 +1,59 @@
package com.openxu.utils;
import android.content.Context;
import android.view.WindowManager;
public class DensityUtil {
/**
* 根据手机的分辨率从 dp 的单位 转成为 px(像素)
*/
public static int dip2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
/**
* 根据手机的分辨率从 px(像素) 的单位 转成为 dp
*/
public static int px2dip(Context context, float pxValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (pxValue / scale + 0.5f);
}
/**
* 将px值转换为sp值,保证文字大小不变
*
* @param pxValue
* @return
*/
public static int px2sp(Context context, float pxValue) {
final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
return (int) (pxValue / fontScale + 0.5f);
}
/**
* 将sp值转换为px值,保证文字大小不变
* @param spValue
* @return
*/
public static int sp2px(Context context, float spValue) {
final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
return (int) (spValue * fontScale + 0.5f);
}
public static int getScreenWidth(Context context){
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
int width = wm.getDefaultDisplay().getWidth();
int height = wm.getDefaultDisplay().getHeight();
return width;
}
public static int getScreenHeight(Context context){
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
int height = wm.getDefaultDisplay().getHeight();
return height;
}
}
@@ -0,0 +1,643 @@
package com.openxu.utils;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.Point;
import android.os.Build;
import android.util.Log;
import android.util.TypedValue;
import android.view.Display;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewGroup.MarginLayoutParams;
import android.view.Window;
import android.view.WindowManager;
import java.lang.reflect.Method;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RequiresPermission;
import androidx.drawerlayout.widget.DrawerLayout;
import static android.Manifest.permission.EXPAND_STATUS_BAR;
/**
* Author: openX
* Time: 2019/3/14 11:09
* class: FBarUtils
* Description:状态栏工具
*
* getStatusBarHeight : 获取状态栏高度(px)
* setStatusBarVisibility : 设置状态栏是否可见
* isStatusBarVisible : 判断状态栏是否可见
* setStatusBarLightMode : 设置状态栏是否为浅色模式
* addMarginTopEqualStatusBarHeight : 为 view 增加 MarginTop 为状态栏高度
* subtractMarginTopEqualStatusBarHeight: 为 view 减少 MarginTop 为状态栏高度
* setStatusBarColor : 设置状态栏颜色
* setStatusBarColor4Drawer : 为 DrawerLayout 设置状态栏颜色
* getActionBarHeight : 获取 ActionBar 高度
* setNotificationBarVisibility : 设置通知栏是否可见
* getNavBarHeight : 获取导航栏高度
* setNavBarVisibility : 设置导航栏是否可见
* isNavBarVisible : 判断导航栏是否可见
* setNavBarColor : 设置导航栏颜色
* getNavBarColor : 获取导航栏颜色
* isSupportNavBar : 判断是否支持导航栏
*/
public final class FBarUtils {
///////////////////////////////////////////////////////////////////////////
// status bar
///////////////////////////////////////////////////////////////////////////
private static final String TAG_STATUS_BAR = "TAG_STATUS_BAR";
private static final String TAG_OFFSET = "TAG_OFFSET";
private static final int KEY_OFFSET = -123;
private FBarUtils() {
throw new UnsupportedOperationException("u can't instantiate me...");
}
/**
* Return the status bar's height.
*
* @return the status bar's height
*/
public static int getStatusBarHeight() {
Resources resources = Resources.getSystem();
int resourceId = resources.getIdentifier("status_bar_height", "dimen", "android");
return resources.getDimensionPixelSize(resourceId);
}
/**
* Set the status bar's visibility.
*
* @param activity The activity.
* @param isVisible True to set status bar visible, false otherwise.
*/
public static void setStatusBarVisibility(@NonNull final Activity activity,
final boolean isVisible) {
setStatusBarVisibility(activity.getWindow(), isVisible);
}
/**
* Set the status bar's visibility.
*
* @param window The window.
* @param isVisible True to set status bar visible, false otherwise.
*/
public static void setStatusBarVisibility(@NonNull final Window window,
final boolean isVisible) {
if (isVisible) {
window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
showStatusBarView(window);
addMarginTopEqualStatusBarHeight(window);
} else {
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
hideStatusBarView(window);
subtractMarginTopEqualStatusBarHeight(window);
}
}
/**
* Return whether the status bar is visible.
*
* @param activity The activity.
* @return {@code true}: yes<br>{@code false}: no
*/
public static boolean isStatusBarVisible(@NonNull final Activity activity) {
int flags = activity.getWindow().getAttributes().flags;
return (flags & WindowManager.LayoutParams.FLAG_FULLSCREEN) == 0;
}
/**
* Set the status bar's light mode.
*
* @param activity The activity.
* @param isLightMode True to set status bar light mode, false otherwise.
*/
public static void setStatusBarLightMode(@NonNull final Activity activity,
final boolean isLightMode) {
setStatusBarLightMode(activity.getWindow(), isLightMode);
}
/**
* Set the status bar's light mode.
*
* @param window The window.
* @param isLightMode True to set status bar light mode, false otherwise.
*/
public static void setStatusBarLightMode(@NonNull final Window window,
final boolean isLightMode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
View decorView = window.getDecorView();
if (decorView != null) {
int vis = decorView.getSystemUiVisibility();
if (isLightMode) {
vis |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
} else {
vis &= ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
}
decorView.setSystemUiVisibility(vis);
}
}
}
/**
* Is the status bar light mode.
*
* @param activity The activity.
* @return {@code true}: yes<br>{@code false}: no
*/
public static boolean isStatusBarLightMode(@NonNull final Activity activity) {
return isStatusBarLightMode(activity.getWindow());
}
/**
* Is the status bar light mode.
*
* @param window The window.
* @return {@code true}: yes<br>{@code false}: no
*/
public static boolean isStatusBarLightMode(@NonNull final Window window) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
View decorView = window.getDecorView();
if (decorView != null) {
int vis = decorView.getSystemUiVisibility();
return (vis & View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) != 0;
}
}
return false;
}
/**
* Add the top margin size equals status bar's height for view.
*
* @param view The view.
*/
public static void addMarginTopEqualStatusBarHeight(@NonNull View view) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return;
view.setTag(TAG_OFFSET);
Object haveSetOffset = view.getTag(KEY_OFFSET);
if (haveSetOffset != null && (Boolean) haveSetOffset) return;
MarginLayoutParams layoutParams = (MarginLayoutParams) view.getLayoutParams();
layoutParams.setMargins(layoutParams.leftMargin,
layoutParams.topMargin + getStatusBarHeight(),
layoutParams.rightMargin,
layoutParams.bottomMargin);
view.setTag(KEY_OFFSET, true);
}
/**
* Subtract the top margin size equals status bar's height for view.
*
* @param view The view.
*/
public static void subtractMarginTopEqualStatusBarHeight(@NonNull View view) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return;
Object haveSetOffset = view.getTag(KEY_OFFSET);
if (haveSetOffset == null || !(Boolean) haveSetOffset) return;
MarginLayoutParams layoutParams = (MarginLayoutParams) view.getLayoutParams();
layoutParams.setMargins(layoutParams.leftMargin,
layoutParams.topMargin - getStatusBarHeight(),
layoutParams.rightMargin,
layoutParams.bottomMargin);
view.setTag(KEY_OFFSET, false);
}
private static void addMarginTopEqualStatusBarHeight(final Window window) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return;
View withTag = window.getDecorView().findViewWithTag(TAG_OFFSET);
if (withTag == null) return;
addMarginTopEqualStatusBarHeight(withTag);
}
private static void subtractMarginTopEqualStatusBarHeight(final Window window) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return;
View withTag = window.getDecorView().findViewWithTag(TAG_OFFSET);
if (withTag == null) return;
subtractMarginTopEqualStatusBarHeight(withTag);
}
/**
* Set the status bar's color.
*
* @param activity The activity.
* @param color The status bar's color.
*/
public static View setStatusBarColor(@NonNull final Activity activity,
@ColorInt final int color) {
return setStatusBarColor(activity, color, false);
}
/**
* Set the status bar's color.
*
* @param activity The activity.
* @param color The status bar's color.
* @param isDecor True to add fake status bar in DecorView,
* false to add fake status bar in ContentView.
*/
public static View setStatusBarColor(@NonNull final Activity activity,
@ColorInt final int color,
final boolean isDecor) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return null;
transparentStatusBar(activity);
return applyStatusBarColor(activity, color, isDecor);
}
/**
* Set the status bar's color.
*
* @param fakeStatusBar The fake status bar view.
* @param color The status bar's color.
*/
public static void setStatusBarColor(@NonNull final View fakeStatusBar,
@ColorInt final int color) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return;
Activity activity = getActivityByView(fakeStatusBar);
if (activity == null) return;
transparentStatusBar(activity);
fakeStatusBar.setVisibility(View.VISIBLE);
ViewGroup.LayoutParams layoutParams = fakeStatusBar.getLayoutParams();
layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
layoutParams.height = getStatusBarHeight();
fakeStatusBar.setBackgroundColor(color);
}
/**
* Set the custom status bar.
*
* @param fakeStatusBar The fake status bar view.
*/
public static void setStatusBarCustom(@NonNull final View fakeStatusBar) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return;
Activity activity = getActivityByView(fakeStatusBar);
if (activity == null) return;
transparentStatusBar(activity);
fakeStatusBar.setVisibility(View.VISIBLE);
ViewGroup.LayoutParams layoutParams = fakeStatusBar.getLayoutParams();
if (layoutParams == null) {
layoutParams = new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
getStatusBarHeight()
);
fakeStatusBar.setLayoutParams(layoutParams);
} else {
layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
layoutParams.height = getStatusBarHeight();
}
}
/**
* Set the status bar's color for DrawerLayout.
* <p>DrawLayout must add {@code android:fitsSystemWindows="true"}</p>
*
* @param drawer The DrawLayout.
* @param fakeStatusBar The fake status bar view.
* @param color The status bar's color.
*/
public static void setStatusBarColor4Drawer(@NonNull final DrawerLayout drawer,
@NonNull final View fakeStatusBar,
@ColorInt final int color) {
setStatusBarColor4Drawer(drawer, fakeStatusBar, color, false);
}
/**
* Set the status bar's color for DrawerLayout.
* <p>DrawLayout must add {@code android:fitsSystemWindows="true"}</p>
*
* @param drawer The DrawLayout.
* @param fakeStatusBar The fake status bar view.
* @param color The status bar's color.
* @param isTop True to set DrawerLayout at the top layer, false otherwise.
*/
public static void setStatusBarColor4Drawer(@NonNull final DrawerLayout drawer,
@NonNull final View fakeStatusBar,
@ColorInt final int color,
final boolean isTop) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return;
Activity activity = getActivityByView(fakeStatusBar);
if (activity == null) return;
transparentStatusBar(activity);
drawer.setFitsSystemWindows(false);
setStatusBarColor(fakeStatusBar, color);
for (int i = 0, count = drawer.getChildCount(); i < count; i++) {
drawer.getChildAt(i).setFitsSystemWindows(false);
}
if (isTop) {
hideStatusBarView(activity);
} else {
setStatusBarColor(activity, color, false);
}
}
private static View applyStatusBarColor(final Activity activity,
final int color,
boolean isDecor) {
ViewGroup parent = isDecor ?
(ViewGroup) activity.getWindow().getDecorView() :
(ViewGroup) activity.findViewById(android.R.id.content);
View fakeStatusBarView = parent.findViewWithTag(TAG_STATUS_BAR);
if (fakeStatusBarView != null) {
if (fakeStatusBarView.getVisibility() == View.GONE) {
fakeStatusBarView.setVisibility(View.VISIBLE);
}
fakeStatusBarView.setBackgroundColor(color);
} else {
fakeStatusBarView = createStatusBarView(activity, color);
parent.addView(fakeStatusBarView);
}
return fakeStatusBarView;
}
private static void hideStatusBarView(final Activity activity) {
hideStatusBarView(activity.getWindow());
}
private static void hideStatusBarView(final Window window) {
ViewGroup decorView = (ViewGroup) window.getDecorView();
View fakeStatusBarView = decorView.findViewWithTag(TAG_STATUS_BAR);
if (fakeStatusBarView == null) return;
fakeStatusBarView.setVisibility(View.GONE);
}
private static void showStatusBarView(final Window window) {
ViewGroup decorView = (ViewGroup) window.getDecorView();
View fakeStatusBarView = decorView.findViewWithTag(TAG_STATUS_BAR);
if (fakeStatusBarView == null) return;
fakeStatusBarView.setVisibility(View.VISIBLE);
}
private static View createStatusBarView(final Activity activity,
final int color) {
View statusBarView = new View(activity);
statusBarView.setLayoutParams(new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, getStatusBarHeight()));
statusBarView.setBackgroundColor(color);
statusBarView.setTag(TAG_STATUS_BAR);
return statusBarView;
}
private static void transparentStatusBar(final Activity activity) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return;
Window window = activity.getWindow();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
int option = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
int vis = window.getDecorView().getSystemUiVisibility() & View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
window.getDecorView().setSystemUiVisibility(option | vis);
} else {
window.getDecorView().setSystemUiVisibility(option);
}
window.setStatusBarColor(Color.TRANSPARENT);
} else {
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
}
}
///////////////////////////////////////////////////////////////////////////
// action bar
///////////////////////////////////////////////////////////////////////////
/**
* Return the action bar's height.
*
* @return the action bar's height
*/
public static int getActionBarHeight() {
TypedValue tv = new TypedValue();
if (FUtils.getApp().getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true)) {
return TypedValue.complexToDimensionPixelSize(
tv.data, FUtils.getApp().getResources().getDisplayMetrics()
);
}
return 0;
}
///////////////////////////////////////////////////////////////////////////
// notification bar
///////////////////////////////////////////////////////////////////////////
/**
* Set the notification bar's visibility.
* <p>Must hold {@code <uses-permission android:name="android.permission.EXPAND_STATUS_BAR" />}</p>
*
* @param isVisible True to set notification bar visible, false otherwise.
*/
@RequiresPermission(EXPAND_STATUS_BAR)
public static void setNotificationBarVisibility(final boolean isVisible) {
String methodName;
if (isVisible) {
methodName = (Build.VERSION.SDK_INT <= 16) ? "expand" : "expandNotificationsPanel";
} else {
methodName = (Build.VERSION.SDK_INT <= 16) ? "collapse" : "collapsePanels";
}
invokePanels(methodName);
}
private static void invokePanels(final String methodName) {
try {
@SuppressLint("WrongConstant")
Object service = FUtils.getApp().getSystemService("statusbar");
@SuppressLint("PrivateApi")
Class<?> statusBarManager = Class.forName("android.app.StatusBarManager");
Method expand = statusBarManager.getMethod(methodName);
expand.invoke(service);
} catch (Exception e) {
e.printStackTrace();
}
}
///////////////////////////////////////////////////////////////////////////
// navigation bar
///////////////////////////////////////////////////////////////////////////
/**
* Return the navigation bar's height.
*
* @return the navigation bar's height
*/
public static int getNavBarHeight() {
Resources res = Resources.getSystem();
int resourceId = res.getIdentifier("navigation_bar_height", "dimen", "android");
if (resourceId != 0) {
return res.getDimensionPixelSize(resourceId);
} else {
return 0;
}
}
/**
* Set the navigation bar's visibility.
*
* @param activity The activity.
* @param isVisible True to set navigation bar visible, false otherwise.
*/
public static void setNavBarVisibility(@NonNull final Activity activity, boolean isVisible) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return;
setNavBarVisibility(activity.getWindow(), isVisible);
}
/**
* Set the navigation bar's visibility.
*
* @param window The window.
* @param isVisible True to set navigation bar visible, false otherwise.
*/
public static void setNavBarVisibility(@NonNull final Window window, boolean isVisible) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return;
final ViewGroup decorView = (ViewGroup) window.getDecorView();
for (int i = 0, count = decorView.getChildCount(); i < count; i++) {
final View child = decorView.getChildAt(i);
final int id = child.getId();
if (id != View.NO_ID) {
String resourceEntryName = FUtils.getApp()
.getResources()
.getResourceEntryName(id);
if ("navigationBarBackground".equals(resourceEntryName)) {
child.setVisibility(isVisible ? View.VISIBLE : View.INVISIBLE);
}
}
}
final int uiOptions = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
if (isVisible) {
decorView.setSystemUiVisibility(decorView.getSystemUiVisibility() & ~uiOptions);
} else {
decorView.setSystemUiVisibility(decorView.getSystemUiVisibility() | uiOptions);
}
}
/**
* Return whether the navigation bar visible.
* <p>Call it in onWindowFocusChanged will get right result.</p>
*
* @param activity The activity.
* @return {@code true}: yes<br>{@code false}: no
*/
public static boolean isNavBarVisible(@NonNull final Activity activity) {
return isNavBarVisible(activity.getWindow());
}
/**
* Return whether the navigation bar visible.
* <p>Call it in onWindowFocusChanged will get right result.</p>
*
* @param window The window.
* @return {@code true}: yes<br>{@code false}: no
*/
public static boolean isNavBarVisible(@NonNull final Window window) {
boolean isVisible = false;
ViewGroup decorView = (ViewGroup) window.getDecorView();
for (int i = 0, count = decorView.getChildCount(); i < count; i++) {
final View child = decorView.getChildAt(i);
final int id = child.getId();
if (id != View.NO_ID) {
String resourceEntryName = FUtils.getApp()
.getResources()
.getResourceEntryName(id);
if ("navigationBarBackground".equals(resourceEntryName)
&& child.getVisibility() == View.VISIBLE) {
isVisible = true;
break;
}
}
}
if (isVisible) {
int visibility = decorView.getSystemUiVisibility();
isVisible = (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0;
}
return isVisible;
}
/**
* Set the navigation bar's color.
*
* @param activity The activity.
* @param color The navigation bar's color.
*/
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
public static void setNavBarColor(@NonNull final Activity activity, @ColorInt final int color) {
setNavBarColor(activity.getWindow(), color);
}
/**
* Set the navigation bar's color.
*
* @param window The window.
* @param color The navigation bar's color.
*/
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
public static void setNavBarColor(@NonNull final Window window, @ColorInt final int color) {
window.setNavigationBarColor(color);
}
/**
* Return the color of navigation bar.
*
* @param activity The activity.
* @return the color of navigation bar
*/
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
public static int getNavBarColor(@NonNull final Activity activity) {
return getNavBarColor(activity.getWindow());
}
/**
* Return the color of navigation bar.
*
* @param window The window.
* @return the color of navigation bar
*/
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
public static int getNavBarColor(@NonNull final Window window) {
return window.getNavigationBarColor();
}
/**
* Return whether the navigation bar visible.
*
* @return {@code true}: yes<br>{@code false}: no
*/
public static boolean isSupportNavBar() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
WindowManager wm = (WindowManager) FUtils.getApp().getSystemService(Context.WINDOW_SERVICE);
if (wm == null) return false;
Display display = wm.getDefaultDisplay();
Point size = new Point();
Point realSize = new Point();
display.getSize(size);
display.getRealSize(realSize);
return realSize.y != size.y || realSize.x != size.x;
}
boolean menu = ViewConfiguration.get(FUtils.getApp()).hasPermanentMenuKey();
boolean back = KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_BACK);
return !menu && !back;
}
private static Activity getActivityByView(@NonNull final View view) {
Context context = view.getContext();
while (context instanceof ContextWrapper) {
if (context instanceof Activity) {
return (Activity) context;
}
context = ((ContextWrapper) context).getBaseContext();
}
Log.e("BarUtils", "the view's Context is not an Activity.");
return null;
}
}

Some files were not shown because too many files have changed in this diff Show More