Android 通过Http实现一个网络速率检测工具实现一个网速测试的一个应用这个应用有以下功能: 1.接口下行速率的检测 ①下行速率检测接口使用http://xxx:xxx/down ②使用http的GET请求请求下载接口当下载数据为10MB为检测完成如TEST_FILE_SIZE 10L * 1024 * 1024 ③检测时要添加进度、失败、成功等数据回调用来通知刷新UI ④速率单位可以为Mbps或MBps支持选择开关动态转换 2.接口上行速率检测 ①上行速率检测接口使用http://xxx:xxx/appupload ②请求时添加以下字段 val fieldName file val fileName aaaa val mimeType application/octet-stream val boundary Boundary-${System.currentTimeMillis()} ②构造一个虚拟文件数据为10M进行上传如 val testData ByteArray(10 * 1024 * 1024) { 0x1 } ③检测时要添加进度、失败、成功等数据回调用来通知刷新UI ④速率单位可以为Mbps或MBps支持选择开关动态转换APP界面速度仪表盘自定义Viewpackage com.dyh.dyhspeedtest import android.animation.ValueAnimator import android.content.Context import android.graphics.* import android.util.AttributeSet import android.view.View import android.view.animation.DecelerateInterpolator import kotlin.math.cos import kotlin.math.min import kotlin.math.sin import androidx.core.graphics.toColorInt /** * 速度仪表盘自定义View * 用于显示网速测试的实时速度 */ class SpeedMeterView JvmOverloads constructor( context: Context, attrs: AttributeSet? null, defStyleAttr: Int 0 ) : View(context, attrs, defStyleAttr) { // 画笔 private val arcBackgroundPaint Paint(Paint.ANTI_ALIAS_FLAG) private val arcProgressPaint Paint(Paint.ANTI_ALIAS_FLAG) private val needlePaint Paint(Paint.ANTI_ALIAS_FLAG) private val textPaint Paint(Paint.ANTI_ALIAS_FLAG) private val scaleTextPaint Paint(Paint.ANTI_ALIAS_FLAG) private val centerCirclePaint Paint(Paint.ANTI_ALIAS_FLAG) // 颜色 private val arcBackgroundColor #E8E8E8.toColorInt() private val arcProgressStartColor #40C4FF.toColorInt() private val arcProgressEndColor #7C4DFF.toColorInt() private val needleColor #7C4DFF.toColorInt() private val textColor #333333.toColorInt() private val scaleTextColor #999999.toColorInt() // 尺寸参数 private var arcWidth 30f private var centerX 0f private var centerY 0f private var radius 0f // 速度相关 private var currentSpeed 0f private var maxSpeed 50f // 最大速度 50 Mbps private var speedUnit Mbps // 动画 private var animator: ValueAnimator? null // 刻度文字 private val scaleTexts arrayOf(0, 5M, 10M, 15M, 20M, 25M, 30M, 35M, 40M, 45M, 50M) // 圆弧角度设置 private val startAngle 135f private val sweepAngle 270f private val arcRect RectF() private var progressGradient: SweepGradient? null init { initPaints() } private fun initPaints() { arcBackgroundPaint.apply { style Paint.Style.STROKE strokeWidth arcWidth color arcBackgroundColor strokeCap Paint.Cap.ROUND } arcProgressPaint.apply { style Paint.Style.STROKE strokeWidth arcWidth strokeCap Paint.Cap.ROUND } needlePaint.apply { style Paint.Style.FILL color needleColor } textPaint.apply { textSize 48f color textColor textAlign Paint.Align.CENTER typeface Typeface.DEFAULT_BOLD } scaleTextPaint.apply { textSize 28f color scaleTextColor textAlign Paint.Align.CENTER } centerCirclePaint.apply { style Paint.Style.FILL color Color.WHITE setShadowLayer(8f, 0f, 2f, Color.parseColor(#20000000)) } } override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) centerX w / 2f centerY h / 2f radius min(w, h) / 2f - arcWidth - 40f arcWidth radius / 8f arcBackgroundPaint.strokeWidth arcWidth arcProgressPaint.strokeWidth arcWidth arcRect.set( centerX - radius, centerY - radius, centerX radius, centerY radius ) // 创建渐变 createGradient() textPaint.textSize radius / 4f scaleTextPaint.textSize radius / 10f } private fun createGradient() { val colors intArrayOf( arcProgressStartColor, arcProgressEndColor, arcProgressEndColor ) val positions floatArrayOf(0f, 0.75f, 1f) progressGradient SweepGradient(centerX, centerY, colors, positions) val matrix Matrix() matrix.setRotate(startAngle, centerX, centerY) progressGradient?.setLocalMatrix(matrix) arcProgressPaint.shader progressGradient } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) // 绘制背景圆弧 canvas.drawArc(arcRect, startAngle, sweepAngle, false, arcBackgroundPaint) // 绘制进度圆弧 val progressSweep (currentSpeed / maxSpeed) * sweepAngle if (progressSweep 0) { canvas.drawArc(arcRect, startAngle, progressSweep, false, arcProgressPaint) } // 绘制刻度文字 drawScaleTexts(canvas) // 绘制指针 drawNeedle(canvas) // 绘制中心速度文字 drawCenterText(canvas) } private fun drawScaleTexts(canvas: Canvas) { val scaleRadius radius arcWidth 25f val totalScales scaleTexts.size for (i in 0 until totalScales) { val angle startAngle (sweepAngle / (totalScales - 1)) * i val radian Math.toRadians(angle.toDouble()) val x centerX scaleRadius * cos(radian).toFloat() val y centerY scaleRadius * sin(radian).toFloat() scaleTextPaint.textSize / 3 canvas.drawText(scaleTexts[i], x, y, scaleTextPaint) } } private fun drawNeedle(canvas: Canvas) { val needleAngle startAngle (currentSpeed / maxSpeed) * sweepAngle val radian Math.toRadians(needleAngle.toDouble()) val needleLength radius - arcWidth val needleEndX centerX needleLength * cos(radian).toFloat() val needleEndY centerY needleLength * sin(radian).toFloat() // 绘制指针线 needlePaint.strokeWidth 6f needlePaint.style Paint.Style.STROKE needlePaint.strokeCap Paint.Cap.ROUND canvas.drawLine(centerX, centerY, needleEndX, needleEndY, needlePaint) // 绘制中心圆 needlePaint.style Paint.Style.FILL canvas.drawCircle(centerX, centerY, 15f, centerCirclePaint) canvas.drawCircle(centerX, centerY, 8f, needlePaint) } private fun drawCenterText(canvas: Canvas) { val speedText String.format(%.1f %s, currentSpeed, speedUnit) val textY centerY radius / 2f canvas.drawText(speedText, centerX, textY, textPaint) } /** * 设置当前速度带动画 */ fun setSpeed(speed: Float, animate: Boolean true) { val targetSpeed speed.coerceIn(0f, maxSpeed) if (animate) { animator?.cancel() animator ValueAnimator.ofFloat(currentSpeed, targetSpeed).apply { duration 500 interpolator DecelerateInterpolator() addUpdateListener { animation - currentSpeed animation.animatedValue as Float invalidate() } start() } } else { currentSpeed targetSpeed invalidate() } } /** * 设置速度单位 */ fun setSpeedUnit(unit: String) { speedUnit unit invalidate() } /** * 设置最大速度 */ fun setMaxSpeed(max: Float) { maxSpeed max updateScaleTexts() invalidate() } private fun updateScaleTexts() { val step maxSpeed / 10 for (i in scaleTexts.indices) { val value (step * i).toInt() scaleTexts[i] if (i 0) 0 else ${value}M } } /** * 重置速度表盘 */ fun reset() { animator?.cancel() currentSpeed 0f invalidate() } override fun onDetachedFromWindow() { super.onDetachedFromWindow() animator?.cancel() } }测速回调接口package com.dyh.dyhspeedtest /** * 测速回调接口 * 用于通知测速进度、成功、失败等状态 */ interface SpeedTestCallback { /** * 测速进度更新 * param progress 进度百分比 (0-100) * param currentSpeedMbps 当前速度 (Mbps) * param downloadedBytes 已传输字节数 * param totalBytes 总字节数 */ fun onProgress(progress: Int, currentSpeedMbps: Double, downloadedBytes: Long, totalBytes: Long) /** * 测速成功完成 * param averageSpeedMbps 平均速度 (Mbps) * param totalBytes 总传输字节数 * param durationMs 总耗时毫秒 */ fun onSuccess(averageSpeedMbps: Double, totalBytes: Long, durationMs: Long) /** * 测速失败 * param errorMessage 错误信息 */ fun onFailure(errorMessage: String) /** * 测速开始 * param testType 测试类型 (DOWNLOAD/UPLOAD) */ fun onStart(testType: TestType) /** * 测速完成无论成功失败 * param testType 测试类型 */ fun onComplete(testType: TestType) } /** * 测试类型枚举 */ enum class TestType { DOWNLOAD, // 下载测试下行速率 UPLOAD // 上传测试上行速率 } /** * 简化的回调适配器可以只重写需要的方法 */ abstract class SimpleSpeedTestCallback : SpeedTestCallback { override fun onProgress(progress: Int, currentSpeedMbps: Double, downloadedBytes: Long, totalBytes: Long) {} override fun onSuccess(averageSpeedMbps: Double, totalBytes: Long, durationMs: Long) {} override fun onFailure(errorMessage: String) {} override fun onStart(testType: TestType) {} override fun onComplete(testType: TestType) {} }网速测试管理类package com.dyh.dyhspeedtest import android.os.Handler import android.os.Looper import android.util.Log import okhttp3.* import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody.Companion.toRequestBody import okio.BufferedSink import java.io.IOException import java.io.InputStream import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean /** * 网速测试管理类 * 负责执行下载和上传测速 */ class SpeedTestManager { companion object { // 下载测试接口 private const val DOWNLOAD_URL XXXXXXXXXXXXXX // 上传测试接口 private const val UPLOAD_URL XXXXXXXXXXXX // 测试文件大小: 500MB (5 * 100 * 1024 * 1024) const val TEST_FILE_SIZE 2L * 1024 * 1024 // 进度更新间隔 (毫秒) private const val PROGRESS_UPDATE_INTERVAL 200L } private val mainHandler Handler(Looper.getMainLooper()) private val client OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(60, TimeUnit.SECONDS) .writeTimeout(60, TimeUnit.SECONDS) .build() private var currentCall: Call? null private val isCancelled AtomicBoolean(false) /** * 开始下载测速 */ fun startDownloadTest(callback: SpeedTestCallback) { isCancelled.set(false) mainHandler.post { callback.onStart(TestType.DOWNLOAD) } val request Request.Builder() .url(DOWNLOAD_URL) .get() .build() currentCall client.newCall(request) currentCall?.enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) { if (!isCancelled.get()) { mainHandler.post { callback.onFailure(下载测试失败: ${e.message}) callback.onComplete(TestType.DOWNLOAD) } } } override fun onResponse(call: Call, response: Response) { if (!response.isSuccessful) { mainHandler.post { callback.onFailure(服务器响应错误: ${response.code}) callback.onComplete(TestType.DOWNLOAD) } return } try { response.body?.let { body - processDownloadResponse(body.byteStream(), body.contentLength(), callback) } ?: run { mainHandler.post { callback.onFailure(响应体为空) callback.onComplete(TestType.DOWNLOAD) } } } catch (e: Exception) { if (!isCancelled.get()) { mainHandler.post { callback.onFailure(处理响应失败: ${e.message}) callback.onComplete(TestType.DOWNLOAD) } } } } }) } private fun processDownloadResponse( inputStream: InputStream, contentLength: Long, callback: SpeedTestCallback ) { val totalBytes if (contentLength 0) contentLength else TEST_FILE_SIZE var downloadedBytes 0L val startTime System.currentTimeMillis() var lastUpdateTime startTime var lastDownloadedBytes 0L val buffer ByteArray(8192) try { inputStream.use { stream - while (!isCancelled.get()) { val bytesRead stream.read(buffer) if (bytesRead -1) break downloadedBytes bytesRead val currentTime System.currentTimeMillis() if (currentTime - lastUpdateTime PROGRESS_UPDATE_INTERVAL) { val intervalBytes downloadedBytes - lastDownloadedBytes val intervalTime currentTime - lastUpdateTime val currentSpeedMbps calculateSpeedMbps(intervalBytes, intervalTime) val progress ((downloadedBytes.toDouble() / totalBytes) * 100).toInt().coerceIn(0, 100) mainHandler.post { callback.onProgress(progress, currentSpeedMbps, downloadedBytes, totalBytes) } lastUpdateTime currentTime lastDownloadedBytes downloadedBytes } // 达到测试大小则停止 if (downloadedBytes TEST_FILE_SIZE) { break } } } if (!isCancelled.get()) { val endTime System.currentTimeMillis() val durationMs endTime - startTime val averageSpeedMbps calculateSpeedMbps(downloadedBytes, durationMs) mainHandler.post { callback.onProgress(100, averageSpeedMbps, downloadedBytes, totalBytes) callback.onSuccess(averageSpeedMbps, downloadedBytes, durationMs) callback.onComplete(TestType.DOWNLOAD) } } } catch (e: Exception) { if (!isCancelled.get()) { mainHandler.post { callback.onFailure(下载过程出错: ${e.message}) callback.onComplete(TestType.DOWNLOAD) } } } } /** * 开始上传测速 */ fun startUploadTest(callback: SpeedTestCallback) { isCancelled.set(false) mainHandler.post { callback.onStart(TestType.UPLOAD) } val fieldName file val fileName aaaa val mimeType application/octet-stream val boundary Boundary-${System.currentTimeMillis()} // 创建测试数据 val testData ByteArray(TEST_FILE_SIZE.toInt()) { 0x1 } // 创建带进度的请求体 val progressRequestBody createProgressRequestBody( testData, mimeType.toMediaType(), callback ) // 构建 multipart 请求 val multipartBody MultipartBody.Builder(boundary) .setType(MultipartBody.FORM) .addFormDataPart(fieldName, fileName, progressRequestBody) .build() val request Request.Builder() .url(UPLOAD_URL) .post(multipartBody) .build() currentCall client.newCall(request) currentCall?.enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) { Log.d(djj, onFailure: ${e.toString()}) // 当出现错误时打印错误堆栈 Log.d(djj, onFailure: ${e.stackTraceToString()}) e.printStackTrace() if (!isCancelled.get()) { mainHandler.post { callback.onFailure(上传测试失败: ${e.message}) callback.onComplete(TestType.UPLOAD) } } } override fun onResponse(call: Call, response: Response) { // 上传完成的处理在 progressRequestBody 中已经完成 response.close() } }) } private fun createProgressRequestBody( data: ByteArray, mediaType: MediaType, callback: SpeedTestCallback ): RequestBody { return object : RequestBody() { private var uploadedBytes 0L private val startTime System.currentTimeMillis() private var lastUpdateTime startTime private var lastUploadedBytes 0L override fun contentType(): MediaType mediaType override fun contentLength(): Long data.size.toLong() override fun writeTo(sink: BufferedSink) { val totalBytes data.size.toLong() var offset 0 val bufferSize 8192 try { while (offset data.size !isCancelled.get()) { val bytesToWrite minOf(bufferSize, data.size - offset) sink.write(data, offset, bytesToWrite) offset bytesToWrite uploadedBytes offset.toLong() val currentTime System.currentTimeMillis() if (currentTime - lastUpdateTime PROGRESS_UPDATE_INTERVAL) { val intervalBytes uploadedBytes - lastUploadedBytes val intervalTime currentTime - lastUpdateTime val currentSpeedMbps calculateSpeedMbps(intervalBytes, intervalTime) val progress ((uploadedBytes.toDouble() / totalBytes) * 100).toInt().coerceIn(0, 100) mainHandler.post { callback.onProgress(progress, currentSpeedMbps, uploadedBytes, totalBytes) } lastUpdateTime currentTime lastUploadedBytes uploadedBytes } } if (!isCancelled.get()) { val endTime System.currentTimeMillis() val durationMs endTime - startTime val averageSpeedMbps calculateSpeedMbps(uploadedBytes, durationMs) mainHandler.post { callback.onProgress(100, averageSpeedMbps, uploadedBytes, totalBytes) callback.onSuccess(averageSpeedMbps, uploadedBytes, durationMs) callback.onComplete(TestType.UPLOAD) } } } catch (e: Exception) { if (!isCancelled.get()) { mainHandler.post { callback.onFailure(上传过程出错: ${e.message}) callback.onComplete(TestType.UPLOAD) } } } } } } /** * 计算速度 (Mbps) * param bytes 字节数 * param timeMs 时间(毫秒) * return 速度 (Mbps) */ private fun calculateSpeedMbps(bytes: Long, timeMs: Long): Double { if (timeMs 0) return 0.0 // 转换为 Mbps: bytes * 8 / 1000000 / (timeMs / 1000) // bytes * 8 * 1000 / 1000000 / timeMs // bytes * 8 / 1000 / timeMs return (bytes * 8.0) / (timeMs * 1000.0) } /** * 将 Mbps 转换为 MBps */ fun mbpsToMBps(mbps: Double): Double { return mbps / 8.0 } /** * 将 MBps 转换为 Mbps */ fun mBpsToMbps(mBps: Double): Double { return mBps * 8.0 } /** * 取消当前测试 */ fun cancel() { isCancelled.set(true) currentCall?.cancel() currentCall null } /** * 是否正在测试中 */ fun isTesting(): Boolean { return currentCall ! null !isCancelled.get() } }其他资源布局及调用方式查看关联资源