内存泄漏之TextLine.recycle

本文提供了一个工具类用于解决Android TextLine组件中存在的内存泄漏问题。该工具类通过反射访问并清空TextLine内部缓存及Span集合,适用于Android 4.0以上版本。
Issue: https://code.google.com/p/android/issues/detail?id=59310

android.text.TextLine has memory leak on mSpanned and SpanSet.

使用下面的工具类移除泄漏:
public class TextLineRecycler {
private static Field sCached;
private static Field sCharacterStyleSpanSet;
private static Field sMetricAffectingSpanSpanSet;
private static Field sReplacementSpanSpanSet;
private static Field sSpans;
private static Field sSpanned;
private static Field sText;

static {
try {
Class clazz = Class.forName("android.text.TextLine");
sCached = clazz.getDeclaredField("sCached");
sSpanned = clazz.getDeclaredField("mSpanned");
sText = clazz.getDeclaredField("mText");
sCached.setAccessible(true);
sSpanned.setAccessible(true);
sText.setAccessible(true);
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
sCharacterStyleSpanSet = clazz.getDeclaredField("mCharacterStyleSpanSet");
sMetricAffectingSpanSpanSet = clazz.getDeclaredField("mMetricAffectingSpanSpanSet");
sReplacementSpanSpanSet = clazz.getDeclaredField("mReplacementSpanSpanSet");
Class spanSetClass = Class.forName("android.text.SpanSet");
sSpans = spanSetClass.getDeclaredField("spans");
sCharacterStyleSpanSet.setAccessible(true);
sMetricAffectingSpanSpanSet.setAccessible(true);
sReplacementSpanSpanSet.setAccessible(true);
sSpans.setAccessible(true);
}
} catch (Exception e) {
Log.e("TextLineRecycler", "", e);
}
}

/**
* recycle system TextLine
*/
public static void recycle() {
try {
Object cached = sCached.get(null);
int length = Array.getLength(cached);
if (cached != null) {
synchronized (cached) {
for (int i = 0; i < length; i++) {
Object textLine = Array.get(cached, i);
if (textLine != null) {
Object text = sText.get(textLine);
// mText has been recycled, recycle mSpanned and other SpanSet here
if (null == text && null != sSpanned.get(textLine)) {
sSpanned.set(textLine, null);
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
recycleSpan(sCharacterStyleSpanSet, textLine);
recycleSpan(sMetricAffectingSpanSpanSet, textLine);
recycleSpan(sReplacementSpanSpanSet, textLine);
}
}
}
}
}
}
} catch (Exception e) {
Log.e("TextLineRecycler", "", e);
}
}

private static <T extends CharacterStyle> void recycleSpan(Field field, Object textLine)
throws IllegalArgumentException, IllegalAccessException {
Object spanSet = field.get(textLine);
T[] spans = (T[]) TextLineRecycler.sSpans.get(spanSet);
if (spans != null) {
for (int i = 0; i < spans.length; i++) {
spans[i] = null;
}
}
}
}
// ======== 1. 初始化 ========== auto.waitFor(); device.keepScreenDim(); setScreenMetrics(1236, 2676); if (!requestScreenCapture()) { toast("❌ 请求截图失败"); exit(); } toast("✅ 脚本启动,请进入列表页..."); // ======== 2. 定义变量 ======== var processedKeys = {}; // 去重缓存 var LIST_PAGE_ID = "recyclerview"; // 列表 RecyclerView ID var TARGET_TEXT_ID = "min_deposit_tv"; // 押金字段 ID var MAX_WAIT_SECONDS = 20; // 等待超时时间 var CSV_FILE_PATH = "/sdcard/AIM/Aac_report.csv"; var successCount = 0; // 成功写入条数 // Java 文件系统支持 var File = java.io.File; var parentDir = new File(CSV_FILE_PATH).getParentFile(); if (!parentDir.exists()) parentDir.mkdirs(); console.info("📁 CSV 日志路径: " + CSV_FILE_PATH); // ======== 3. 所有工具函数定义(必须前置!)======== // ✅ extractNumber —— 提取数字 function extractNumber(text) { if (typeof text === 'undefined' || text === null) return null; var clean = String(text).replace(/[^\d.]/g, ''); var match = clean.match(/^\d*\.?\d+$/); var num = match ? parseFloat(match[0]) : null; return isNaN(num) ? null : num; } // ✅ round —— 四舍五入 function round(num, digits) { if (typeof digits === 'undefined') digits = 2; var factor = Math.pow(10, digits); return Math.round(num * factor) / factor; } // ✅ normalizeString —— 字符串归一化(用于唯一键) function normalizeString(str) { if (typeof str !== 'string') str = String(str); return str.trim().toLowerCase() .replace(/\s+/g, ' ') .replace(/[^\w\u4e00-\u9fa5]/g, ''); // 移除非字母/汉字/数字字符 } // ✅ waitForListPage —— 等待列表页出现 function waitForListPage(timeout) { if (typeof timeout === 'undefined') timeout = MAX_WAIT_SECONDS * 1000; var start_time = new Date().getTime(); while ((new Date().getTime() - start_time) < timeout) { try { if (id(LIST_PAGE_ID).exists()) { var widget = id(LIST_PAGE_ID).findOnce(); if (widget && widget.visibleToUser && widget.visibleToUser()) { log("✅ 成功返回列表页"); return true; } } } catch (err) {} sleep(500); } log("❌ 等待列表页超时"); return false; } // ✅ getTaskName —— 获取物品名称(从详情页获取) function getTaskName() { try { var w = id("tv_order_details_title").findOne(3000); if (!w) { log("❌ 未找到标题控件"); return "未知物品"; } var txt = (w.text && w.text()) || (w.desc && w.desc()) || "未命名"; setClip(txt); // 复制到剪贴板备用 log("📋 名称: " + txt); return txt; } catch (e) { log("⚠️ 获取名称异常: " + e.message); return "异常"; } } // ✅ getSellingPrice —— 获取我的售价 function getSellingPrice() { try { var w = id("tv_selling_price_value").findOne(5000); if (!w) { log("❌ 售卖价控件不存在"); return null; } var rawText = w.text ? w.text() : ""; var price = extractNumber(rawText); if (price === null) { log("❌ 解析失败: " + rawText); } else { log("✅ 我的售价: " + price); } return price; } catch (e) { log("⚠️ 获取售价出错: " + e.message); return null; } } // ✅ getCurrentCategoryContainer —— 获取选中的分类容器 function getCurrentCategoryContainer() { var list = id("item_category_ll").className("android.view.ViewGroup").find(); for (var i = 0; i < list.size(); i++) { var c = list.get(i); try { if (c.clickable() && c.selected()) { return c; } } catch (e) {} } return null; } // ✅ extractPricingFrom —— 从分类项中提取价格 function extractPricingFrom(parent) { try { var children = parent.children(); for (var i = 0; i < children.size(); i++) { var ch = children.get(i); var t = (ch.text && ch.text()) || (ch.desc && ch.desc()) || ""; var num = extractNumber(t); if (num !== null) { return { value: num, rawText: t }; } } } catch (e) { log("⚠️ 提取定价失败: " + e.message); } return null; } // ✅ getWearLevel —— 获取磨损区间 function getWearLevel() { try { var w = id("tv_abrade_filter").findOne(2000); return w ? (w.text() || "未获取") : "未获取"; } catch (e) { return "获取失败"; } } // ✅ swipeUp —— 上滑加载更多 function swipeUp() { var h = device.height; var w = device.width; try { gesture(500, [w / 2, h * 0.7], [w / 2, h * 0.3]); return true; } catch (e) { return false; } } // ===================================== // 🔍 scanAndClickItem —— 扫描并点击符合条件的商品 // ===================================== function scanAndClickItem() { var rv = id(LIST_PAGE_ID).findOne(2000); if (!rv) { log("❌ 找不到 RecyclerView"); return false; } var items = rv.children(); var itemCount = items.size(); for (var i = 0; i < itemCount; i++) { var child = items.get(i); // 🔍 查找押金控件 var depositWidget = child.findOne(id(TARGET_TEXT_ID)); if (!depositWidget) continue; var rawText = depositWidget.text(); var deposit = extractNumber(rawText); if (isNaN(deposit) || deposit <= 500) continue; // 🔍 查找名称控件(列表页上的 name_tv) var nameWidget = child.findOne(id("name_tv")); var itemName = "未知物品"; if (nameWidget) { itemName = (nameWidget.text() || "").trim(); if (!itemName) itemName = (nameWidget.desc() || "").trim(); } if (!itemName || itemName.length === 0) itemName = "未命名-" + deposit; // ✅ 构造唯一键:归一化名称 + 押金(保留两位小数精度) var key = normalizeString(itemName) + "@" + Math.round(deposit * 100); // 检查是否已处理 if (processedKeys[key]) { log("🔁 跳过已处理项: " + itemName + " | ¥" + deposit); continue; } // ✅ 尝试点击 log("🎯 发现新目标: " + itemName + " | 押金¥" + deposit); if (child.click()) { log("✅ 成功点击 → 进入详情页"); processedKeys[key] = true; return true; // 成功点击一个就退出 } else { log("❌ 点击失败,可能被遮挡或不可见"); } } return false; // 未找到可点击项 } // ===================================== // ✅ B阶段:图像识别点击(磨损区间 & 磨损度) // ===================================== function performBitmapClicks() { log("🖼️ B阶段:执行图像识别操作"); sleep(2000); // B1: 点击“磨损区间” if (text("磨损区间").exists()) { var cap = captureScreen(); var tem = images.read("/sdcard/脚本/小图/磨损区间.png"); if (!tem) { log("❌ 无法读取模板图片:磨损区间.png"); } else { var p = images.findImage(cap, tem, { region: [206, 987, 142, 177], threshold: 0.9 }); if (p) { click(p.x + tem.getWidth() / 2, p.y + tem.getHeight() / 2); log("🖱️ 已点击:磨损区间"); sleep(1000); } else { log("🔍 未匹配到:磨损区间"); } cap.recycle(); tem.recycle(); } } // B2: 点击“磨损度” sleep(1000); var cap1 = captureScreen(); var tem1 = images.read("/sdcard/脚本/小图/磨损度.png"); if (!tem1) { log("❌ 无法读取模板图片:磨损度.png"); } else { var p1 = images.findImage(cap1, tem1, { region: [27, 1089, 93, 184], threshold: 0.9 }); if (p1) { click(p1.x + tem1.getWidth() / 2 + 47, p1.y + tem1.getHeight() / 2 + 136); log("🖱️ 已点击:磨损度"); } else { log("🔍 未匹配到:磨损度"); } cap1.recycle(); tem1.recycle(); } } // ===================================== // ✅ C阶段:数据采集与 CSV 记录 // ===================================== function collectAndRecordData() { log("📝 C阶段:采集数据并比较差价"); var taskName = getTaskName(); var sellingPrice = getSellingPrice(); if (!sellingPrice) { toast("⚠️ 售卖价获取失败,跳过"); return false; } var container = getCurrentCategoryContainer(); if (!container) { toast("🚫 未找到选中分类容器"); return false; } var pricing = extractPricingFrom(container); if (!pricing) { toast("⚠️ 分类中无有效定价"); return false; } var categoryPrice = pricing.value; var diff = round(Math.abs(sellingPrice - categoryPrice), 2); var wearLevel = getWearLevel(); // 当差价 > 5时才记录 if (diff > 5) { try { var file = new File(CSV_FILE_PATH); var needsHeader = !file.exists() || file.length() === 0; var writer = open(CSV_FILE_PATH, "a"); if (needsHeader) { writer.write("时间,物品名称,磨损程度,定价,售价,差价\n"); log("📊 创建新 CSV 文件并写入表头"); } // 转义双引号 var escapedName = taskName.replace(/"/g, '""'); var line = [ '"' + escapedName + '"', '"' + wearLevel + '"', categoryPrice.toFixed(2), sellingPrice.toFixed(2), diff.toFixed(2), new Date().toLocaleString(), ].join(",") + "\n"; writer.write(line); writer.close(); successCount++; toastLog( "📌 物品: " + taskName + "\n" + "🔺 差额: " + diff + "\n" + "📈 累计: " + successCount + " 条" ); } catch (e) { log("❌ 写入 CSV 失败: " + (e.message || e.toString())); } } else { log("📉 差价 ≤1,不记录。累计仍为: " + successCount + " 条"); } return true; } // ======== 4. 主循环开始 ========= while (true) { log("🔄 ========== 新一轮循环开始 =========="); // A: Wait for List Page if (!waitForListPage()) { back(); sleep(2000); continue; } // D: Scan and Click var clicked = scanAndClickItem(); if (!clicked) { log("📭 未发现新目标,执行 E 阶段:滑动加载"); if (swipeUp()) { sleep(1500); continue; } else { toastLog("🔚 已到底部,脚本结束"); break; } } // B: Bitmap-based Click performBitmapClicks(); // C: Collect & Compare collectAndRecordData(); // 返回列表 back(); log("🔙 返回列表页..."); if (!waitForListPage()) { toastLog("⏳ 返回超时,脚本终止"); break; } sleep(1000); } 用Es语法修改以上auto脚本,修改写入模式为追加写入
11-11
// ==Auto.js== // @name 【精简版】列表扫描 + 磨损筛选 + CSV记录(ES5) // @desc 去除所有重复代码,仅保留必要逻辑 auto.waitFor(); console.show(); device.keepScreenDim(); setScreenMetrics(1236, 2676); // 请按实际设备修改 // ================ 配置常量 ================ var LIST_PAGE_ID = "recyclerview"; // 列表 RecyclerView ID var DEPOSIT_ID = "min_deposit_tv"; // 在售字段 ID var NAME_ID = "name_tv"; // 商品名称 ID var WEAR_FILTER_ID = "tv_abrade_filter"; // 磨损过滤器 ID var SORT_LIST_ID = "rv_sort_list"; // 排序面板列表 ID var SELLING_PRICE_ID = "tv_selling_price_value"; // 售卖价 ID var CATEGORY_CONTAINER_ID = "item_category_ll"; // 分类容器 ID var MAX_WAIT_MS = 5000; // 控件等待超时 var CSV_PATH = "/sdcard/AIM/Aa1.csv"; // CSV 输出路径 var successCount = 0; // 创建目录 var File = java.io.File; var parentDir = new File(CSV_PATH).getParentFile(); if (!parentDir.exists()) parentDir.mkdirs(); // ================ 工具函数(ES5,仅定义一次) ================ function round(num, digits) { var d = digits || 0; var f = Math.pow(10, d); return Math.round((num + '') * f) / f; } function extractNumber(text) { if (!text) return null; var clean = String(text).replace(/[^\d.]/g, ''); var m = clean.match(/^\d*\.\d+$|^\d+$/); if (!m) return null; var n = parseFloat(m[0]); return isNaN(n) ? null : n; } function normalizeString(str) { if (typeof str !== 'string') str = String(str); return str.trim().toLowerCase() .replace(/\s+/g, ' ') .replace(/[^\w\u4e00-\u9fa5]/g, ''); } function formatDateTime() { var now = new Date(); function pad(n) { return n < 10 ? '0' + n : n; } return now.getFullYear() + " " + pad(now.getHours()) + ":" + pad(now.getMinutes()) + ":" + pad(now.getSeconds()); } function clickCenter(bounds) { if (!bounds) return false; click(bounds.centerX(), bounds.centerY()); sleep(800); return true; } function waitFor(query, timeout) { var start = new Date().getTime(); var t = timeout || MAX_WAIT_MS; while ((new Date().getTime() - start) < t) { try { var w = query.findOne(1000); if (w) return w; } catch (e) {} sleep(300); } return null; } function waitForVisible(idStr) { var start = new Date().getTime(); while ((new Date().getTime() - start) < MAX_WAIT_MS) { try { var w = id(idStr).findOne(1000); if (w && w.visibleToUser && w.visibleToUser()) return w; } catch (e) {} sleep(300); } return null; } // ================ 数据采集函数 ================ function getTaskName() { try { var w = id("tv_order_details_title").findOne(3000); var txt = w ? (w.text() || w.desc() || "未知") : "未知"; log("📋 名称: " + txt); return txt; } catch (e) { log("⚠️ 获取名称失败: " + e.message); return "异常"; } } function getSellingPrice() { var w = waitFor(id(SELLING_PRICE_ID), 3000); if (!w) { log("❌ 未找到售卖价控件"); return null; } var price = extractNumber(w.text()); if (price === null) log("❌ 解析售价失败: " + w.text()); else log("✅ 我的售价: " + price); return price; } function getCurrentCategoryPrice() { var list = id(CATEGORY_CONTAINER_ID).find(); for (var i = 0; i < list.size(); i++) { var c = list.get(i); try { if (c.selected && typeof c.selected === 'function' && c.selected()) { var pricing = extractPricingFrom(c); if (pricing !== null) return pricing; } } catch (e) {} } return null; } function extractPricingFrom(parent) { try { var children = parent.children(); for (var i = 0; i < children.size(); i++) { var ch = children.get(i); var text = (ch.text && ch.text()) || (ch.desc && ch.desc()) || ''; var num = extractNumber(text); if (num !== null && num > 1) return num; } } catch (e) {} return null; } function getWearLevel() { var w = waitFor(id(WEAR_FILTER_ID), 2000); return w ? (w.text() || w.desc() || "未获取") : "未获取"; } function getAbraidFilterRange() { var w = waitFor(id(WEAR_FILTER_ID).className("android.widget.TextView"), 2000); if (!w) return null; var txt = (w.text && w.text()) || (w.desc && w.desc()) || ''; txt = String(txt).trim(); var m = txt.match(/(\d+\.\d+)\s*[-~\u81F3]\s*(\d+\.\d+)/); if (!m) return null; var minW = parseFloat(m[1]), maxW = parseFloat(m[2]); if (isNaN(minW) || isNaN(maxW) || minW >= maxW) return null; return { min: minW, max: maxW }; } // ================ 滑动 & 图像识别辅助 ================ function swipeUp() { var h = device.height, w = device.width; try { gesture(500, [w/2, h*0.7], [w/2, h*0.3]); sleep(1000); return true; } catch (e) { sleep(800); return false; } } function performBitmapClicks() { var cap = captureScreen(); var tem = images.read("/sdcard/脚本/小图/筛选.png"); if (cap && tem) { var p = images.findImage(cap, tem, { region: [957, 293, 242, 67], threshold: 0.9 }); if (!p) { device.setMusicVolume(7); media.playMusic("/sdcard/Music/My love 请别让爱凋落.mp3"); sleep(media.getMusicDuration() || 5000); engines.myEngine().forceStop(); } cap.recycle(); if (tem) tem.recycle(); } } // ================ 核心:磨损筛选逻辑 ================ function processWearFilter() { log("🔧 开始处理磨损筛选..."); // Step 1: 点击【磨损区间】 if (text("磨损区间").exists()) { var widget = text("磨损区间").findOne(2000); if (widget && widget.bounds) { clickCenter(widget.bounds()); log("🖱️ 已点击【磨损区间】"); sleep(1000); } } // Step 2: 等待 rv_sort_list 加载,并点击索引1 var list = waitFor(id(SORT_LIST_ID), 5000); if (!list) { toast("❌ 未找到磨损列表"); return false; } var items = list.children(); if (items.size() <= 5) { toast("❌ 选项不足,无法点击索引1"); return false; } var target1 = items[1]; if (!clickCenter(target1.bounds())) return false; log("✅ 成功点击索引1"); sleep(1000); // Step 3: 循环 step=2 to 4 var initialMinWear = null; var finalMaxWear = null; var categoryPrice = null; var diff = null; for (var step = 2; step <= 4; step++) { var sellingPrice = getSellingPrice(); if (!sellingPrice) break; categoryPrice = getCurrentCategoryPrice(); if (!categoryPrice) break; diff = round(sellingPrice - categoryPrice, 2); log("📊 当前差价: " + diff.toFixed(2)); if (diff <= 5) { log("🔴 差价≤5,终止推进"); break; } if (initialMinWear === null) { var range = getAbraidFilterRange(); if (range) initialMinWear = range.min; } // 重新打开面板 var filter = waitFor(id(WEAR_FILTER_ID), 3000); if (!filter || !clickCenter(filter.bounds())) break; list = waitFor(id(SORT_LIST_ID), 4000); if (!list) break; items = list.children(); if (step >= items.size()) break; var target = items[step]; if (!clickCenter(target.bounds())) break; log("✅ 已点击第 %d 档", step); sleep(1000); var currentRange = getAbraidFilterRange(); if (currentRange) finalMaxWear = currentRange.max; } // 返回是否成功完成且差价 > 5 return finalMaxWear !== null && diff > 5; } // ================ CSV 记录 ================ function writeToCSV(data) { try { var file = new File(CSV_PATH); var needsHeader = !file.exists() || file.length() === 0; var fw = new java.io.FileWriter(file, true); // 追加模式 var bw = new java.io.BufferedWriter(fw); if (needsHeader) { bw.write("物品名称,最小磨损,最大磨损,定价,售价,差价,时间\n"); } var escapedName = data.name.replace(/"/g, '""'); var line = [ '"' + escapedName + '"', '"' + data.wear + '"', data.category.toFixed(2), data.selling.toFixed(2), data.diff.toFixed(2), data.time ].join(",") + "\n"; bw.write(line); bw.close(); fw.close(); successCount++; toastLog("📌 已记录: " + data.name + " | 差价:" + data.diff); } catch (e) { log("❌ CSV写入失败: " + e.message); } } // ================ 主循环 ================ toast("✅ 脚本启动,请进入列表页..."); var processedKeys = {}; while (true) { log("🔄 ========== 新一轮循环开始 =========="); // A: 等待列表页 if (!waitForVisible(LIST_PAGE_ID)) { log("🔙 返回中..."); back(); sleep(2000); continue; } // B: 扫描并点击符合条件的商品 var rv = id(LIST_PAGE_ID).findOne(2000); if (!rv) { log("❌ 未找到商品列表"); continue; } var found = false; var children = rv.children(); for (var i = 0; i < children.size(); i++) { var item = children.get(i); var depositWidget = item.findOne(id(DEPOSIT_ID)); if (!depositWidget) continue; var deposit = extractNumber(depositWidget.text()); if (isNaN(deposit) || deposit <= 500) continue; var nameWidget = item.findOne(id(NAME_ID)); var name = (nameWidget && (nameWidget.text() || nameWidget.desc())) || "未命名-" + deposit; var key = normalizeString(name) + "@" + Math.round(deposit * 100); if (processedKeys[key]) { log("🔁 跳过已处理: " + name); continue; } log("🎯 发现目标: " + name + " | 在售¥" + deposit); if (item.click()) { processedKeys[key] = true; found = true; break; } else { log("❌ 点击失败"); } } if (!found) { log("📭 无新目标,上滑加载"); performBitmapClicks(); // 图像检测音乐提醒 if (swipeUp()) { sleep(1500); continue; } else { toastLog("🔚 已到底部,脚本结束"); break; } } // C: 进入详情页后执行磨损筛选 sleep(2000); var shouldRecord = processWearFilter(); if (shouldRecord) { var name = getTaskName(); var selling = getSellingPrice(); var category = getCurrentCategoryPrice(); var wear = getWearLevel(); var diff = round(selling - category, 2); if (selling && category && diff > 5) { writeToCSV({ name: name, wear: wear, category: category, selling: selling, diff: diff, time: formatDateTime() }); } } else { log("🟡 条件不满足,跳过记录"); } // 返回列表 back(); sleep(1500); } 用ES5语法修改auto脚本
11-11
package com.github.mikephil.charting.renderer; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Paint.Align; import android.graphics.Paint.Style; import android.graphics.Path; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.os.Build; import android.text.Layout; import android.text.StaticLayout; import android.text.TextPaint; import com.github.mikephil.charting.animation.ChartAnimator; import com.github.mikephil.charting.charts.LineChart; import com.github.mikephil.charting.charts.PieChart; import com.github.mikephil.charting.data.Entry; import com.github.mikephil.charting.data.PieData; import com.github.mikephil.charting.data.PieDataSet; import com.github.mikephil.charting.data.PieEntry; import com.github.mikephil.charting.formatter.ValueFormatter; import com.github.mikephil.charting.highlight.Highlight; import com.github.mikephil.charting.interfaces.datasets.IPieDataSet; import com.github.mikephil.charting.utils.ColorTemplate; import com.github.mikephil.charting.utils.MPPointF; import com.github.mikephil.charting.utils.Utils; import com.github.mikephil.charting.utils.ViewPortHandler; import java.lang.ref.WeakReference; import java.util.List; public class PieChartRenderer extends DataRenderer { protected PieChart mChart; /** * paint for the hole in the center of the pie chart and the transparent * circle */ protected Paint mHolePaint; protected Paint mTransparentCirclePaint; protected Paint mValueLinePaint; /** * paint object for the text that can be displayed in the center of the * chart */ private TextPaint mCenterTextPaint; /** * paint object used for drwing the slice-text */ private Paint mEntryLabelsPaint; private StaticLayout mCenterTextLayout; private CharSequence mCenterTextLastValue; private RectF mCenterTextLastBounds = new RectF(); private RectF[] mRectBuffer = {new RectF(), new RectF(), new RectF()}; /** * Bitmap for drawing the center hole */ protected WeakReference<Bitmap> mDrawBitmap; protected Canvas mBitmapCanvas; public PieChartRenderer(PieChart chart, ChartAnimator animator, ViewPortHandler viewPortHandler) { super(animator, viewPortHandler); mChart = chart; mHolePaint = new Paint(Paint.ANTI_ALIAS_FLAG); mHolePaint.setColor(Color.WHITE); mHolePaint.setStyle(Style.FILL); mTransparentCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG); mTransparentCirclePaint.setColor(Color.WHITE); mTransparentCirclePaint.setStyle(Style.FILL); mTransparentCirclePaint.setAlpha(105); mCenterTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); mCenterTextPaint.setColor(Color.BLACK); mCenterTextPaint.setTextSize(Utils.convertDpToPixel(12f)); mValuePaint.setTextSize(Utils.convertDpToPixel(13f)); mValuePaint.setColor(Color.WHITE); mValuePaint.setTextAlign(Align.CENTER); mEntryLabelsPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mEntryLabelsPaint.setColor(Color.WHITE); mEntryLabelsPaint.setTextAlign(Align.CENTER); mEntryLabelsPaint.setTextSize(Utils.convertDpToPixel(13f)); mValueLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG); mValueLinePaint.setStyle(Style.STROKE); } public Paint getPaintHole() { return mHolePaint; } public Paint getPaintTransparentCircle() { return mTransparentCirclePaint; } public TextPaint getPaintCenterText() { return mCenterTextPaint; } public Paint getPaintEntryLabels() { return mEntryLabelsPaint; } @Override public void initBuffers() { // TODO Auto-generated method stub } @Override public void drawData(Canvas c) { int width = (int) mViewPortHandler.getChartWidth(); int height = (int) mViewPortHandler.getChartHeight(); Bitmap drawBitmap = mDrawBitmap == null ? null : mDrawBitmap.get(); if (drawBitmap == null || (drawBitmap.getWidth() != width) || (drawBitmap.getHeight() != height)) { if (width > 0 && height > 0) { drawBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_4444); mDrawBitmap = new WeakReference<>(drawBitmap); mBitmapCanvas = new Canvas(drawBitmap); } else return; } drawBitmap.eraseColor(Color.TRANSPARENT); PieData pieData = mChart.getData(); for (IPieDataSet set : pieData.getDataSets()) { if (set.isVisible() && set.getEntryCount() > 0) drawDataSet(c, set); } } private Path mPathBuffer = new Path(); private RectF mInnerRectBuffer = new RectF(); protected float calculateMinimumRadiusForSpacedSlice( MPPointF center, float radius, float angle, float arcStartPointX, float arcStartPointY, float startAngle, float sweepAngle) { final float angleMiddle = startAngle + sweepAngle / 2.f; // Other point of the arc float arcEndPointX = center.x + radius * (float) Math.cos((startAngle + sweepAngle) * Utils.FDEG2RAD); float arcEndPointY = center.y + radius * (float) Math.sin((startAngle + sweepAngle) * Utils.FDEG2RAD); // Middle point on the arc float arcMidPointX = center.x + radius * (float) Math.cos(angleMiddle * Utils.FDEG2RAD); float arcMidPointY = center.y + radius * (float) Math.sin(angleMiddle * Utils.FDEG2RAD); // This is the base of the contained triangle double basePointsDistance = Math.sqrt( Math.pow(arcEndPointX - arcStartPointX, 2) + Math.pow(arcEndPointY - arcStartPointY, 2)); // After reducing space from both sides of the "slice", // the angle of the contained triangle should stay the same. // So let's find out the height of that triangle. float containedTriangleHeight = (float) (basePointsDistance / 2.0 * Math.tan((180.0 - angle) / 2.0 * Utils.DEG2RAD)); // Now we subtract that from the radius float spacedRadius = radius - containedTriangleHeight; // And now subtract the height of the arc that's between the triangle and the outer circle spacedRadius -= Math.sqrt( Math.pow(arcMidPointX - (arcEndPointX + arcStartPointX) / 2.f, 2) + Math.pow(arcMidPointY - (arcEndPointY + arcStartPointY) / 2.f, 2)); return spacedRadius; } /** * Calculates the sliceSpace to use based on visible values and their size compared to the set sliceSpace. * * @param dataSet * @return */ protected float getSliceSpace(IPieDataSet dataSet) { if (!dataSet.isAutomaticallyDisableSliceSpacingEnabled()) return dataSet.getSliceSpace(); float spaceSizeRatio = dataSet.getSliceSpace() / mViewPortHandler.getSmallestContentExtension(); float minValueRatio = dataSet.getYMin() / mChart.getData().getYValueSum() * 2; float sliceSpace = spaceSizeRatio > minValueRatio ? 0f : dataSet.getSliceSpace(); return sliceSpace; } protected void drawDataSet(Canvas c, IPieDataSet dataSet) { float angle = 0; float rotationAngle = mChart.getRotationAngle(); float phaseX = mAnimator.getPhaseX(); float phaseY = mAnimator.getPhaseY(); final RectF circleBox = mChart.getCircleBox(); final int entryCount = dataSet.getEntryCount(); final float[] drawAngles = mChart.getDrawAngles(); final MPPointF center = mChart.getCenterCircleBox(); final float radius = mChart.getRadius(); final boolean drawInnerArc = mChart.isDrawHoleEnabled() && !mChart.isDrawSlicesUnderHoleEnabled(); final float userInnerRadius = drawInnerArc ? radius * (mChart.getHoleRadius() / 100.f) : 0.f; final float roundedRadius = (radius - (radius * mChart.getHoleRadius() / 100f)) / 2f; final RectF roundedCircleBox = new RectF(); final boolean drawRoundedSlices = drawInnerArc && mChart.isDrawRoundedSlicesEnabled(); int visibleAngleCount = 0; for (int j = 0; j < entryCount; j++) { // draw only if the value is greater than zero if ((Math.abs(dataSet.getEntryForIndex(j).getY()) > Utils.FLOAT_EPSILON)) { visibleAngleCount++; } } final float sliceSpace = visibleAngleCount <= 1 ? 0.f : getSliceSpace(dataSet); for (int j = 0; j < entryCount; j++) { float sliceAngle = drawAngles[j]; float innerRadius = userInnerRadius; Entry e = dataSet.getEntryForIndex(j); // draw only if the value is greater than zero if (!(Math.abs(e.getY()) > Utils.FLOAT_EPSILON)) { angle += sliceAngle * phaseX; continue; } // Don't draw if it's highlighted, unless the chart uses rounded slices if (mChart.needsHighlight(j) && !drawRoundedSlices) { angle += sliceAngle * phaseX; continue; } final boolean accountForSliceSpacing = sliceSpace > 0.f && sliceAngle <= 180.f; mRenderPaint.setColor(dataSet.getColor(j)); final float sliceSpaceAngleOuter = visibleAngleCount == 1 ? 0.f : sliceSpace / (Utils.FDEG2RAD * radius); final float startAngleOuter = rotationAngle + (angle + sliceSpaceAngleOuter / 2.f) * phaseY; float sweepAngleOuter = (sliceAngle - sliceSpaceAngleOuter) * phaseY; if (sweepAngleOuter < 0.f) { sweepAngleOuter = 0.f; } mPathBuffer.reset(); if (drawRoundedSlices) { float x = center.x + (radius - roundedRadius) * (float) Math.cos(startAngleOuter * Utils.FDEG2RAD); float y = center.y + (radius - roundedRadius) * (float) Math.sin(startAngleOuter * Utils.FDEG2RAD); roundedCircleBox.set(x - roundedRadius, y - roundedRadius, x + roundedRadius, y + roundedRadius); } float arcStartPointX = center.x + radius * (float) Math.cos(startAngleOuter * Utils.FDEG2RAD); float arcStartPointY = center.y + radius * (float) Math.sin(startAngleOuter * Utils.FDEG2RAD); if (sweepAngleOuter >= 360.f && sweepAngleOuter % 360f <= Utils.FLOAT_EPSILON) { // Android is doing "mod 360" mPathBuffer.addCircle(center.x, center.y, radius, Path.Direction.CW); } else { if (drawRoundedSlices) { mPathBuffer.arcTo(roundedCircleBox, startAngleOuter + 180, -180); } mPathBuffer.arcTo( circleBox, startAngleOuter, sweepAngleOuter ); } // API < 21 does not receive floats in addArc, but a RectF mInnerRectBuffer.set( center.x - innerRadius, center.y - innerRadius, center.x + innerRadius, center.y + innerRadius); if (drawInnerArc && (innerRadius > 0.f || accountForSliceSpacing)) { if (accountForSliceSpacing) { float minSpacedRadius = calculateMinimumRadiusForSpacedSlice( center, radius, sliceAngle * phaseY, arcStartPointX, arcStartPointY, startAngleOuter, sweepAngleOuter); if (minSpacedRadius < 0.f) minSpacedRadius = -minSpacedRadius; innerRadius = Math.max(innerRadius, minSpacedRadius); } final float sliceSpaceAngleInner = visibleAngleCount == 1 || innerRadius == 0.f ? 0.f : sliceSpace / (Utils.FDEG2RAD * innerRadius); final float startAngleInner = rotationAngle + (angle + sliceSpaceAngleInner / 2.f) * phaseY; float sweepAngleInner = (sliceAngle - sliceSpaceAngleInner) * phaseY; if (sweepAngleInner < 0.f) { sweepAngleInner = 0.f; } final float endAngleInner = startAngleInner + sweepAngleInner; if (sweepAngleOuter >= 360.f && sweepAngleOuter % 360f <= Utils.FLOAT_EPSILON) { // Android is doing "mod 360" mPathBuffer.addCircle(center.x, center.y, innerRadius, Path.Direction.CCW); } else { if (drawRoundedSlices) { float x = center.x + (radius - roundedRadius) * (float) Math.cos(endAngleInner * Utils.FDEG2RAD); float y = center.y + (radius - roundedRadius) * (float) Math.sin(endAngleInner * Utils.FDEG2RAD); roundedCircleBox.set(x - roundedRadius, y - roundedRadius, x + roundedRadius, y + roundedRadius); mPathBuffer.arcTo(roundedCircleBox, endAngleInner, 180); } else mPathBuffer.lineTo( center.x + innerRadius * (float) Math.cos(endAngleInner * Utils.FDEG2RAD), center.y + innerRadius * (float) Math.sin(endAngleInner * Utils.FDEG2RAD)); mPathBuffer.arcTo( mInnerRectBuffer, endAngleInner, -sweepAngleInner ); } } else { if (sweepAngleOuter % 360f > Utils.FLOAT_EPSILON) { if (accountForSliceSpacing) { float angleMiddle = startAngleOuter + sweepAngleOuter / 2.f; float sliceSpaceOffset = calculateMinimumRadiusForSpacedSlice( center, radius, sliceAngle * phaseY, arcStartPointX, arcStartPointY, startAngleOuter, sweepAngleOuter); float arcEndPointX = center.x + sliceSpaceOffset * (float) Math.cos(angleMiddle * Utils.FDEG2RAD); float arcEndPointY = center.y + sliceSpaceOffset * (float) Math.sin(angleMiddle * Utils.FDEG2RAD); mPathBuffer.lineTo( arcEndPointX, arcEndPointY); } else { mPathBuffer.lineTo( center.x, center.y); } } } mPathBuffer.close(); mBitmapCanvas.drawPath(mPathBuffer, mRenderPaint); angle += sliceAngle * phaseX; } MPPointF.recycleInstance(center); } @Override public void drawValues(Canvas c) { MPPointF center = mChart.getCenterCircleBox(); // get whole the radius float radius = mChart.getRadius(); float rotationAngle = mChart.getRotationAngle(); float[] drawAngles = mChart.getDrawAngles(); float[] absoluteAngles = mChart.getAbsoluteAngles(); float phaseX = mAnimator.getPhaseX(); float phaseY = mAnimator.getPhaseY(); final float roundedRadius = (radius - (radius * mChart.getHoleRadius() / 100f)) / 2f; final float holeRadiusPercent = mChart.getHoleRadius() / 100.f; float labelRadiusOffset = radius / 10f * 3.6f; if (mChart.isDrawHoleEnabled()) { labelRadiusOffset = (radius - (radius * holeRadiusPercent)) / 2f; if (!mChart.isDrawSlicesUnderHoleEnabled() && mChart.isDrawRoundedSlicesEnabled()) { // Add curved circle slice and spacing to rotation angle, so that it sits nicely inside rotationAngle += roundedRadius * 360 / (Math.PI * 2 * radius); } } final float labelRadius = radius - labelRadiusOffset; PieData data = mChart.getData(); List<IPieDataSet> dataSets = data.getDataSets(); float yValueSum = data.getYValueSum(); boolean drawEntryLabels = mChart.isDrawEntryLabelsEnabled(); float angle; int xIndex = 0; c.save(); float offset = Utils.convertDpToPixel(5.f); for (int i = 0; i < dataSets.size(); i++) { IPieDataSet dataSet = dataSets.get(i); final boolean drawValues = dataSet.isDrawValuesEnabled(); if (!drawValues && !drawEntryLabels) continue; final PieDataSet.ValuePosition xValuePosition = dataSet.getXValuePosition(); final PieDataSet.ValuePosition yValuePosition = dataSet.getYValuePosition(); // apply the text-styling defined by the DataSet applyValueTextStyle(dataSet); float lineHeight = Utils.calcTextHeight(mValuePaint, "Q") + Utils.convertDpToPixel(4f); ValueFormatter formatter = dataSet.getValueFormatter(); int entryCount = dataSet.getEntryCount(); mValueLinePaint.setColor(dataSet.getValueLineColor()); mValueLinePaint.setStrokeWidth(Utils.convertDpToPixel(dataSet.getValueLineWidth())); final float sliceSpace = getSliceSpace(dataSet); MPPointF iconsOffset = MPPointF.getInstance(dataSet.getIconsOffset()); iconsOffset.x = Utils.convertDpToPixel(iconsOffset.x); iconsOffset.y = Utils.convertDpToPixel(iconsOffset.y); for (int j = 0; j < entryCount; j++) { PieEntry entry = dataSet.getEntryForIndex(j); if (xIndex == 0) angle = 0.f; else angle = absoluteAngles[xIndex - 1] * phaseX; final float sliceAngle = drawAngles[xIndex]; final float sliceSpaceMiddleAngle = sliceSpace / (Utils.FDEG2RAD * labelRadius); // offset needed to center the drawn text in the slice final float angleOffset = (sliceAngle - sliceSpaceMiddleAngle / 2.f) / 2.f; angle = angle + angleOffset; final float transformedAngle = rotationAngle + angle * phaseY; float value = mChart.isUsePercentValuesEnabled() ? entry.getY() / yValueSum * 100f : entry.getY(); String formattedValue = formatter.getPieLabel(value, entry); String entryLabel = entry.getLabel(); final float sliceXBase = (float) Math.cos(transformedAngle * Utils.FDEG2RAD); final float sliceYBase = (float) Math.sin(transformedAngle * Utils.FDEG2RAD); final boolean drawXOutside = drawEntryLabels && xValuePosition == PieDataSet.ValuePosition.OUTSIDE_SLICE; final boolean drawYOutside = drawValues && yValuePosition == PieDataSet.ValuePosition.OUTSIDE_SLICE; final boolean drawXInside = drawEntryLabels && xValuePosition == PieDataSet.ValuePosition.INSIDE_SLICE; final boolean drawYInside = drawValues && yValuePosition == PieDataSet.ValuePosition.INSIDE_SLICE; if (drawXOutside || drawYOutside) { final float valueLineLength1 = dataSet.getValueLinePart1Length(); final float valueLineLength2 = dataSet.getValueLinePart2Length(); final float valueLinePart1OffsetPercentage = dataSet.getValueLinePart1OffsetPercentage() / 100.f; float pt2x, pt2y; float labelPtx, labelPty; float line1Radius; if (mChart.isDrawHoleEnabled()) line1Radius = (radius - (radius * holeRadiusPercent)) * valueLinePart1OffsetPercentage + (radius * holeRadiusPercent); else line1Radius = radius * valueLinePart1OffsetPercentage; final float polyline2Width = dataSet.isValueLineVariableLength() ? labelRadius * valueLineLength2 * (float) Math.abs(Math.sin( transformedAngle * Utils.FDEG2RAD)) : labelRadius * valueLineLength2; final float pt0x = line1Radius * sliceXBase + center.x; final float pt0y = line1Radius * sliceYBase + center.y; final float pt1x = labelRadius * (1 + valueLineLength1) * sliceXBase + center.x; final float pt1y = labelRadius * (1 + valueLineLength1) * sliceYBase + center.y; if (transformedAngle % 360.0 >= 90.0 && transformedAngle % 360.0 <= 270.0) { pt2x = pt1x - polyline2Width; pt2y = pt1y; mValuePaint.setTextAlign(Align.RIGHT); if(drawXOutside) mEntryLabelsPaint.setTextAlign(Align.RIGHT); labelPtx = pt2x - offset; labelPty = pt2y; } else { pt2x = pt1x + polyline2Width; pt2y = pt1y; mValuePaint.setTextAlign(Align.LEFT); if(drawXOutside) mEntryLabelsPaint.setTextAlign(Align.LEFT); labelPtx = pt2x + offset; labelPty = pt2y; } if (dataSet.getValueLineColor() != ColorTemplate.COLOR_NONE) { if (dataSet.isUsingSliceColorAsValueLineColor()) { mValueLinePaint.setColor(dataSet.getColor(j)); } c.drawLine(pt0x, pt0y, pt1x, pt1y, mValueLinePaint); c.drawLine(pt1x, pt1y, pt2x, pt2y, mValueLinePaint); } // draw everything, depending on settings if (drawXOutside && drawYOutside) { drawValue(c, formattedValue, labelPtx, labelPty, dataSet.getValueTextColor(j)); if (j < data.getEntryCount() && entryLabel != null) { drawEntryLabel(c, entryLabel, labelPtx, labelPty + lineHeight); } } else if (drawXOutside) { if (j < data.getEntryCount() && entryLabel != null) { drawEntryLabel(c, entryLabel, labelPtx, labelPty + lineHeight / 2.f); } } else if (drawYOutside) { drawValue(c, formattedValue, labelPtx, labelPty + lineHeight / 2.f, dataSet.getValueTextColor(j)); } } if (drawXInside || drawYInside) { // calculate the text position float x = labelRadius * sliceXBase + center.x; float y = labelRadius * sliceYBase + center.y; mValuePaint.setTextAlign(Align.CENTER); // draw everything, depending on settings if (drawXInside && drawYInside) { drawValue(c, formattedValue, x, y, dataSet.getValueTextColor(j)); if (j < data.getEntryCount() && entryLabel != null) { drawEntryLabel(c, entryLabel, x, y + lineHeight); } } else if (drawXInside) { if (j < data.getEntryCount() && entryLabel != null) { drawEntryLabel(c, entryLabel, x, y + lineHeight / 2f); } } else if (drawYInside) { drawValue(c, formattedValue, x, y + lineHeight / 2f, dataSet.getValueTextColor(j)); } } if (entry.getIcon() != null && dataSet.isDrawIconsEnabled()) { Drawable icon = entry.getIcon(); float x = (labelRadius + iconsOffset.y) * sliceXBase + center.x; float y = (labelRadius + iconsOffset.y) * sliceYBase + center.y; y += iconsOffset.x; Utils.drawImage( c, icon, (int)x, (int)y, icon.getIntrinsicWidth(), icon.getIntrinsicHeight()); } xIndex++; } MPPointF.recycleInstance(iconsOffset); } MPPointF.recycleInstance(center); c.restore(); } @Override public void drawValue(Canvas c, String valueText, float x, float y, int color) { mValuePaint.setColor(color); c.drawText(valueText, x, y, mValuePaint); } /** * Draws an entry label at the specified position. * * @param c * @param label * @param x * @param y */ protected void drawEntryLabel(Canvas c, String label, float x, float y) { c.drawText(label, x, y, mEntryLabelsPaint); } @Override public void drawExtras(Canvas c) { drawHole(c); c.drawBitmap(mDrawBitmap.get(), 0, 0, null); drawCenterText(c); } private Path mHoleCirclePath = new Path(); /** * draws the hole in the center of the chart and the transparent circle / * hole */ protected void drawHole(Canvas c) { if (mChart.isDrawHoleEnabled() && mBitmapCanvas != null) { float radius = mChart.getRadius(); float holeRadius = radius * (mChart.getHoleRadius() / 100); MPPointF center = mChart.getCenterCircleBox(); if (Color.alpha(mHolePaint.getColor()) > 0) { // draw the hole-circle mBitmapCanvas.drawCircle( center.x, center.y, holeRadius, mHolePaint); } // only draw the circle if it can be seen (not covered by the hole) if (Color.alpha(mTransparentCirclePaint.getColor()) > 0 && mChart.getTransparentCircleRadius() > mChart.getHoleRadius()) { int alpha = mTransparentCirclePaint.getAlpha(); float secondHoleRadius = radius * (mChart.getTransparentCircleRadius() / 100); mTransparentCirclePaint.setAlpha((int) ((float) alpha * mAnimator.getPhaseX() * mAnimator.getPhaseY())); // draw the transparent-circle mHoleCirclePath.reset(); mHoleCirclePath.addCircle(center.x, center.y, secondHoleRadius, Path.Direction.CW); mHoleCirclePath.addCircle(center.x, center.y, holeRadius, Path.Direction.CCW); mBitmapCanvas.drawPath(mHoleCirclePath, mTransparentCirclePaint); // reset alpha mTransparentCirclePaint.setAlpha(alpha); } MPPointF.recycleInstance(center); } } protected Path mDrawCenterTextPathBuffer = new Path(); /** * draws the description text in the center of the pie chart makes most * sense when center-hole is enabled */ protected void drawCenterText(Canvas c) { CharSequence centerText = mChart.getCenterText(); if (mChart.isDrawCenterTextEnabled() && centerText != null) { MPPointF center = mChart.getCenterCircleBox(); MPPointF offset = mChart.getCenterTextOffset(); float x = center.x + offset.x; float y = center.y + offset.y; float innerRadius = mChart.isDrawHoleEnabled() && !mChart.isDrawSlicesUnderHoleEnabled() ? mChart.getRadius() * (mChart.getHoleRadius() / 100f) : mChart.getRadius(); RectF holeRect = mRectBuffer[0]; holeRect.left = x - innerRadius; holeRect.top = y - innerRadius; holeRect.right = x + innerRadius; holeRect.bottom = y + innerRadius; RectF boundingRect = mRectBuffer[1]; boundingRect.set(holeRect); float radiusPercent = mChart.getCenterTextRadiusPercent() / 100f; if (radiusPercent > 0.0) { boundingRect.inset( (boundingRect.width() - boundingRect.width() * radiusPercent) / 2.f, (boundingRect.height() - boundingRect.height() * radiusPercent) / 2.f ); } if (!centerText.equals(mCenterTextLastValue) || !boundingRect.equals(mCenterTextLastBounds)) { // Next time we won't recalculate StaticLayout... mCenterTextLastBounds.set(boundingRect); mCenterTextLastValue = centerText; float width = mCenterTextLastBounds.width(); // If width is 0, it will crash. Always have a minimum of 1 mCenterTextLayout = new StaticLayout(centerText, 0, centerText.length(), mCenterTextPaint, (int) Math.max(Math.ceil(width), 1.f), Layout.Alignment.ALIGN_CENTER, 1.f, 0.f, false); } //float layoutWidth = Utils.getStaticLayoutMaxWidth(mCenterTextLayout); float layoutHeight = mCenterTextLayout.getHeight(); c.save(); if (Build.VERSION.SDK_INT >= 18) { Path path = mDrawCenterTextPathBuffer; path.reset(); path.addOval(holeRect, Path.Direction.CW); c.clipPath(path); } c.translate(boundingRect.left, boundingRect.top + (boundingRect.height() - layoutHeight) / 2.f); mCenterTextLayout.draw(c); c.restore(); MPPointF.recycleInstance(center); MPPointF.recycleInstance(offset); } } protected RectF mDrawHighlightedRectF = new RectF(); @Override public void drawHighlighted(Canvas c, Highlight[] indices) { /* Skip entirely if using rounded circle slices, because it doesn't make sense to highlight * in this way. * TODO: add support for changing slice color with highlighting rather than only shifting the slice */ final boolean drawInnerArc = mChart.isDrawHoleEnabled() && !mChart.isDrawSlicesUnderHoleEnabled(); if (drawInnerArc && mChart.isDrawRoundedSlicesEnabled()) return; float phaseX = mAnimator.getPhaseX(); float phaseY = mAnimator.getPhaseY(); float angle; float rotationAngle = mChart.getRotationAngle(); float[] drawAngles = mChart.getDrawAngles(); float[] absoluteAngles = mChart.getAbsoluteAngles(); final MPPointF center = mChart.getCenterCircleBox(); final float radius = mChart.getRadius(); final float userInnerRadius = drawInnerArc ? radius * (mChart.getHoleRadius() / 100.f) : 0.f; final RectF highlightedCircleBox = mDrawHighlightedRectF; highlightedCircleBox.set(0,0,0,0); for (int i = 0; i < indices.length; i++) { // get the index to highlight int index = (int) indices[i].getX(); if (index >= drawAngles.length) continue; IPieDataSet set = mChart.getData() .getDataSetByIndex(indices[i] .getDataSetIndex()); if (set == null || !set.isHighlightEnabled()) continue; final int entryCount = set.getEntryCount(); int visibleAngleCount = 0; for (int j = 0; j < entryCount; j++) { // draw only if the value is greater than zero if ((Math.abs(set.getEntryForIndex(j).getY()) > Utils.FLOAT_EPSILON)) { visibleAngleCount++; } } if (index == 0) angle = 0.f; else angle = absoluteAngles[index - 1] * phaseX; final float sliceSpace = visibleAngleCount <= 1 ? 0.f : set.getSliceSpace(); float sliceAngle = drawAngles[index]; float innerRadius = userInnerRadius; float shift = set.getSelectionShift(); final float highlightedRadius = radius + shift; highlightedCircleBox.set(mChart.getCircleBox()); highlightedCircleBox.inset(-shift, -shift); final boolean accountForSliceSpacing = sliceSpace > 0.f && sliceAngle <= 180.f; mRenderPaint.setColor(set.getColor(index)); final float sliceSpaceAngleOuter = visibleAngleCount == 1 ? 0.f : sliceSpace / (Utils.FDEG2RAD * radius); final float sliceSpaceAngleShifted = visibleAngleCount == 1 ? 0.f : sliceSpace / (Utils.FDEG2RAD * highlightedRadius); final float startAngleOuter = rotationAngle + (angle + sliceSpaceAngleOuter / 2.f) * phaseY; float sweepAngleOuter = (sliceAngle - sliceSpaceAngleOuter) * phaseY; if (sweepAngleOuter < 0.f) { sweepAngleOuter = 0.f; } final float startAngleShifted = rotationAngle + (angle + sliceSpaceAngleShifted / 2.f) * phaseY; float sweepAngleShifted = (sliceAngle - sliceSpaceAngleShifted) * phaseY; if (sweepAngleShifted < 0.f) { sweepAngleShifted = 0.f; } mPathBuffer.reset(); if (sweepAngleOuter >= 360.f && sweepAngleOuter % 360f <= Utils.FLOAT_EPSILON) { // Android is doing "mod 360" mPathBuffer.addCircle(center.x, center.y, highlightedRadius, Path.Direction.CW); } else { mPathBuffer.moveTo( center.x + highlightedRadius * (float) Math.cos(startAngleShifted * Utils.FDEG2RAD), center.y + highlightedRadius * (float) Math.sin(startAngleShifted * Utils.FDEG2RAD)); mPathBuffer.arcTo( highlightedCircleBox, startAngleShifted, sweepAngleShifted ); } float sliceSpaceRadius = 0.f; if (accountForSliceSpacing) { sliceSpaceRadius = calculateMinimumRadiusForSpacedSlice( center, radius, sliceAngle * phaseY, center.x + radius * (float) Math.cos(startAngleOuter * Utils.FDEG2RAD), center.y + radius * (float) Math.sin(startAngleOuter * Utils.FDEG2RAD), startAngleOuter, sweepAngleOuter); } // API < 21 does not receive floats in addArc, but a RectF mInnerRectBuffer.set( center.x - innerRadius, center.y - innerRadius, center.x + innerRadius, center.y + innerRadius); if (drawInnerArc && (innerRadius > 0.f || accountForSliceSpacing)) { if (accountForSliceSpacing) { float minSpacedRadius = sliceSpaceRadius; if (minSpacedRadius < 0.f) minSpacedRadius = -minSpacedRadius; innerRadius = Math.max(innerRadius, minSpacedRadius); } final float sliceSpaceAngleInner = visibleAngleCount == 1 || innerRadius == 0.f ? 0.f : sliceSpace / (Utils.FDEG2RAD * innerRadius); final float startAngleInner = rotationAngle + (angle + sliceSpaceAngleInner / 2.f) * phaseY; float sweepAngleInner = (sliceAngle - sliceSpaceAngleInner) * phaseY; if (sweepAngleInner < 0.f) { sweepAngleInner = 0.f; } final float endAngleInner = startAngleInner + sweepAngleInner; if (sweepAngleOuter >= 360.f && sweepAngleOuter % 360f <= Utils.FLOAT_EPSILON) { // Android is doing "mod 360" mPathBuffer.addCircle(center.x, center.y, innerRadius, Path.Direction.CCW); } else { mPathBuffer.lineTo( center.x + innerRadius * (float) Math.cos(endAngleInner * Utils.FDEG2RAD), center.y + innerRadius * (float) Math.sin(endAngleInner * Utils.FDEG2RAD)); mPathBuffer.arcTo( mInnerRectBuffer, endAngleInner, -sweepAngleInner ); } } else { if (sweepAngleOuter % 360f > Utils.FLOAT_EPSILON) { if (accountForSliceSpacing) { final float angleMiddle = startAngleOuter + sweepAngleOuter / 2.f; final float arcEndPointX = center.x + sliceSpaceRadius * (float) Math.cos(angleMiddle * Utils.FDEG2RAD); final float arcEndPointY = center.y + sliceSpaceRadius * (float) Math.sin(angleMiddle * Utils.FDEG2RAD); mPathBuffer.lineTo( arcEndPointX, arcEndPointY); } else { mPathBuffer.lineTo( center.x, center.y); } } } mPathBuffer.close(); mBitmapCanvas.drawPath(mPathBuffer, mRenderPaint); } MPPointF.recycleInstance(center); } /** * This gives all pie-slices a rounded edge. * * @param c */ protected void drawRoundedSlices(Canvas c) { if (!mChart.isDrawRoundedSlicesEnabled()) return; IPieDataSet dataSet = mChart.getData().getDataSet(); if (!dataSet.isVisible()) return; float phaseX = mAnimator.getPhaseX(); float phaseY = mAnimator.getPhaseY(); MPPointF center = mChart.getCenterCircleBox(); float r = mChart.getRadius(); // calculate the radius of the "slice-circle" float circleRadius = (r - (r * mChart.getHoleRadius() / 100f)) / 2f; float[] drawAngles = mChart.getDrawAngles(); float angle = mChart.getRotationAngle(); for (int j = 0; j < dataSet.getEntryCount(); j++) { float sliceAngle = drawAngles[j]; Entry e = dataSet.getEntryForIndex(j); // draw only if the value is greater than zero if ((Math.abs(e.getY()) > Utils.FLOAT_EPSILON)) { float x = (float) ((r - circleRadius) * Math.cos(Math.toRadians((angle + sliceAngle) * phaseY)) + center.x); float y = (float) ((r - circleRadius) * Math.sin(Math.toRadians((angle + sliceAngle) * phaseY)) + center.y); mRenderPaint.setColor(dataSet.getColor(j)); mBitmapCanvas.drawCircle(x, y, circleRadius, mRenderPaint); } angle += sliceAngle * phaseX; } MPPointF.recycleInstance(center); } /** * Releases the drawing bitmap. This should be called when {@link LineChart#onDetachedFromWindow()}. */ public void releaseBitmap() { if (mBitmapCanvas != null) { mBitmapCanvas.setBitmap(null); mBitmapCanvas = null; } if (mDrawBitmap != null) { Bitmap drawBitmap = mDrawBitmap.get(); if (drawBitmap != null) { drawBitmap.recycle(); } mDrawBitmap.clear(); mDrawBitmap = null; } } } 解释这个代码
12-01
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值