Android自定义控件--时间标尺控件

1、自定义刻度尺控件

     时间标尺可以左右滑动,并支持手势缩放

     点击日期,时间标尺可以跳转到对应的的日期的00:00

     点击实时,时间标尺可以跳转到当前时间

     实现的最终效果如下图所示:

 

2、在values文件夹下新建attrs_time_rule_pressure.xml文件,设置自定义属性:



    
    
    
    
    
    
    #eb4c1c
    
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
    

3、创建TimeRulePressure.java文件:

package com.sharetronic.ffmpegvideo.utils;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.os.Build;
import android.support.annotation.IntRange;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.Scroller;

import com.sharetronic.ffmpegvideo.BuildConfig;
import com.sharetronic.ffmpegvideo.R;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

/**
 * TimeRulePressure
 * 

* 时间尺控件 *

* 功能: * - 可选择多天(00:00 ~ 24:00)内的任一时刻,精确到秒级 * - 可显示多个时间块 * - 支持滑动及惯性滑动 * - 支持缩放时间间隔 * - 支持滑动与缩放的连续切换 *

* 思路: * - 时间绘制思路参考{@link } * - 时间缩放,采用缩放手势检测器 ScaleGestureDetector * - 缩放的等级估算方式:进入默认比例为1,根据每隔所占的秒数与宽度,可估算出每个等级的宽度范围,再与默认等级对应的宽度相除,即可算出缩放比例 * - 惯性滑动,使用速度追踪器 VelocityTracker * - 缩放与滑动之间的连续操作,ScaleGestureDetector 开始与结束的条件是第二个手指按下与松开, * 所以onTouchEvent()中应该使用 getActionMasked()来监听第二个手指的 DOWN(ACTION_POINTER_DOWN) 与 UP(ACTION_POINTER_UP) 事件, * MOVE 都是一样的 * - 时间块,由起始时间与终止时间组成,采用一个有序的集合来装入即可 *

* Author: tiger-Y * Description: * Date 2018/12/21 */ public class TimeRulePressure extends View { private static final boolean LOG_ENABLE = BuildConfig.DEBUG; public static final int MAX_TIME_VALUE = 24 * 3600; private int bgColor; /** * 刻度颜色 */ private int gradationColor; /** * 时间块的高度 */ private float partHeight; /** * 时间块的颜色 */ private int partColor; /** * 刻度宽度 */ private float gradationWidth; /** * 秒、分、时刻度的长度 */ private float secondLen; private float minuteLen; private float hourLen; /** * 刻度数值颜色、大小、与时刻度的距离 */ private int gradationTextColor; private float gradationTextSize; private float gradationTextGap; /** * 当前时间,单位:s */ private @IntRange(from = 0, to = MAX_TIME_VALUE) int currentTime; /** * 指针颜色 */ private int indicatorColor; /** * 指针上三角形的边长 */ private float indicatorTriangleSideLen; /** * 指针的宽度 */ private float indicatorWidth; /** * 最小单位对应的单位秒数值,一共四级: 10s、1min、5min、15min * 与 {@link #mPerTextCounts} 和 {@link #mPerCountScaleThresholds} 对应的索引值 *

* 可以组合优化成数组 */ private static int[] mUnitSeconds = { 10, 10, 10, 10, 60, 60, 5 * 60, 5 * 60, 15 * 60, 15 * 60, 15 * 60, 15 * 60, 15 * 60, 15 * 60 }; /** * 数值显示间隔。一共13级,第一级最大值,不包括 */ @SuppressWarnings("all") private static int[] mPerTextCounts = { 60, 60, 2 * 60, 4 * 60, // 10s/unit: 最大值, 1min, 2min, 4min 5 * 60, 10 * 60, // 1min/unit: 5min, 10min 20 * 60, 30 * 60, // 5min/unit: 20min, 30min 3600, 2 * 3600, 3 * 3600, 4 * 3600, 5 * 3600, 6 * 3600 // 15min/unit }; /** * 与 {@link #mPerTextCounts} 对应的阈值,在此阈值与前一个阈值之间,则使用此阈值对应的间隔数值 * 如:1.5f 代表 4*60 对应的阈值,如果 mScale >= 1.5f && mScale < 1.8f,则使用 4*60 *

* 这些数值,都是估算出来的 */ @SuppressWarnings("all") private float[] mPerCountScaleThresholds = { 6f, 3.6f, 1.8f, 1.5f, // 10s/unit: 最大值, 1min, 2min, 4min 0.8f, 0.4f, // 1min/unit: 5min, 10min 0.25f, 0.125f, // 5min/unit: 20min, 30min 0.07f, 0.04f, 0.03f, 0.025f, 0.02f, 0.015f // 15min/unit: 1h, 2h, 3h, 4h, 5h, 6h }; /** * 默认mScale为1 */ private float mScale = 1; /** * 1s对应的间隔,比较好估算 */ private final float mOneSecondGap = dp2px(12) / 60f; /** * 当前最小单位秒数值对应的间隔 */ private float mUnitGap = mOneSecondGap * 60; /** * 默认索引值 */ private int mPerTextCountIndex = 4; /** * 一格代表的秒数。默认1min */ private int mUnitSecond = mUnitSeconds[mPerTextCountIndex]; /** * 数值文字宽度的一半:时间格式为“00:00”,所以长度固定 */ private final float mTextHalfWidth; private final int SCROLL_SLOP; private final int MIN_VELOCITY; private final int MAX_VELOCITY; /** * 当前时间与 00:00 的距离值 */ private float mCurrentDistance; private Paint mPaint; private TextPaint mTextPaint; private Path mTrianglePath; private Scroller mScroller; private VelocityTracker mVelocityTracker; /** * 缩放手势检测器 */ private ScaleGestureDetector mScaleGestureDetector; /** * 日期列表 */ private ArrayList dateArray; /** * 日期布局的高度 */ private float dateHeight; /** * 日期控件字体、高度、宽度、颜色、控件背景颜色 */ private float dateTextSize; private float dateItemHeight; private float dateItemWidth; private int dateItemColor; private int dateBgColor; /** * 被点击的日期控件索引 */ private int dateClickIndex = 0; /** * 日期间控件的间隔 */ private float dateItemToItemDistance; /** * 总时间,日期的天数 * 一天的秒数 */ // private int TOTAL_TIME_VALUE = MAX_TIME_VALUE * dateArray.size(); private int mWidth, mHeight; private int mHalfWidth; private int mInitialX; private int mLastX, mLastY; private boolean isMoving; private boolean isScaling; private List mTimePartList; private OnTimeChangedListener mListener; public interface OnTimeChangedListener { void onTimeChanged(int newTimeValue); } /** * 时间片段 */ public static class TimePart { /** * 起始时间,单位:s,取值范围∈[0, 86399] * 0 —— 00:00:00 * 86399 —— 23:59:59 */ public int startTime; /** * 结束时间,必须大于{@link #startTime} */ public int endTime; } public TimeRulePressure(Context context) { this(context, null); } public TimeRulePressure(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public TimeRulePressure(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initAttrs(context, attrs); init(context); initScaleGestureDetector(context); mTextHalfWidth = mTextPaint.measureText("00:00") * .5f; ViewConfiguration viewConfiguration = ViewConfiguration.get(context); SCROLL_SLOP = viewConfiguration.getScaledTouchSlop(); MIN_VELOCITY = viewConfiguration.getScaledMinimumFlingVelocity(); MAX_VELOCITY = viewConfiguration.getScaledMaximumFlingVelocity(); calculateValues(); } private void initAttrs(Context context, AttributeSet attrs) { TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TimeRulePressure); bgColor = ta.getColor(R.styleable.TimeRulePressure_rule_bgColor, Color.parseColor("#EEEEEE")); gradationColor = ta.getColor(R.styleable.TimeRulePressure_rule_gradationColor, Color.GRAY); partHeight = ta.getDimension(R.styleable.TimeRulePressure_trv_partHeight, dp2px(20)); partColor = ta.getColor(R.styleable.TimeRulePressure_trv_partColor, Color.parseColor("#F58D24")); gradationWidth = ta.getDimension(R.styleable.TimeRulePressure_trv_gradationWidth, 1); secondLen = ta.getDimension(R.styleable.TimeRulePressure_trv_secondLen, dp2px(3)); minuteLen = ta.getDimension(R.styleable.TimeRulePressure_trv_minuteLen, dp2px(5)); hourLen = ta.getDimension(R.styleable.TimeRulePressure_trv_hourLen, dp2px(10)); gradationTextColor = ta.getColor(R.styleable.TimeRulePressure_trv_gradationTextColor, Color.parseColor("#FF888888")); gradationTextSize = ta.getDimension(R.styleable.TimeRulePressure_trv_gradationTextSize, sp2px(12)); gradationTextGap = ta.getDimension(R.styleable.TimeRulePressure_trv_gradationTextGap, dp2px(2)); currentTime = ta.getInt(R.styleable.TimeRulePressure_trv_currentTime, 0); indicatorTriangleSideLen = ta.getDimension(R.styleable.TimeRulePressure_trv_indicatorTriangleSideLen, dp2px(15)); indicatorWidth = ta.getDimension(R.styleable.TimeRulePressure_rule_indicatorLineWidth, dp2px(1)); indicatorColor = ta.getColor(R.styleable.TimeRulePressure_rule_indicatorLineColor, Color.RED); dateHeight = ta.getDimension(R.styleable.TimeRulePressure_trv_dateLayoutHeight, dp2px(40)); dateTextSize = ta.getDimension(R.styleable.TimeRulePressure_trv_dateTextSize, sp2px(12)); dateItemHeight = ta.getDimension(R.styleable.TimeRulePressure_trv_dateHeight, dp2px(20)); dateItemWidth = ta.getDimension(R.styleable.TimeRulePressure_trv_dateWidth, dp2px(40)); dateItemColor = ta.getColor(R.styleable.TimeRulePressure_trv_dateTextColor, Color.parseColor("#FF888888")); dateBgColor = ta.getColor(R.styleable.TimeRulePressure_trv_dateBgColor, Color.parseColor("#AE592D")); dateItemToItemDistance = ta.getDimension(R.styleable.TimeRulePressure_trv_dateDistance, dp2px(20)); ta.recycle(); } private void calculateValues() { mCurrentDistance = currentTime / mUnitSecond * mUnitGap; } private void init(Context context) { mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); mTextPaint.setTextSize(gradationTextSize); mTextPaint.setColor(gradationTextColor); mTrianglePath = new Path(); mScroller = new Scroller(context); //日期 dateArray = new ArrayList<>(); dateArray.add("12/19"); dateArray.add("12/20"); dateArray.add(" 实时"); } private void initScaleGestureDetector(Context context) { mScaleGestureDetector = new ScaleGestureDetector(context, new ScaleGestureDetector.OnScaleGestureListener() { /** * 缩放被触发(会调用0次或者多次), * 如果返回 true 则表示当前缩放事件已经被处理,检测器会重新积累缩放因子 * 返回 false 则会继续积累缩放因子。 */ @Override public boolean onScale(ScaleGestureDetector detector) { final float scaleFactor = detector.getScaleFactor(); logD("onScale...focusX=%f, focusY=%f, scaleFactor=%f", detector.getFocusX(), detector.getFocusY(), scaleFactor); final float maxScale = mPerCountScaleThresholds[0]; final float minScale = mPerCountScaleThresholds[mPerCountScaleThresholds.length - 1]; if (scaleFactor > 1 && mScale >= maxScale) { // 已经放大到最大值 return true; } else if (scaleFactor < 1 && mScale <= minScale) { // 已经缩小到最小值 return true; } mScale *= scaleFactor; mScale = Math.max(minScale, Math.min(maxScale, mScale)); mPerTextCountIndex = findScaleIndex(mScale); mUnitSecond = mUnitSeconds[mPerTextCountIndex]; mUnitGap = mScale * mOneSecondGap * mUnitSecond; logD("onScale: mScale=%f, mPerTextCountIndex=%d, mUnitSecond=%d, mUnitGap=%f", mScale, mPerTextCountIndex, mUnitSecond, mUnitGap); mCurrentDistance = (float) currentTime / mUnitSecond * mUnitGap; invalidate(); return true; } @Override public boolean onScaleBegin(ScaleGestureDetector detector) { logD("onScaleBegin..."); isScaling = true; return true; } @Override public void onScaleEnd(ScaleGestureDetector detector) { isScaling = false; logD("onScaleEnd..."); } }); // 调整最小跨度值。默认值27mm(>=sw600dp的32mm),太大了,效果不好 Class clazz = ScaleGestureDetector.class; int newMinSpan = ViewConfiguration.get(context).getScaledTouchSlop(); try { Field mMinSpanField = clazz.getDeclaredField("mMinSpan"); mMinSpanField.setAccessible(true); mMinSpanField.set(mScaleGestureDetector, newMinSpan); mMinSpanField.setAccessible(false); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } /** * 二分法查找缩放值对应的索引值 */ private int findScaleIndex(float scale) { final int size = mPerCountScaleThresholds.length; int min = 0; int max = size - 1; int mid = (min + max) >> 1; while (!(scale >= mPerCountScaleThresholds[mid] && scale < mPerCountScaleThresholds[mid - 1])) { if (scale >= mPerCountScaleThresholds[mid - 1]) { // 因为值往小区,index往大取,所以不能为mid -1 max = mid; } else { min = mid + 1; } mid = (min + max) >> 1; if (min >= max) { break; } if (mid == 0) { break; } } return mid; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { mWidth = MeasureSpec.getSize(widthMeasureSpec); mHeight = MeasureSpec.getSize(heightMeasureSpec); // 只处理wrap_content的高度,设置为80dp if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) { mHeight = dp2px(60); } mHalfWidth = mWidth >> 1; setMeasuredDimension(mWidth, mHeight); } @Override public boolean onTouchEvent(MotionEvent event) { final int actionIndex = event.getActionIndex(); int pointerId = event.getPointerId(actionIndex); final int actionMasked = event.getActionMasked(); final int action = event.getAction(); final int pointerCount = event.getPointerCount(); logD("onTouchEvent: isScaling=%b, actionIndex=%d, pointerId=%d, actionMasked=%d, action=%d, pointerCount=%d", isScaling, actionIndex, pointerId, actionMasked, action, pointerCount); final int x = (int) event.getX(); final int y = (int) event.getY(); mScaleGestureDetector.onTouchEvent(event); if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(event); switch (actionMasked) { case MotionEvent.ACTION_DOWN: //点击日期控件 eventDateClicked(event); isMoving = false; mInitialX = x; if (!mScroller.isFinished()) { mScroller.forceFinished(true); } break; case MotionEvent.ACTION_POINTER_DOWN: // 只要第二手指按下,就禁止滑动 isScaling = true; isMoving = false; break; case MotionEvent.ACTION_MOVE: if (isScaling) { break; } int dx = x - mLastX; if (!isMoving) { final int dy = y - mLastY; if (Math.abs(x - mInitialX) <= SCROLL_SLOP || Math.abs(dx) <= Math.abs(dy)) { break; } isMoving = true; } mCurrentDistance -= dx; computeTime(); break; case MotionEvent.ACTION_UP: if (isScaling || !isMoving) { break; } mVelocityTracker.computeCurrentVelocity(1000, MAX_VELOCITY); final int xVelocity = (int) mVelocityTracker.getXVelocity(); if (Math.abs(xVelocity) >= MIN_VELOCITY) { // 惯性滑动 final int maxDistance = (int) (MAX_TIME_VALUE * (dateArray.size()-1) / mUnitGap * mUnitGap); mScroller.fling((int) mCurrentDistance, 0, -xVelocity, 0, 0, maxDistance, 0, 0); invalidate(); } break; case MotionEvent.ACTION_POINTER_UP: // 两个中的有一个手指被抬起,允许滑动。同时把未抬起的手机当前位置赋给初始X isScaling = false; int restIndex = actionIndex == 0 ? 1 : 0; mInitialX = (int) event.getX(restIndex); break; default: break; } mLastX = x; mLastY = y; return true; } private void computeTime() { // 不用转float,肯定能整除 float maxDistance = MAX_TIME_VALUE * (dateArray.size()-1) / mUnitSecond * mUnitGap; // 限定范围 mCurrentDistance = Math.min(maxDistance, Math.max(0, mCurrentDistance)); currentTime = (int) (mCurrentDistance / mUnitGap * mUnitSecond); if (mListener != null) { mListener.onTimeChanged(currentTime); } //滑动时,更改到对应的日期 movingChangeDate(currentTime); invalidate(); } @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) @Override protected void onDraw(final Canvas canvas) { // 背景 canvas.drawColor(bgColor); //日期 drawDate(canvas); // 刻度 drawRule(canvas); // 时间段 drawTimeParts(canvas); // 当前时间指针 drawTimeIndicator(canvas); } /** * 绘制日期控件 * * @param canvas */ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) private void drawDate(Canvas canvas) { //日期控件一半的宽度 float dateHalfWidth = dateItemWidth / 2; //日期一半高度 //绘制日期控件布局 // mPaint.setColor(Color.parseColor("#595B5D")); for (int i = 0; i < dateArray.size(); i++) { if(i == dateClickIndex){ mPaint.setColor(dateBgColor); mPaint.setStyle(Paint.Style.FILL); canvas.drawRoundRect(dateItemToItemDistance + (dateItemWidth + dateItemToItemDistance) * i, dp2px(5), (dateItemToItemDistance + dateItemWidth) * (i + 1), dp2px(5) + dateItemHeight, dp2px(10), dp2px(10), mPaint); }else { // mPaint.setColor(bgColor); mPaint.setColor(Color.parseColor("#595B5D")); mPaint.setStyle(Paint.Style.STROKE); canvas.drawRoundRect(dateItemToItemDistance + (dateItemWidth + dateItemToItemDistance) * i, dp2px(5), (dateItemToItemDistance + dateItemWidth) * (i + 1), dp2px(5) + dateItemHeight, dp2px(10), dp2px(10), mPaint); } // canvas.drawRoundRect(dateItemToItemDistance + (dateItemWidth + dateItemToItemDistance) * i, dp2px(5), (dateItemToItemDistance + dateItemWidth) * (i + 1), dp2px(5) + dateItemHeight, dp2px(10), dp2px(10), mPaint); canvas.drawText(dateArray.get(i), dateItemToItemDistance + dateHalfWidth - mTextHalfWidth + (dateItemToItemDistance + dateItemWidth) * i, dp2px(5) + dateHalfWidth - dateTextSize / 2, mTextPaint); } //绘制日期下面的分割线 mPaint.setColor(Color.parseColor("#595B5D")); mPaint.setStrokeWidth(1); canvas.drawLine(0, dateHeight - dp2px(10), mWidth, dateHeight - dp2px(10), mPaint); } /** * 点击日期控件 */ private void eventDateClicked(MotionEvent event) { int x = (int) event.getX(); int y = (int) event.getY(); for (int i = 0; i < dateArray.size(); i++) { if (x >= (dateItemToItemDistance + (dateItemWidth + dateItemToItemDistance) * i) && x <= ((dateItemToItemDistance + dateItemWidth) * (i + 1)) && y >= dp2px(5) && y <= (dp2px(5) + dateItemHeight)) { Log.d("TimeRulePresure", "x =" + x + " y =" + y + " i =" + i); dateClickIndex = i; setCurrentTime(MAX_TIME_VALUE * i); postInvalidate(); } } } /** * 滑动时,更改到对应的日期 */ private void movingChangeDate(int distance){ for (int i = 0; i < dateArray.size()-1; i++) { if(distance >= MAX_TIME_VALUE * i && distance < MAX_TIME_VALUE * (i + 1)){ dateClickIndex = i; } } } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { mCurrentDistance = mScroller.getCurrX(); computeTime(); } } /** * 绘制刻度 */ private void drawRule(Canvas canvas) { // 移动画布坐标系 canvas.save(); // canvas.translate(0, partHeight); canvas.translate(0, dateHeight); mPaint.setColor(gradationColor); mPaint.setStrokeWidth(gradationWidth); // 刻度 // int start = 0; float offset = mHalfWidth - mCurrentDistance; final int perTextCount = mPerTextCounts[mPerTextCountIndex]; for (int i = 0; i < dateArray.size()-1; i++) { int start = 0; while (start <= MAX_TIME_VALUE - 1) { // 刻度 if (start % 3600 == 0) { // 时刻度 canvas.drawLine(offset, 0, offset, hourLen, mPaint); } else if (start % 60 == 0) { // 分刻度 canvas.drawLine(offset, 0, offset, minuteLen, mPaint); } else { // 秒刻度 canvas.drawLine(offset, 0, offset, secondLen, mPaint); } // 时间数值 if (start % perTextCount == 0) { String text = formatTimeHHmm(start); canvas.drawText(text, offset - mTextHalfWidth, hourLen + gradationTextGap + gradationTextSize, mTextPaint); } start += mUnitSecond; offset += mUnitGap; } } canvas.restore(); } /** * 绘制当前时间指针 */ private void drawTimeIndicator(Canvas canvas) { // 指针 mPaint.setColor(indicatorColor); mPaint.setStrokeWidth(indicatorWidth); canvas.drawLine(mHalfWidth, dateHeight - dp2px(10), mHalfWidth, mHeight, mPaint); // 正三角形 if (mTrianglePath.isEmpty()) { // final float halfSideLen = indicatorTriangleSideLen * .5f; mTrianglePath.moveTo(mHalfWidth - halfSideLen, dp2px(80)); mTrianglePath.rLineTo(indicatorTriangleSideLen, 0); mTrianglePath.rLineTo(-halfSideLen, -(float) (Math.sin(Math.toRadians(60)) * halfSideLen)); mTrianglePath.close(); } mPaint.setStrokeWidth(1); mPaint.setStyle(Paint.Style.FILL); canvas.drawPath(mTrianglePath, mPaint); mPaint.setStyle(Paint.Style.STROKE); } /** * 绘制时间段 */ private void drawTimeParts(Canvas canvas) { if (mTimePartList == null) { return; } // 不用矩形,直接使用直线绘制 mPaint.setStrokeWidth(partHeight); mPaint.setColor(partColor); float start, end; final float halfPartHeight = partHeight * .5f; final float secondGap = mUnitGap / mUnitSecond; for (int i = 0, size = mTimePartList.size(); i < size; i++) { TimePart timePart = mTimePartList.get(i); start = mHalfWidth - mCurrentDistance + timePart.startTime * secondGap; end = mHalfWidth - mCurrentDistance + timePart.endTime * secondGap; canvas.drawLine(start, halfPartHeight, end, halfPartHeight, mPaint); } } /** * 格式化时间 HH:mm * * @param timeValue 具体时间值 * @return 格式化后的字符串,eg:3600 to 01:00 */ public static String formatTimeHHmm(@IntRange(from = 0, to = MAX_TIME_VALUE) int timeValue) { if (timeValue < 0) { timeValue = 0; } int hour = timeValue / 3600; int minute = timeValue % 3600 / 60; StringBuilder sb = new StringBuilder(); if (hour < 10) { sb.append('0'); } sb.append(hour).append(':'); if (minute < 10) { sb.append('0'); } sb.append(minute); return sb.toString(); } /** * 格式化时间 HH:mm:ss * * @param timeValue 具体时间值 * @return 格式化后的字符串,eg:3600 to 01:00:00 */ public static String formatTimeHHmmss(@IntRange(from = 0, to = MAX_TIME_VALUE) int timeValue) { int hour = timeValue / 3600; int minute = timeValue % 3600 / 60; int second = timeValue % 3600 % 60; StringBuilder sb = new StringBuilder(); if (hour < 10) { sb.append('0'); } sb.append(hour).append(':'); if (minute < 10) { sb.append('0'); } sb.append(minute); sb.append(':'); if (second < 10) { sb.append('0'); } sb.append(second); return sb.toString(); } private int dp2px(float dp) { return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics()); } private int sp2px(float sp) { return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, getResources().getDisplayMetrics()); } @SuppressWarnings("all") private void logD(String format, Object... args) { if (LOG_ENABLE) { Log.d("MoneySelectRuleView", String.format("zjun@" + format, args)); } } /** * 设置时间变化监听事件 * * @param listener 监听回调 */ public void setOnTimeChangedListener(OnTimeChangedListener listener) { this.mListener = listener; } /** * 设置时间块(段)集合 * * @param timePartList 时间块集合 */ public void setTimePartList(List timePartList) { this.mTimePartList = timePartList; postInvalidate(); } /** * 设置当前时间 * * @param currentTime 当前时间 */ public void setCurrentTime(@IntRange(from = 0, to = MAX_TIME_VALUE*3) int currentTime) { this.currentTime = currentTime; calculateValues(); postInvalidate(); } /** * 设置日期列表 * @param list */ public void setDateArray(ArrayList list) { if (dateArray.isEmpty()) { dateArray = new ArrayList<>(); } dateArray.clear(); dateArray.addAll(list); dateArray.add("实时"); postInvalidate(); } }

4、在布局文件中引用

参考:

https://github.com/zjun615/RulerView

https://www.jianshu.com/p/5d1fa50298b3

https://www.jb51.net/article/140917.htm

https://blog.csdn.net/u014005316/article/details/54667576

你可能感兴趣的:(Android自定义控件)