@@ -0,0 +1,282 @@
package cc.winboll.studio.positions.views ;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/10 08:29
* @Describe 沙漏计时器控件
*/
import android.content.Context ;
import android.graphics.Color ;
import android.graphics.drawable.ClipDrawable ;
import android.graphics.drawable.ColorDrawable ;
import android.graphics.drawable.Drawable ;
import android.graphics.drawable.LayerDrawable ;
import android.text.InputFilter ;
import android.text.InputType ;
import android.util.AttributeSet ;
import android.view.Gravity ;
import android.view.ViewGroup ;
import android.widget.EditText ;
import android.widget.LinearLayout ;
import android.widget.ProgressBar ;
import android.widget.Switch ;
import android.widget.TextView ;
/**
* 沙漏视图类( Java 7语法, 修复ProgressDrawable和setHeight问题)
*/
public class HourglassView extends LinearLayout {
public static final String TAG = " HourglassView " ;
// 数据模型
private String hourglassId ;
private int hour ; // 小时
private int minute ; // 分钟
private boolean isEnabled ; // 开关状态
// 控件引用
private EditText etHour ;
private EditText etMinute ;
private ProgressBar progressBar ;
private Switch switchControl ;
// 样式参数
private int textSize = 16 ;
private int padding = 8 ;
private int progressColor = 0xFF2196F3 ; // 进度条颜色
private int progressBgColor = 0xFFE0E0E0 ; // 进度条背景色
private int textColor = 0xFF333333 ;
private int editTextWidth = 40 ; // 输入框宽度( dp)
private int progressHeight = 8 ; // 进度条高度( dp, 新增参数)
public HourglassView ( Context context ) {
super ( context ) ;
initView ( ) ;
}
public HourglassView ( Context context , AttributeSet attrs ) {
super ( context , attrs ) ;
initView ( ) ;
}
/**
* 初始化视图布局
*/
private void initView ( ) {
setOrientation ( HORIZONTAL ) ;
setGravity ( Gravity . CENTER_VERTICAL ) ;
setPadding ( dp2px ( padding ) , dp2px ( padding ) , dp2px ( padding ) , dp2px ( padding ) ) ;
// 1. 左侧时间输入区域(水平布局)
LinearLayout inputLayout = new LinearLayout ( getContext ( ) ) ;
inputLayout . setOrientation ( HORIZONTAL ) ;
inputLayout . setGravity ( Gravity . CENTER_VERTICAL ) ;
LayoutParams inputParams = new LayoutParams (
ViewGroup . LayoutParams . WRAP_CONTENT ,
ViewGroup . LayoutParams . WRAP_CONTENT
) ;
inputParams . setMargins ( 0 , 0 , dp2px ( padding * 2 ) , 0 ) ;
addView ( inputLayout , inputParams ) ;
// 小时输入框
etHour = createNumberEditText ( ) ;
etHour . setHint ( " 时 " ) ;
etHour . setFilters ( new InputFilter [ ] { new InputFilter . LengthFilter ( 2 ) } ) ;
inputLayout . addView ( etHour , getEditTextParams ( ) ) ;
// 分隔符
TextView divider = new TextView ( getContext ( ) ) ;
divider . setText ( " : " ) ;
divider . setTextSize ( textSize ) ;
divider . setTextColor ( textColor ) ;
LayoutParams dividerParams = new LayoutParams (
ViewGroup . LayoutParams . WRAP_CONTENT ,
ViewGroup . LayoutParams . WRAP_CONTENT
) ;
dividerParams . setMargins ( dp2px ( padding / 2 ) , 0 , dp2px ( padding / 2 ) , 0 ) ;
inputLayout . addView ( divider , dividerParams ) ;
// 分钟输入框
etMinute = createNumberEditText ( ) ;
etMinute . setHint ( " 分 " ) ;
etMinute . setFilters ( new InputFilter [ ] { new InputFilter . LengthFilter ( 2 ) } ) ;
inputLayout . addView ( etMinute , getEditTextParams ( ) ) ;
// 2. 中间进度条( 修复: 通过LayoutParams设置高度, 替代setHeight)
progressBar = new ProgressBar ( getContext ( ) , null , android . R . attr . progressBarStyleHorizontal ) ;
progressBar . setProgressDrawable ( createProgressDrawable ( ) ) ; // 传入Drawable类型
// 修复核心: 用LayoutParams设置进度条高度( 兼容低版本)
LayoutParams progressParams = new LayoutParams (
0 ,
dp2px ( progressHeight ) , // 直接在布局参数中设置高度( dp转px)
1 . 0f
) ;
progressParams . setMargins ( 0 , 0 , dp2px ( padding * 2 ) , 0 ) ;
addView ( progressBar , progressParams ) ;
// 3. 右侧开关
switchControl = new Switch ( getContext ( ) ) ;
switchControl . setOnCheckedChangeListener ( new Switch . OnCheckedChangeListener ( ) {
@Override
public void onCheckedChanged ( android . widget . CompoundButton buttonView , boolean isChecked ) {
isEnabled = isChecked ;
// 开关状态控制输入框是否可编辑
etHour . setEnabled ( ! isChecked ) ;
etMinute . setEnabled ( ! isChecked ) ;
// 更新进度条(仅在开关开启时生效)
if ( isChecked ) {
updateProgressBar ( ) ;
}
}
} ) ;
addView ( switchControl ) ;
// 初始状态
isEnabled = false ;
etHour . setEnabled ( true ) ;
etMinute . setEnabled ( true ) ;
}
/**
* 创建数字输入框
*/
private EditText createNumberEditText ( ) {
EditText editText = new EditText ( getContext ( ) ) ;
editText . setInputType ( InputType . TYPE_CLASS_NUMBER ) ;
editText . setTextSize ( textSize ) ;
editText . setTextColor ( textColor ) ;
editText . setGravity ( Gravity . CENTER ) ;
editText . setSingleLine ( true ) ;
editText . setBackgroundResource ( android . R . drawable . edit_text ) ; // 默认输入框背景
return editText ;
}
/**
* 获取输入框布局参数
*/
private LayoutParams getEditTextParams ( ) {
LayoutParams params = new LayoutParams (
dp2px ( editTextWidth ) ,
ViewGroup . LayoutParams . WRAP_CONTENT
) ;
params . setMargins ( 0 , 0 , dp2px ( padding ) , 0 ) ;
return params ;
}
/**
* 修复核心: 创建ProgressDrawable( 返回Drawable类型, 而非Paint)
* 用LayerDrawable实现「背景+进度」的双层进度条
*/
private Drawable createProgressDrawable ( ) {
// 1. 进度条背景(灰色)
ColorDrawable bgDrawable = new ColorDrawable ( progressBgColor ) ;
// 2. 进度条前景(主题色)
ColorDrawable progressDrawable = new ColorDrawable ( progressColor ) ;
// 3. 用ClipDrawable包裹前景, 实现进度裁剪
ClipDrawable clipDrawable = new ClipDrawable ( progressDrawable , Gravity . LEFT , ClipDrawable . HORIZONTAL ) ;
// 4. 组合成LayerDrawable( 顺序: 背景在下, 进度在上)
Drawable [ ] layers = new Drawable [ ] { bgDrawable , clipDrawable } ;
LayerDrawable layerDrawable = new LayerDrawable ( layers ) ;
// 5. 设置进度条的层级ID( 必须与系统ProgressBar的ID匹配)
layerDrawable . setId ( 0 , android . R . id . background ) ;
layerDrawable . setId ( 1 , android . R . id . progress ) ;
return layerDrawable ;
}
/**
* 更新进度条(总时间 = 小时*60 + 分钟,单位:分钟)
*/
private void updateProgressBar ( ) {
try {
// 获取输入的时间( 为空时默认0)
int inputHour = TextUtils . isEmpty ( etHour . getText ( ) . toString ( ) . trim ( ) )
? 0 : Integer . parseInt ( etHour . getText ( ) . toString ( ) . trim ( ) ) ;
int inputMinute = TextUtils . isEmpty ( etMinute . getText ( ) . toString ( ) . trim ( ) )
? 0 : Integer . parseInt ( etMinute . getText ( ) . toString ( ) . trim ( ) ) ;
// 校验时间合法性( 小时0-99, 分钟0-59)
inputHour = Math . max ( 0 , Math . min ( 99 , inputHour ) ) ;
inputMinute = Math . max ( 0 , Math . min ( 59 , inputMinute ) ) ;
// 计算总分钟数(进度条最大值)
int totalMinutes = inputHour * 60 + inputMinute ;
totalMinutes = Math . max ( 1 , totalMinutes ) ; // 最小1分钟, 避免进度条无长度
// 更新进度条
progressBar . setMax ( totalMinutes ) ;
progressBar . setProgress ( totalMinutes ) ; // 初始显示满进度,可根据实际需求修改
// 更新数据模型
this . hour = inputHour ;
this . minute = inputMinute ;
} catch ( NumberFormatException e ) {
// 输入非法时重置进度条
progressBar . setMax ( 0 ) ;
progressBar . setProgress ( 0 ) ;
}
}
/**
* dp转px( 适配不同设备)
*/
private int dp2px ( int dp ) {
return ( int ) ( dp * getContext ( ) . getResources ( ) . getDisplayMetrics ( ) . density + 0 . 5f ) ;
}
// ------------------- 数据模型 getter/setter -------------------
public String getHourglassId ( ) {
return hourglassId ;
}
public void setHourglassId ( String hourglassId ) {
this . hourglassId = hourglassId ;
}
public int getHour ( ) {
return hour ;
}
public void setHour ( int hour ) {
this . hour = Math . max ( 0 , Math . min ( 99 , hour ) ) ; // 限制范围
etHour . setText ( String . valueOf ( this . hour ) ) ;
}
public int getMinute ( ) {
return minute ;
}
public void setMinute ( int minute ) {
this . minute = Math . max ( 0 , Math . min ( 59 , minute ) ) ; // 限制范围
etMinute . setText ( String . valueOf ( this . minute ) ) ;
}
public boolean isEnabled ( ) {
return isEnabled ;
}
public void setEnabled ( boolean enabled ) {
isEnabled = enabled ;
switchControl . setChecked ( enabled ) ;
}
/**
* 手动更新进度条(外部调用)
*/
public void refreshProgress ( ) {
if ( isEnabled ) {
updateProgressBar ( ) ;
}
}
// 工具类: 判断字符串是否为空( Java7无TextUtils.isEmpty, 手动实现)
private static class TextUtils {
public static boolean isEmpty ( CharSequence str ) {
return str = = null | | str . length ( ) = = 0 ;
}
}
}