Android 实现自定义折线图控件
前言
日前,有一个“折现图”的需求,如下图所示:
概述
如何自定义折线图?首先将折线图的绘制部分拆分成三部分:
- 原点
- X轴
- Y轴
- 折线
原点
第一步,需要定义出“折线图”原点的位置,由图得:
可以发现,原点的位置由X轴、Y轴所占空间决定:
OriginX:Y轴宽度 OriginY:View高度 - X轴高度
计算Y轴宽度
思路:遍历Y轴的绘制文字,用画笔测量其最大宽度,在加上其左右Margin间距即Y轴宽度
Y轴宽度 = Y轴MarginLeft + Y轴最大文字宽度 + Y轴MariginRight
计算X轴高度
思路:获取X轴画笔FontMetrics,根据其top、bottom计算出X轴文字高度,在加上其上下Margin间距即X轴高度
val fontMetrics = xAxisTextPaint.fontMetrics val lineHeight = fontMetrics.bottom - fontMetrics.top xAxisHeight = lineHeight + xAxisOptions.textMarginTop + xAxisOptions.textMarginBottom
X轴
第二步,根据原点位置,绘制X轴轴线、网格线、文本
绘制轴线
绘制轴线比较简单,沿原点向控件右侧画一条直线即可
if (xAxisOptions.isEnableLine) { xAxisLinePaint.strokeWidth = xAxisOptions.lineWidth xAxisLinePaint.color = xAxisOptions.lineColor xAxisLinePaint.pathEffect = xAxisOptions.linePathEffect canvas.drawLine(originX, originY, width.toFloat(), originY, xAxisLinePaint) }
X轴刻度间隔
在绘制网格线、文本之前需要先计算X轴的刻度间隔:
这里处理的方式比较随意,直接将X轴等分7份即可(因为需要显示近7天的数据)
xGap = (width - originX) / 7
网格线、文本
网格线:只需要根据X轴的刻度,沿Y轴方向依次向控件顶部,画直线即可
文本:文本需要通过画笔,提前测量出待绘制文本的区域,然后计算出居中位置绘制即可
xAxisTexts.forEachIndexed { index, text -> val pointX = originX + index * xGap //刻度线 if (xAxisOptions.isEnableRuler) { xAxisLinePaint.strokeWidth = xAxisOptions.rulerWidth xAxisLinePaint.color = xAxisOptions.rulerColor canvas.drawLine( pointX, originY, pointX, originY - xAxisOptions.rulerHeight, xAxisLinePaint ) } //网格线 if (xAxisOptions.isEnableGrid) { xAxisLinePaint.strokeWidth = xAxisOptions.gridWidth xAxisLinePaint.color = xAxisOptions.gridColor xAxisLinePaint.pathEffect = xAxisOptions.gridPathEffect canvas.drawLine(pointX, originY, pointX, 0f, xAxisLinePaint) } //文本 bounds.setEmpty() xAxisTextPaint.textSize = xAxisOptions.textSize xAxisTextPaint.color = xAxisOptions.textColor xAxisTextPaint.getTextBounds(text, 0, text.length, bounds) val fm = xAxisTextPaint.fontMetrics val fontHeight = fm.bottom - fm.top val fontX = originX + index * xGap + (xGap - bounds.width()) / 2f val fontBaseline = originY + (xAxisHeight - fontHeight) / 2f - fm.top canvas.drawText(text, fontX, fontBaseline, xAxisTextPaint) }
Y轴
第三步:根据原点位置,绘制Y轴轴线、网格线、文本
计算Y轴分布
个人认为,这里是自定义折线图的一个难点,这里经过查阅资料,使用该文章中的算法:
基于JavaScript实现数值型坐标轴刻度计算算法(echarts的y轴刻度计算)
/** * 根据Y轴最大值、数量获取Y轴的标准间隔 */ private fun getYInterval(maxY: Int): Int { val yIntervalCount = yAxisCount - 1 val rawInterval = maxY / yIntervalCount.toFloat() val magicPower = floor(log10(rawInterval.toDouble())) var magic = 10.0.pow(magicPower).toFloat() if (magic == rawInterval) { magic = rawInterval } else { magic *= 10 } val rawStandardInterval = rawInterval / magic val standardInterval = getStandardInterval(rawStandardInterval) * magic return standardInterval.roundToInt() } /** * 根据初始的归一化后的间隔,转化为目标的间隔 */ private fun getStandardInterval(x: Float): Float { return when { x <= 0.1f -> 0.1f x <= 0.2f -> 0.2f x <= 0.25f -> 0.25f x <= 0.5f -> 0.5f x <= 1f -> 1f else -> getStandardInterval(x / 10) * 10 } }
刻度间隔、网格线、文本
Y轴的轴线、网格线、文本剩下的内容与X轴的处理方式几乎一致
//绘制Y轴 //轴线 if (yAxisOptions.isEnableLine) { yAxisLinePaint.strokeWidth = yAxisOptions.lineWidth yAxisLinePaint.color = yAxisOptions.lineColor yAxisLinePaint.pathEffect = yAxisOptions.linePathEffect canvas.drawLine(originX, 0f, originX, originY, yAxisLinePaint) } yAxisTexts.forEachIndexed { index, text -> //刻度线 val pointY = originY - index * yGap if (yAxisOptions.isEnableRuler) { yAxisLinePaint.strokeWidth = yAxisOptions.rulerWidth yAxisLinePaint.color = yAxisOptions.rulerColor canvas.drawLine( originX, pointY, originX + yAxisOptions.rulerHeight, pointY, yAxisLinePaint ) } //网格线 if (yAxisOptions.isEnableGrid) { yAxisLinePaint.strokeWidth = yAxisOptions.gridWidth yAxisLinePaint.color = yAxisOptions.gridColor yAxisLinePaint.pathEffect = yAxisOptions.gridPathEffect canvas.drawLine(originX, pointY, width.toFloat(), pointY, yAxisLinePaint) } //文本 bounds.setEmpty() yAxisTextPaint.textSize = yAxisOptions.textSize yAxisTextPaint.color = yAxisOptions.textColor yAxisTextPaint.getTextBounds(text, 0, text.length, bounds) val fm = yAxisTextPaint.fontMetrics val x = (yAxisWidth - bounds.width()) / 2f val fontHeight = fm.bottom - fm.top val y = originY - index * yGap - fontHeight / 2f - fm.top canvas.drawText(text, x, y, yAxisTextPaint) }
折线
折线的连接,这里使用的是Path,将一个一个坐标点连接,最后将Path绘制,就形成了图中的折线图
//绘制数据 path.reset() points.forEachIndexed { index, point -> val x = originX + index * xGap + xGap / 2f val y = originY - (point.yAxis.toFloat() / yAxisMaxValue) * (yGap * (yAxisCount - 1)) if (index == 0) { path.moveTo(x, y) } else { path.lineTo(x, y) } //圆点 circlePaint.color = dataOptions.circleColor canvas.drawCircle(x, y, dataOptions.circleRadius, circlePaint) } pathPaint.strokeWidth = dataOptions.pathWidth pathPaint.color = dataOptions.pathColor canvas.drawPath(path, pathPaint)
值得注意的是:坐标点X根据间隔是相对确定的,而坐标点Y则需要进行百分比换算
代码
折线图LineChart
package com.vander.pool.widget.linechart import android.content.Context import android.graphics.* import android.text.TextPaint import android.util.AttributeSet import android.view.View import java.text.DecimalFormat import kotlin.math.floor import kotlin.math.log10 import kotlin.math.pow import kotlin.math.roundToInt class LineChart : View { private var options = ChartOptions() /** * X轴相关 */ private val xAxisTextPaint = TextPaint(Paint.ANTI_ALIAS_FLAG) private val xAxisLinePaint = Paint(Paint.ANTI_ALIAS_FLAG) private val xAxisTexts = mutableListOf<String>() private var xAxisHeight = 0f /** * Y轴相关 */ private val yAxisTextPaint = TextPaint(Paint.ANTI_ALIAS_FLAG) private val yAxisLinePaint = Paint(Paint.ANTI_ALIAS_FLAG) private val yAxisTexts = mutableListOf<String>() private var yAxisWidth = 0f private val yAxisCount = 5 private var yAxisMaxValue: Int = 0 /** * 原点 */ private var originX = 0f private var originY = 0f private var xGap = 0f private var yGap = 0f /** * 数据相关 */ private val pathPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { it.style = Paint.Style.STROKE } private val circlePaint = Paint(Paint.ANTI_ALIAS_FLAG).also { it.color = Color.parseColor("#79EBCF") it.style = Paint.Style.FILL } private val points = mutableListOf<ChartBean>() private val bounds = Rect() private val path = Path() constructor(context: Context) : this(context, null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) override fun onDraw(canvas: Canvas) { super.onDraw(canvas) if (points.isEmpty()) return val xAxisOptions = options.xAxisOptions val yAxisOptions = options.yAxisOptions val dataOptions = options.dataOptions //设置原点 originX = yAxisWidth originY = height - xAxisHeight //设置X轴Y轴间隔 xGap = (width - originX) / points.size //Y轴默认顶部会留出一半空间 yGap = originY / (yAxisCount - 1 + 0.5f) //绘制X轴 //轴线 if (xAxisOptions.isEnableLine) { xAxisLinePaint.strokeWidth = xAxisOptions.lineWidth xAxisLinePaint.color = xAxisOptions.lineColor xAxisLinePaint.pathEffect = xAxisOptions.linePathEffect canvas.drawLine(originX, originY, width.toFloat(), originY, xAxisLinePaint) } xAxisTexts.forEachIndexed { index, text -> val pointX = originX + index * xGap //刻度线 if (xAxisOptions.isEnableRuler) { xAxisLinePaint.strokeWidth = xAxisOptions.rulerWidth xAxisLinePaint.color = xAxisOptions.rulerColor canvas.drawLine( pointX, originY, pointX, originY - xAxisOptions.rulerHeight, xAxisLinePaint ) } //网格线 if (xAxisOptions.isEnableGrid) { xAxisLinePaint.strokeWidth = xAxisOptions.gridWidth xAxisLinePaint.color = xAxisOptions.gridColor xAxisLinePaint.pathEffect = xAxisOptions.gridPathEffect canvas.drawLine(pointX, originY, pointX, 0f, xAxisLinePaint) } //文本 bounds.setEmpty() xAxisTextPaint.textSize = xAxisOptions.textSize xAxisTextPaint.color = xAxisOptions.textColor xAxisTextPaint.getTextBounds(text, 0, text.length, bounds) val fm = xAxisTextPaint.fontMetrics val fontHeight = fm.bottom - fm.top val fontX = originX + index * xGap + (xGap - bounds.width()) / 2f val fontBaseline = originY + (xAxisHeight - fontHeight) / 2f - fm.top canvas.drawText(text, fontX, fontBaseline, xAxisTextPaint) } //绘制Y轴 //轴线 if (yAxisOptions.isEnableLine) { yAxisLinePaint.strokeWidth = yAxisOptions.lineWidth yAxisLinePaint.color = yAxisOptions.lineColor yAxisLinePaint.pathEffect = yAxisOptions.linePathEffect canvas.drawLine(originX, 0f, originX, originY, yAxisLinePaint) } yAxisTexts.forEachIndexed { index, text -> //刻度线 val pointY = originY - index * yGap if (yAxisOptions.isEnableRuler) { yAxisLinePaint.strokeWidth = yAxisOptions.rulerWidth yAxisLinePaint.color = yAxisOptions.rulerColor canvas.drawLine( originX, pointY, originX + yAxisOptions.rulerHeight, pointY, yAxisLinePaint ) } //网格线 if (yAxisOptions.isEnableGrid) { yAxisLinePaint.strokeWidth = yAxisOptions.gridWidth yAxisLinePaint.color = yAxisOptions.gridColor yAxisLinePaint.pathEffect = yAxisOptions.gridPathEffect canvas.drawLine(originX, pointY, width.toFloat(), pointY, yAxisLinePaint) } //文本 bounds.setEmpty() yAxisTextPaint.textSize = yAxisOptions.textSize yAxisTextPaint.color = yAxisOptions.textColor yAxisTextPaint.getTextBounds(text, 0, text.length, bounds) val fm = yAxisTextPaint.fontMetrics val x = (yAxisWidth - bounds.width()) / 2f val fontHeight = fm.bottom - fm.top val y = originY - index * yGap - fontHeight / 2f - fm.top canvas.drawText(text, x, y, yAxisTextPaint) } //绘制数据 path.reset() points.forEachIndexed { index, point -> val x = originX + index * xGap + xGap / 2f val y = originY - (point.yAxis.toFloat() / yAxisMaxValue) * (yGap * (yAxisCount - 1)) if (index == 0) { path.moveTo(x, y) } else { path.lineTo(x, y) } //圆点 circlePaint.color = dataOptions.circleColor canvas.drawCircle(x, y, dataOptions.circleRadius, circlePaint) } pathPaint.strokeWidth = dataOptions.pathWidth pathPaint.color = dataOptions.pathColor canvas.drawPath(path, pathPaint) } /** * 设置数据 */ fun setData(list: List<ChartBean>) { points.clear() points.addAll(list) //设置X轴、Y轴数据 setXAxisData(list) setYAxisData(list) invalidate() } /** * 设置X轴数据 */ private fun setXAxisData(list: List<ChartBean>) { val xAxisOptions = options.xAxisOptions val values = list.map { it.xAxis } //X轴文本 xAxisTexts.clear() xAxisTexts.addAll(values) //X轴高度 val fontMetrics = xAxisTextPaint.fontMetrics val lineHeight = fontMetrics.bottom - fontMetrics.top xAxisHeight = lineHeight + xAxisOptions.textMarginTop + xAxisOptions.textMarginBottom } /** * 设置Y轴数据 */ private fun setYAxisData(list: List<ChartBean>) { val yAxisOptions = options.yAxisOptions yAxisTextPaint.textSize = yAxisOptions.textSize yAxisTextPaint.color = yAxisOptions.textColor val texts = list.map { it.yAxis.toString() } yAxisTexts.clear() yAxisTexts.addAll(texts) //Y轴高度 val maxTextWidth = yAxisTexts.maxOf { yAxisTextPaint.measureText(it) } yAxisWidth = maxTextWidth + yAxisOptions.textMarginLeft + yAxisOptions.textMarginRight //Y轴间隔 val maxY = list.maxOf { it.yAxis } val interval = when { maxY <= 10 -> getYInterval(10) else -> getYInterval(maxY) } //Y轴文字 yAxisTexts.clear() for (index in 0..yAxisCount) { val value = index * interval yAxisTexts.add(formatNum(value)) } yAxisMaxValue = (yAxisCount - 1) * interval } /** * 格式化数值 */ private fun formatNum(num: Int): String { val absNum = Math.abs(num) return if (absNum >= 0 && absNum < 1000) { return num.toString() } else { val format = DecimalFormat("0.0") val value = num / 1000f "${format.format(value)}k" } } /** * 根据Y轴最大值、数量获取Y轴的标准间隔 */ private fun getYInterval(maxY: Int): Int { val yIntervalCount = yAxisCount - 1 val rawInterval = maxY / yIntervalCount.toFloat() val magicPower = floor(log10(rawInterval.toDouble())) var magic = 10.0.pow(magicPower).toFloat() if (magic == rawInterval) { magic = rawInterval } else { magic *= 10 } val rawStandardInterval = rawInterval / magic val standardInterval = getStandardInterval(rawStandardInterval) * magic return standardInterval.roundToInt() } /** * 根据初始的归一化后的间隔,转化为目标的间隔 */ private fun getStandardInterval(x: Float): Float { return when { x <= 0.1f -> 0.1f x <= 0.2f -> 0.2f x <= 0.25f -> 0.25f x <= 0.5f -> 0.5f x <= 1f -> 1f else -> getStandardInterval(x / 10) * 10 } } /** * 重置参数 */ fun setOptions(newOptions: ChartOptions) { this.options = newOptions setData(points) } fun getOptions(): ChartOptions { return options } data class ChartBean(val xAxis: String, val yAxis: Int) }
ChartOptions配置选项:
class ChartOptions { //X轴配置 var xAxisOptions = AxisOptions() //Y轴配置 var yAxisOptions = AxisOptions() //数据配置 var dataOptions = DataOptions() } /** * 轴线配置参数 */ class AxisOptions { companion object { private const val DEFAULT_TEXT_SIZE = 20f private const val DEFAULT_TEXT_COLOR = Color.BLACK private const val DEFAULT_TEXT_MARGIN = 20 private const val DEFAULT_LINE_WIDTH = 2f private const val DEFAULT_RULER_WIDTH = 10f } /** * 文字大小 */ @FloatRange(from = 1.0) var textSize: Float = DEFAULT_TEXT_SIZE @ColorInt var textColor: Int = DEFAULT_TEXT_COLOR /** * X轴文字内容上下两侧margin */ var textMarginTop: Int = DEFAULT_TEXT_MARGIN var textMarginBottom: Int = DEFAULT_TEXT_MARGIN /** * Y轴文字内容左右两侧margin */ var textMarginLeft: Int = DEFAULT_TEXT_MARGIN var textMarginRight: Int = DEFAULT_TEXT_MARGIN /** * 轴线 */ var lineWidth: Float = DEFAULT_LINE_WIDTH @ColorInt var lineColor: Int = DEFAULT_TEXT_COLOR var isEnableLine = true var linePathEffect: PathEffect? = null /** * 刻度 */ var rulerWidth = DEFAULT_LINE_WIDTH var rulerHeight = DEFAULT_RULER_WIDTH @ColorInt var rulerColor = DEFAULT_TEXT_COLOR var isEnableRuler = true /** * 网格 */ var gridWidth: Float = DEFAULT_LINE_WIDTH @ColorInt var gridColor: Int = DEFAULT_TEXT_COLOR var gridPathEffect: PathEffect? = null var isEnableGrid = true } /** * 数据配置参数 */ class DataOptions { companion object { private const val DEFAULT_PATH_WIDTH = 2f private const val DEFAULT_PATH_COLOR = Color.BLACK private const val DEFAULT_CIRCLE_RADIUS = 10f private const val DEFAULT_CIRCLE_COLOR = Color.BLACK } var pathWidth = DEFAULT_PATH_WIDTH var pathColor = DEFAULT_PATH_COLOR var circleRadius = DEFAULT_CIRCLE_RADIUS var circleColor = DEFAULT_CIRCLE_COLOR }
Demo样式:
private fun initView() { val options = binding.chart.getOptions() //X轴 val xAxisOptions = options.xAxisOptions xAxisOptions.isEnableLine = false xAxisOptions.textColor = Color.parseColor("#999999") xAxisOptions.textSize = dpToPx(12) xAxisOptions.textMarginTop = dpToPx(12).toInt() xAxisOptions.textMarginBottom = dpToPx(12).toInt() xAxisOptions.isEnableGrid = false xAxisOptions.isEnableRuler = false //Y轴 val yAxisOptions = options.yAxisOptions yAxisOptions.isEnableLine = false yAxisOptions.textColor = Color.parseColor("#999999") yAxisOptions.textSize = dpToPx(12) yAxisOptions.textMarginLeft = dpToPx(12).toInt() yAxisOptions.textMarginRight = dpToPx(12).toInt() yAxisOptions.gridColor = Color.parseColor("#999999") yAxisOptions.gridWidth = dpToPx(0.5f) val dashLength = dpToPx(8f) yAxisOptions.gridPathEffect = DashPathEffect(floatArrayOf(dashLength, dashLength / 2), 0f) yAxisOptions.isEnableRuler = false //数据 val dataOptions = options.dataOptions dataOptions.pathColor = Color.parseColor("#79EBCF") dataOptions.pathWidth = dpToPx(1f) dataOptions.circleColor = Color.parseColor("#79EBCF") dataOptions.circleRadius = dpToPx(3f) binding.chart.setOnClickListener { initChartData() } binding.toolbar.setLeftClick { finish() } } private fun initChartData() { val random = 1000 val list = mutableListOf<LineChart.ChartBean>() list.add(LineChart.ChartBean("05-01", Random.nextInt(random))) list.add(LineChart.ChartBean("05-02", Random.nextInt(random))) list.add(LineChart.ChartBean("05-03", Random.nextInt(random))) list.add(LineChart.ChartBean("05-04", Random.nextInt(random))) list.add(LineChart.ChartBean("05-05", Random.nextInt(random))) list.add(LineChart.ChartBean("05-06", Random.nextInt(random))) list.add(LineChart.ChartBean("05-07", Random.nextInt(random))) binding.chart.setData(list) //文本 val text = list.joinToString("\n") { "x : ${it.xAxis} y:${it.yAxis}" } binding.value.text = text }
到此这篇关于Android 实现自定义折线图控件的文章就介绍到这了,更多相关Android折线图控件内容请搜索猪先飞以前的文章或继续浏览下面的相关文章希望大家以后多多支持猪先飞!
原文出处:https://juejin.cn/post/7105243451037319204
相关文章
- 下面我们来看一篇关于Android子控件超出父控件的范围显示出来方法,希望这篇文章能够帮助到各位朋友,有碰到此问题的朋友可以进来看看哦。 <RelativeLayout xmlns:an...2016-10-02
- 这篇文章主要给大家介绍了关于C#创建自定义控件及添加自定义属性和事件使用的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用C#具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧...2020-06-25
- 本文实例讲述了JS实现自定义简单网页软键盘效果。分享给大家供大家参考,具体如下:这是一款自定义的简单点的网页软键盘,没有使用任何控件,仅是为了练习JavaScript编写水平,安全性方面没有过多考虑,有顾虑的可以不用,目的是学...2015-11-08
Android开发中findViewById()函数用法与简化
findViewById方法在android开发中是获取页面控件的值了,有没有发现我们一个页面控件多了会反复研究写findViewById呢,下面我们一起来看它的简化方法。 Android中Fin...2016-09-20- 如果我们的项目需要做来电及短信的功能,那么我们就得在Android模拟器开发这些功能,本来就来告诉我们如何在Android模拟器上模拟来电及来短信的功能。 在Android模拟...2016-09-20
- 夜神android模拟器如何设置代理呢?对于这个问题其实操作起来是非常的简单,下面小编来为各位详细介绍夜神android模拟器设置代理的方法,希望例子能够帮助到各位。 app...2016-09-20
- 为了增强android应用的用户体验,我们可以在一些Button按钮上自定义动态的设置一些样式,比如交互时改变字体、颜色、背景图等。 今天来看一个通过重写Button来动态实...2016-09-20
- 这篇文章主要介绍了C#实现跨线程操作控件方法,主要采用异步访问方式实现,需要的朋友可以参考下...2020-06-25
- 如果我们要在Android应用APP中加载html5页面,我们可以使用WebView,本文我们分享两个WebView加载html5页面实例应用。 实例一:WebView加载html5实现炫酷引导页面大多...2016-09-20
- 深入理解Android中View和ViewGroup从组成架构上看,似乎ViewGroup在View之上,View需要继承ViewGroup,但实际上不是这样的。View是基类,ViewGroup是它的子类。本教程我们深...2016-09-20
- 下面我们来看一篇关于Android自定义WebView网络视频播放控件开发例子,这个文章写得非常的不错下面给各位共享一下吧。 因为业务需要,以下代码均以Youtube网站在线视...2016-10-02
- 自定义一个jquery模态窗口插件,将它集成到现有平台框架中时,它只能在mainFrame窗口中显示,无法在顶层窗口显示. 解决这个问题的办法: 通过以下代码就可能实现在顶层窗口弹窗 复制代码 代码如下: $(window.top.documen...2014-05-31
- java开发的Android应用,性能一直是一个大问题,,或许是Java语言本身比较消耗内存。本文我们来谈谈Android 性能优化之MemoryFile文件读写。 Android匿名共享内存对外A...2016-09-20
- TextView默认是横着显示了,今天我们一起来看看Android设置TextView竖着显示如何来实现吧,今天我们就一起来看看操作细节,具体的如下所示。 在开发Android程序的时候,...2016-10-02
- 这篇文章主要介绍了自定义feignClient的常见坑及解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教...2021-10-20
- 今天小编就为大家分享一篇pytorch 自定义卷积核进行卷积操作方式,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-05-06
android.os.BinderProxy cannot be cast to com解决办法
本文章来给大家介绍关于android.os.BinderProxy cannot be cast to com解决办法,希望此文章对各位有帮助呀。 Android在绑定服务的时候出现java.lang.ClassCastExc...2016-09-20- 这篇文章主要介绍了Android 实现钉钉自动打卡功能的步骤,帮助大家更好的理解和学习使用Android,感兴趣的朋友可以了解下...2021-03-15
PHP YII框架开发小技巧之模型(models)中rules自定义验证规则
YII的models中的rules部分是一些表单的验证规则,对于表单验证十分有用,在相应的视图(views)里面添加了表单,在表单被提交之前程序都会自动先来这里面的规则里验证,只有通过对其有效的限制规则后才能被提交,可以很有效地保证...2015-11-24- 这篇文章主要介绍了jquery自定义插件开发之window的实现过程的相关资料,需要的朋友可以参考下...2016-05-09