android 控件自定义

本文介绍了Android控件自定义的几个案例,包括重写EditText保持光标在最后,实现换行RadioGroup,自定义NumberPicker展示日期滚动,以及Android悬浮按钮的使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一.重写edittext 设置光标始终在最后

package com.lgd.factoryattendance.View;

import android.content.Context;
import android.util.AttributeSet;
import android.widget.EditText;

/**
 * @Author lgd
 * @Date 2024/3/8 14:26
 * 重写edittext 设置光标始终在最后
 */
public class LastInputEditText extends androidx.appcompat.widget.AppCompatEditText {

    public LastInputEditText(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    public LastInputEditText(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public LastInputEditText(Context context) {
        super(context);
    }

    @Override
    protected void onSelectionChanged(int selStart, int selEnd) {
        super.onSelectionChanged(selStart, selEnd);
        //保证光标始终在最后面
        if(selStart==selEnd){//防止不能多选
            setSelection(getText().length());
        }
    }
}

二.自定义 换行 radioGroup

package com.lgd.factoryattendance.View;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.RadioGroup;

/**
 * @Author lgd
 * @Date 2024/4/9 10:39
 *     自定义 换行 radioGroup
 */
public class LineFeedRadioGroup extends RadioGroup {
    private static final String TAG = "RadioGroupEx";

    public LineFeedRadioGroup(Context context) {
        super(context);
    }

    public LineFeedRadioGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        //调用ViewGroup的方法,测量子view
        measureChildren(widthMeasureSpec, heightMeasureSpec);

        //最大的宽
        int maxWidth = 0;
        //累计的高
        int totalHeight = 0;

        //当前这一行的累计行宽
        int lineWidth = 0;
        //当前这行的最大行高
        int maxLineHeight = 0;
        //用于记录换行前的行宽和行高
        int oldHeight;
        int oldWidth;

        int count = getChildCount();
        //假设 widthMode和heightMode都是AT_MOST
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();
            //得到这一行的最高
            oldHeight = maxLineHeight;
            //当前最大宽度
            oldWidth = maxWidth;

            int deltaX = child.getMeasuredWidth() + params.leftMargin + params.rightMargin;
            if (lineWidth + deltaX + getPaddingLeft() + getPaddingRight() > widthSize) {//如果折行,height增加
                //和目前最大的宽度比较,得到最宽。不能加上当前的child的宽,所以用的是oldWidth
                maxWidth = Math.max(lineWidth, oldWidth);
                //重置宽度
                lineWidth = deltaX;
                //累加高度
                totalHeight += oldHeight;
                //重置行高,当前这个View,属于下一行,因此当前最大行高为这个child的高度加上margin
                maxLineHeight = child.getMeasuredHeight() + params.topMargin + params.bottomMargin;
//                Log.v(TAG, "maxHeight:" + totalHeight + "---" + "maxWidth:" + maxWidth);

            } else {
                //不换行,累加宽度
                lineWidth += deltaX;
                //不换行,计算行最高
                int deltaY = child.getMeasuredHeight() + params.topMargin + params.bottomMargin;
                maxLineHeight = Math.max(maxLineHeight, deltaY);
            }
            if (i == count - 1) {
                //前面没有加上下一行的搞,如果是最后一行,还要再叠加上最后一行的最高的值
                totalHeight += maxLineHeight;
                //计算最后一行和前面的最宽的一行比较
                maxWidth = Math.max(lineWidth, oldWidth);
            }
        }

        //加上当前容器的padding值
        maxWidth += getPaddingLeft() + getPaddingRight();
        totalHeight += getPaddingTop() + getPaddingBottom();
        setMeasuredDimension(widthMode == MeasureSpec.EXACTLY ? widthSize : maxWidth,
                heightMode == MeasureSpec.EXACTLY ? heightSize : totalHeight);

    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int count = getChildCount();
        //pre为前面所有的child的相加后的位置
        int preLeft = getPaddingLeft();
        int preTop = getPaddingTop();
        //记录每一行的最高值
        int maxHeight = 0;
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();
            //r-l为当前容器的宽度。如果子view的累积宽度大于容器宽度,就换行。
            if (preLeft + params.leftMargin + child.getMeasuredWidth() + params.rightMargin + getPaddingRight() > (r - l)) {
                //重置
                preLeft = getPaddingLeft();
                //要选择child的height最大的作为设置
                preTop = preTop + maxHeight;
                maxHeight = getChildAt(i).getMeasuredHeight() + params.topMargin + params.bottomMargin;
            } else { //不换行,计算最大高度
                maxHeight = Math.max(maxHeight, child.getMeasuredHeight() + params.topMargin + params.bottomMargin);
            }
            //left坐标
            int left = preLeft + params.leftMargin;
            //top坐标
            int top = preTop + params.topMargin;
            int right = left + child.getMeasuredWidth();
            int bottom = top + child.getMeasuredHeight();
            //为子view布局
            child.layout(left, top, right, bottom);
            //计算布局结束后,preLeft的值
            preLeft += params.leftMargin + child.getMeasuredWidth() + params.rightMargin;
        }
    }

}

xml

<com.lgd.factoryattendance.View.LineFeedRadioGroup
    android:id="@+id/rg_electric_code"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal">
</com.lgd.factoryattendance.View.LineFeedRadioGroup>

动态添加 RadioButton

List<String> list = new ArrayList<>();
//   动态 添加 RadioButton
@SuppressLint({"ResourceType", "UseCompatLoadingForColorStateLists"})
public void addRadioButton(RadioGroup radioGroup){
    list.clear();
    list.add("B04011990126");
    list.add("B04011990057");
    list.add("B04011990087");
    for (int i = 1; i < list.size(); i++) {
        RadioButton rb_craft = new RadioButton(HomePageActivity.this);
        // 设置 RadioButton 的大小和边距
        ViewGroup.MarginLayoutParams marginLayoutParams = new ViewGroup.MarginLayoutParams(
                LinearLayout.LayoutParams.WRAP_CONTENT,
                LinearLayout.LayoutParams.WRAP_CONTENT
        );
        marginLayoutParams.height = dpToPx(30);
        marginLayoutParams.setMarginEnd(dpToPx(10));
        rb_craft.setLayoutParams(marginLayoutParams);
        rb_craft.setText( list.get(i));
        rb_craft.setPadding(dpToPx(10),0,dpToPx(10),0);
        rb_craft.setButtonDrawable(null);
        rb_craft.setGravity(Gravity.CENTER);
        rb_craft.setBackgroundResource(R.drawable.selector_speed_borrow_select_day);
        rb_craft.setTextColor(getResources().getColorStateList(R.drawable.selector_select_day));
        rb_craft.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 25);
        radioGroup.addView(rb_craft,marginLayoutParams);
    }
}

//    dp  转化为  像素
public int dpToPx(int dp) {
    float density = getResources().getDisplayMetrics().density;
    return Math.round((float) dp * density);
}

三.自定义 NumberPicker 实现 日期滚动

package com.lgd.factoryattendance.View;

import android.content.Context;
import android.graphics.drawable.ColorDrawable;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.NumberPicker;

import com.lgd.factoryattendance.R;

import java.lang.reflect.Field;

/**
 * @Author lgd
 * @Date 2024/3/28 8:57
 *自定义 NumberPicker 实现 日期滚动
 */
public class TextConfigNumberPicker extends NumberPicker {

    public TextConfigNumberPicker(Context context) {
        super(context);
    }

    public TextConfigNumberPicker(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public TextConfigNumberPicker(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public void addView(View child) {
        this.addView(child, null);
    }

    @Override
    public void addView(View child, ViewGroup.LayoutParams params) {
        this.addView(child, 0, params);
    }

    @Override
    public void addView(View child, int index, ViewGroup.LayoutParams params) {
        super.addView(child, index, params);
        updateView(child);
    }

    private void updateView(View view) {
        if (view instanceof EditText) {
            //设置通用(默认)文字的颜色和大小
            ((EditText) view).setTextColor(getResources().getColor(R.color.black));
            ((EditText) view).setTextSize(20);
        }
        try {
            //设置分割线大小颜色
            Field mSelectionDivider = this.getFile("mSelectionDivider");
            mSelectionDivider.set(this, new ColorDrawable(getResources().getColor(R.color.lightskyblue)));
            mSelectionDivider.set(this, 10);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    //反射获取控件 mSelectionDivider mInputText当前选择的view
    public Field getFile(String fieldName) {
        try {
            //设置分割线的颜色值
            Field pickerFields = NumberPicker.class.getDeclaredField(fieldName);
            pickerFields.setAccessible(true);
            return pickerFields;
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }

    //设置选中控件的style  注意一点要在setOnValueChangedListener或 setOnScrollChangeListener,setOnScrollListener中调
    // 用picker.performClick();两种情况动画效果不同
    //同时注意他的代用位置 要在当前控件初始化之后 否则获得不了
    public void setMInputStyle(Float size) {
        Field mInputText = this.getFile("mInputText");
        try {
            ((EditText) mInputText.get(this)).setTextSize(size);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

}
xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="20dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <com.lgd.factoryattendance.View.TextConfigNumberPicker
            android:id="@+id/yearPicker"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:descendantFocusability="blocksDescendants"
            />
        <com.lgd.factoryattendance.View.TextConfigNumberPicker
            android:id="@+id/monthPicker"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:descendantFocusability="blocksDescendants"/>
    </LinearLayout>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:orientation="horizontal"
        android:layout_marginTop="30dp"
        android:layout_marginBottom="20dp">
        <Button
            android:id="@+id/Cancel_BT"
            android:layout_width="0dp"
            android:layout_height="50dp"
            android:layout_weight="1"
            android:text="取消"
            android:background="@drawable/button"
            android:textColor="@color/white"
            android:textSize="20dp"
            android:layout_marginRight="30dp"
            android:layout_marginLeft="10dp"/>
        <Button
            android:id="@+id/Ok_BT"
            android:layout_width="0dp"
            android:layout_height="50dp"
            android:layout_weight="1"
            android:text="确定"
            android:background="@drawable/button"
            android:textColor="@color/white"
            android:textSize="20dp"
            android:layout_marginLeft="30dp"
            android:layout_marginRight="10dp"/>

    </LinearLayout>
</LinearLayout>
使用:
// 设置 年月 选择弹框控件
    public void setData(){
        // 加载自定义布局
        View view = LayoutInflater.from(AdminActivity.this).inflate(R.layout.dialog_date_picker, null);
        final TextConfigNumberPicker yearPicker = view.findViewById(R.id.yearPicker);
        final TextConfigNumberPicker monthPicker = view.findViewById(R.id.monthPicker);
        final Button Ok_BT = view.findViewById(R.id.Ok_BT);
        final Button Cancel_BT = view.findViewById(R.id.Cancel_BT);
        // 设置年份范围
        final int curYear = Calendar.getInstance().get(Calendar.YEAR);
        yearPicker.setMinValue(curYear);
        yearPicker.setMaxValue(curYear + 10);
        // 设置月份范围
        String[] months = new String[]{"01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"};
        monthPicker.setDisplayedValues(months);
        monthPicker.setMinValue(0);
        monthPicker.setMaxValue(11);

        yearPicker.setMInputStyle(16f);
        monthPicker.setMInputStyle(16f);
        // 设置滚动监听器
        yearPicker.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
            @Override
            public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
                picker.performClick();  //刷新选中状态
                int year = picker.getValue();
                int month = monthPicker.getValue() + 1; // 加1是因为数组下标从0开始
//                Log.d("TAG", "Selected year: " + year + ", month: " + month);
            }
        });
        monthPicker.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
            @Override
            public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
                picker.performClick();
                int year = yearPicker.getValue();
                int month = picker.getValue() + 1; // 加1是因为数组下标从0开始
//                Log.d("TAG", "Selected year: " + year + ", month: " + month);
            }
        });
        // 构建AlertDialog
        AlertDialog.Builder builder = new AlertDialog.Builder(AdminActivity.this);
        builder.setTitle("请选择年月");
        builder.setView(view);
        AlertDialog alertDialog = builder.create();
        Ok_BT.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                int year = yearPicker.getValue();
                int month = monthPicker.getValue() + 1; // 加1是因为数组下标从0开始
                StringBuilder sb = new StringBuilder();
                sb.append(year).append("-").append(month);
                String str = timeConvert(sb.toString());
                selectMonth_TV.setText(str);
                alertDialog.dismiss();
            }
        });
        Cancel_BT.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                alertDialog.dismiss();
            }
        });

        alertDialog.show();
    }
    // 时间格式转换
    public String timeConvert(String time){
        SimpleDateFormat inputFormat = new SimpleDateFormat("yyyy-M");
        SimpleDateFormat outputFormat = new SimpleDateFormat("yyyy-MM");
        String time_str = null;
        try {
            Date date = inputFormat.parse(time);
            String formattedDate = outputFormat.format(date);
            time_str = formattedDate;
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return time_str;
    }

四. android悬浮按钮

package com.example.esp8266.activity.view;


import android.animation.ObjectAnimator;
import android.content.Context;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.MotionEvent;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.BounceInterpolator;
import android.view.animation.DecelerateInterpolator;

import androidx.annotation.NonNull;
import com.google.android.material.floatingactionbutton.FloatingActionButton;

public class FloatBtn extends FloatingActionButton {

    private static final String TAG = "FloatActionButton";
    private int mLastX, mLastY;//按下时的X,Y坐标
    private int mDownX, mDownY;//按下时的X,Y坐标 用来计算移动的距离
    private int mScreenWidth, mScreenHeight;//ViewTree的宽和高
    // 重写了所有的构造函数,因为不知道会用哪种
    // 每个构造函数都进行了数据的初始化,即获取屏幕的高度和宽度

    private int dx, dy, left, top , right, bottom;

    public FloatBtn(Context context) {
        super(context);
        initData(context);
    }

    public FloatBtn(Context context, AttributeSet attrs) {
        super(context, attrs);
        initData(context);
    }

    public FloatBtn(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initData(context);
    }
    // 初始化数据:高度和宽度。看上去没啥问题,但是运行的时候浮动按钮的一部分会跑到虚拟导航栏的下面,也就是说获取的高度超过了显示的部分,但是宽度是正常的。
    private void initData(Context context) {
        DisplayMetrics metrics = context.getResources().getDisplayMetrics();
        mScreenHeight = (metrics.heightPixels)-150;  // 减去导航栏 高度
        mScreenWidth = metrics.widthPixels;
    }
    // 重写onTouchEvent方法,主要在这个方法里面来处理点击和拖动事件
    @Override
    public boolean onTouchEvent(@NonNull MotionEvent ev) {
        int actionId = ev.getAction();
        // Log.i(TAG, "onTouchEvent: " + actionId);
        switch (actionId) {
            // 无论是拖动还是点击 都需要先按下去,所以在这个case里记录按钮按下时的坐标
            case MotionEvent.ACTION_DOWN:
                // Log.i(TAG, "onTouchEvent: " + "DOWN");
                mLastX = (int) ev.getRawX();// getRawX: 获取的距离屏幕边缘的距离,而不是组件的
                mLastY = (int) ev.getRawY();
                mDownX = mLastX;// 判断是点击事件还是拖动事件时需要用到
                mDownY = mLastY;
                break;
            // 抬起事件:无论是拖动还是点击,完成之后都需要抬起按钮,在这个case里判断是拖动按钮还是点击按钮,主要是通过移动的距离来判断的,如果移动的距离超过5则认为是拖动,否则就是点击事件。如果是拖动事件则需要在此消费掉此事件,不会再执行onClick的方法。
            case MotionEvent.ACTION_UP:
                // Log.i(TAG, "onTouchEvent: " + "UP");
                if (calculateDistance(mDownX, mDownY, (int) ev.getRawX(), (int) ev.getRawY()) <= 5) {
                    Log.i(TAG, "onTouchEvent: 点击事件");
                } else {
                    Log.i(TAG, "onTouchEvent: 拖动事件");
//                    if (mLastX > mScreenWidth/2){
//                        //靠左吸附
//                        ObjectAnimator objectAnimator=ObjectAnimator.ofFloat(this,"x",getX(),0);
//                        objectAnimator.setInterpolator(new BounceInterpolator());  //插值器 最好阶段回弹效果
//                        objectAnimator.setDuration(800);
//                        objectAnimator.start();
//                    }
//                    else {
//                        // 靠右吸附
//                        animate().setInterpolator(new DecelerateInterpolator())  //减速
//                                .setDuration(500)
//                                .xBy(mScreenWidth-getWidth()-getX())
//                                .start();
//                    }
                    // 消费掉此事件
                    return true;
                }
                break;
            case MotionEvent.ACTION_MOVE:  // 移动事件:根据移动之后的位置进行重绘
                dx = (int) ev.getRawX() - mLastX;//x方向的偏移量
                dy = (int) ev.getRawY() - mLastY;//y方向的偏移量
                // getLeft(): Left position of this view relative to its parent.
                // 计算组件此时的位置,距离父容器上下左右的距离=偏移量 + 原来的距离
                left = getLeft() + dx;
                top = getTop() + dy;
                right = getRight() + dx;
                bottom = getBottom() + dy;
//                if (dy < 0) {
//                    Log.i(TAG, "onTouchEvent: 向上拖动");
//                } else {
//                    Log.i(TAG, "onTouchEvent: 向下拖动");
//                }
//                if (dx < 0) {
//                    Log.i(TAG, "onTouchEvent: 向左拖动");
//                } else {
//                    Log.i(TAG, "onTouchEvent: 向右拖动");
//                }
                mLastX = (int) ev.getRawX();
                mLastY = (int) ev.getRawY();
                if (top < 0) {
                    // 移出了上边界
                    top = 0;
                    bottom = getHeight();
                }
                if (left < 0) {
                    // 移出了左边界
                    left = 0;
                    right = getWidth();
                }
                if (bottom > mScreenHeight) {
                    // 移出了下边界
                    bottom = mScreenHeight;
                    top = bottom - getHeight();
                }
                if (right > mScreenWidth) {
                    // 移出了右边界
                    right = mScreenWidth;
                    left = right - getWidth();
                }
                layout(left, top, right, bottom);
                break;
            default:
                break;
        }
        return super.onTouchEvent(ev);
    }
    // get the distance between (downX, downY) and (lastX, lastY)
    private int calculateDistance(int downX, int downY, int lastX, int lastY) {
        return (int) Math.sqrt(Math.pow(1.0f * (lastX - downX), 2.0) + Math.pow((lastY - downY) * 1.0f, 2.0));
    }



}

使用:

<FrameLayout
    android:id="@+id/frameConfig"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_weight="1"
    android:layout_marginTop="-5dp"
    tools:ignore="InefficientWeight">
    <!--悬浮按钮-->
    <com.example.esp8266.activity.view.FloatBtn
        android:id="@+id/connect"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:clickable="true"
        android:focusable="true"
        android:layout_gravity="bottom|end"
        android:layout_marginBottom="25dp"
        android:layout_marginEnd="20dp"
        tools:ignore="InvalidId"
        android:src="@drawable/link_ico"
        android:scaleType="center"
        app:fabSize="normal"
        app:borderWidth="0dp"
        android:tooltipText="@string/linkBoard"
        tools:targetApi="O" />

</FrameLayout>

五. 自定义仿iOS 音量,亮度调节

原文链接:Android安卓仿IOS音量调节-自定义view系列(4)_android 音量滑动条 控件-优快云博客

Android仿IOS屏幕亮度调节-自定义view系列(5)_android 调节亮度自定义view-优快云博客

自定义音量 View

package com.lgd.factoryattendance.View;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.media.AudioManager;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

import androidx.annotation.Nullable;

import com.lgd.factoryattendance.R;

/**
 * @Author lgd
 * @Date 2024/8/29 13:35
 */
public class IosColumnAudioView extends View {

    private Context mContext;
    //日志TAG
    private static final String TAG = IosColumnAudioView.class.getName();
    //系统声音广播名
    private static final String VOLUME_CHANGED_ACTION = "android.media.VOLUME_CHANGED_ACTION";
    private static final String EXTRA_VOLUME_STREAM_TYPE = "android.media.EXTRA_VOLUME_STREAM_TYPE";
    //系统声音广播接收器
    private MyVolumeReceiver mVolumeReceiver = null;
    //标记-是否是当前自己在调整音量大小
    private boolean isMeAdjustVolume = true;
    //当前UI高度与view高度的比例
    private double mCurrentDrawLoudRate = 0;
    //当前真实音量与总音量大小比例
    private double mCurrentRealLoudRate = 0;
    //系统最大声音index
    private int mMaxLoud=0;
    //记录按压时手指相对于组件view的高度
    private float mDownY;
    //手指移动的距离,视为音量调整
    private float mMoveDistance;
    //系统audio管理
    private AudioManager mAudioManager;
    //当前音量文字数字
    private String mTextLoud ="";
    //画笔
    private Paint mPaint;
    //位置
    private RectF mRectF;
    //当前Canvas LayerId
    private int layerId = 0;
    //音量图标圆弧位置
    private RectF mRectVolumeArc =new RectF();
    //音量图标静音位置
    private Rect mRectVolumeDrawable=new Rect();
    //音量图标margin
    private int mRectVolumeDrawableMargin=10;
    //音量图标粗细
    private final static int mRectVolumeDrawableWidth=4;
    //音量图标开始角度
    private final static int mRectVolumeDrawableStartAngle=315;
    //音量图标终止角度
    private final static int mRectVolumeDrawableEndAngle=90;
    /**
     * 设置声音流类型-默认音乐-iosColumnAudioView_setAudioStreamType
     */
    private int mAudioManagerStreamType = AudioManager.STREAM_MUSIC;
    /**
     * 设置圆弧度数-xml-iosColumnAudioView_setRadiusXY
     */
    private float mRXY=40;
    /**
     * 设置当前音量颜色-xml-iosColumnAudioView_setColorLoud
     */
    private int mColorLoud = Color.parseColor("#ECECEC");
    /**
     * 设置组件背景颜色-xml-iosColumnAudioView_setColorBackground
     */
    private int mColorBackground = Color.parseColor("#898989");

    /**
     * 设置是否画音量文字-iosColumnAudioView_setIsDrawTextVolume
     * @param context
     */
    private boolean mIsDrawTextVolume = true;
    /**
     * 设置文字大小-xml-iosColumnAudioView_setTextSize
     */
    private float mTextSize = 15;
    /**
     * 设置文字颜色-xml-iosColumnAudioView_setTextColor
     */
    private int mTextColor = Color.BLACK;
    /**
     * 设置文字高度-xml-iosColumnAudioView_setTextHeight
     * @param context
     */
    private int mTextHeight = -1;
    /**
     * 设置是否画音量图标-iosColumnAudioView_setIsDrawDrawableVolume
     * @param context
     */
    private boolean mIsDrawDrawableVolume = true;
    /**
     * 设置音量圆弧颜色-xml-iosColumnAudioView_setColorVolume
     */
    private int mColorVolume = Color.DKGRAY;
    /**
     * 设置音量静音图标drawable-xml-iosColumnAudioView_setColorVolumeDrawable
     */
    private Drawable mColorDrawable = null;
    //固定组件高度长度,这里不做适配,可自行修改
    private int mViewHeight = 150, mViewWeight = 50;

    public IosColumnAudioView(Context context) {
        super(context);
        initial(context);
    }

    public IosColumnAudioView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mTextSize = sp2px(context,mTextSize);
        TypedArray typedArray=context.obtainStyledAttributes(attrs, R.styleable.IosColumnAudioView);
        mColorBackground = typedArray.getColor(R.styleable.IosColumnAudioView_iosColumnAudioView_setColorBackground,mColorBackground);
        mColorLoud = typedArray.getColor(R.styleable.IosColumnAudioView_iosColumnAudioView_setColorLoud,mColorLoud);
//        mAudioManagerStreamType =typedArray.getInteger(R.styleable.IosColumnAudioView_iosColumnAudioView_setAudioStreamType, mAudioManagerStreamType);
        mRXY = typedArray.getDimension(R.styleable.IosColumnAudioView_iosColumnAudioView_setRadiusXY, mRXY);
        mTextSize = typedArray.getDimension(R.styleable.IosColumnAudioView_iosColumnAudioView_setTextSize,mTextSize);
        mTextColor = typedArray.getColor(R.styleable.IosColumnAudioView_iosColumnAudioView_setTextColor,mTextColor);
        mTextHeight = typedArray.getInt(R.styleable.IosColumnAudioView_iosColumnAudioView_setTextHeight,mTextHeight);
        mIsDrawTextVolume = typedArray.getBoolean(R.styleable.IosColumnAudioView_iosColumnAudioView_setIsDrawTextVolume,mIsDrawTextVolume);
        mIsDrawDrawableVolume = typedArray.getBoolean(R.styleable.IosColumnAudioView_iosColumnAudioView_setIsDrawDrawableVolume,mIsDrawDrawableVolume);
        mColorVolume = typedArray.getColor(R.styleable.IosColumnAudioView_iosColumnAudioView_setVolumeColor, mColorVolume);
        mColorDrawable = typedArray.getDrawable(R.styleable.IosColumnAudioView_iosColumnAudioView_setVolumeDrawable);
        typedArray.recycle();
        initial(context);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        mRectVolumeDrawableMargin = MeasureSpec.getSize(widthMeasureSpec)/10;
        //固定组件高度长度,这里不做适配,可自行修改
        setMeasuredDimension(dp2px(mContext,mViewWeight),dp2px(mContext,mViewHeight));
    }

    private void initial(Context context){
        mContext=context;
        mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
        mMaxLoud = mAudioManager.getStreamMaxVolume(mAudioManagerStreamType);
        mCurrentDrawLoudRate = getCalculateLoudRate();
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mRectF = new RectF();
        setWillNotDraw(false);
        setBackgroundColor(Color.TRANSPARENT);
        mPaint.setTextSize(mTextSize);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                mDownY=event.getY();
                isMeAdjustVolume=true;
                break;
            case MotionEvent.ACTION_MOVE:
                mMoveDistance = mDownY - event.getY();
                calculateLoudRate();
                mDownY=event.getY();
                break;
            case MotionEvent.ACTION_UP:
                isMeAdjustVolume=false;
                break;
            default:
                break;
        }
        refreshAll();
        return true;
    }

    /**
     * 更新所有内容-ui-音乐大小
     */
    private void refreshAll(){
        refreshStreamVolume(((int)(mCurrentDrawLoudRate * mMaxLoud))<= mMaxLoud? (int)(mCurrentDrawLoudRate * mMaxLoud) : mMaxLoud);
        refreshUI();
    }
    /**
     * 设置音量大小
     * @param currentVolume
     */
    public void refreshStreamVolume(int currentVolume){
        mAudioManager.setStreamVolume(mAudioManagerStreamType,currentVolume, 0);
    }
    /**
     * 刷新UI
     */
    public void refreshUI(){
        invalidate();
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        layerId = canvas.saveLayer(0, 0, canvas.getWidth(), canvas.getHeight(), null, Canvas.ALL_SAVE_FLAG);
        onDrawBackground(canvas); //画背景
        onDrawLoud(canvas); //画当前音量前景表示当前多大音量
        onDrawText(canvas); //画文字
        onDrawVolumeDrawable(canvas);//画底部音量图标
        canvas.restoreToCount(layerId);
    }
    /**
     * 计算手指移动后音量UI占比大小,视其为音量大小
     */
    private void calculateLoudRate(){
        mCurrentDrawLoudRate = ( getHeight() * mCurrentDrawLoudRate + mMoveDistance) /  getHeight();
        if(mCurrentDrawLoudRate >=1){
            mCurrentDrawLoudRate =1;
        }
        if(mCurrentDrawLoudRate <=0){
            mCurrentDrawLoudRate =0;
        }
    }
    /**
     * 画圆弧背景
     * @param canvas
     */
    private void onDrawBackground(Canvas canvas){
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(mColorBackground );
        mRectF.left=0;
        mRectF.top=0;
        mRectF.right=canvas.getWidth();
        mRectF.bottom=canvas.getHeight();
        canvas.drawRoundRect(mRectF,mRXY,mRXY,mPaint);
    }
    /**
     * 画音量背景-方形-随手势上下滑动而变化用来显示音量大小
     * @param canvas
     */
    private void onDrawLoud(Canvas canvas){
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP));
        mPaint.setColor(mColorLoud);
        mRectF.left=0;
        mRectF.top=(canvas.getHeight()-(int)(canvas.getHeight() * mCurrentDrawLoudRate));
        mRectF.right=canvas.getWidth();
        mRectF.bottom=canvas.getHeight();
        canvas.drawRect(mRectF,mPaint);
        mPaint.setXfermode(null);
    }
    /**
     * 画文字-展示当前语音大小
     * @param canvas
     */
    private void onDrawText(Canvas canvas){
        if(mIsDrawTextVolume) { //如果开启了则开始绘制
            mPaint.setStyle(Paint.Style.FILL);
            mTextLoud = "" + (int) (mCurrentDrawLoudRate * 100);
            mPaint.setColor(mTextColor);
            canvas.drawText(mTextLoud, (canvas.getWidth() / 2 - mPaint.measureText(mTextLoud) / 2), mTextHeight >= 0 ? mTextHeight : getHeight() / 6, mPaint);
        }
    }
    /**
     * 画音量图标
     */
    private void onDrawVolumeDrawable(Canvas canvas){
        if(mIsDrawDrawableVolume){ //如果开启了则开始绘制
            mPaint.setStyle(Paint.Style.STROKE);
            mPaint.setStrokeWidth(mRectVolumeDrawableWidth);
            mPaint.setColor(mColorVolume);
            if (getCalculateLoudRate()>0){ //如果当前实际系统音量>0,则绘圆弧,否则绘制静音图片
                onDrawVolumeDrawableArc(canvas); //画音量圆弧
            }else if (getCalculateLoudRate() == 0 && mColorDrawable != null){
                onDrawVolumeMutedDrawable(canvas);
            }
        }
    }

    /**
     * 画音量图片-静音drawable
     */
    private void onDrawVolumeMutedDrawable(Canvas canvas){

        mRectVolumeDrawable.left=mRectVolumeDrawableMargin * 3;
        mRectVolumeDrawable.right=canvas.getWidth()-mRectVolumeDrawableMargin * 3;
        mRectVolumeDrawable.bottom=(int)(canvas.getHeight()*0.9)-mRectVolumeDrawableMargin * 3;
        mRectVolumeDrawable.top= (int)(mRectVolumeDrawable.bottom-canvas.getWidth()+mRectVolumeDrawableMargin *2 *3);
        mColorDrawable.setBounds(mRectVolumeDrawable);
        mColorDrawable.draw(canvas);
    }
    /**
     * 画音量图标-圆弧-计算多少个,这里是展示4个
     */
    private void onDrawVolumeDrawableArc(Canvas canvas){
        for(int i = 0; i<=(int)(mCurrentDrawLoudRate /0.33); i++){
            onDrawVolumeDrawableArc(canvas,(int)(mCurrentDrawLoudRate /0.33)-i); //画圆弧
        }
    }

    /**
     * 画音量图标-圆弧
     */
    private void onDrawVolumeDrawableArc(Canvas canvas, int index){
        index=4-index;
        mRectVolumeArc.left=mRectVolumeDrawableMargin * index;
        mRectVolumeArc.right=canvas.getWidth()-mRectVolumeDrawableMargin * index;
        mRectVolumeArc.bottom=(int)(canvas.getHeight()*0.9)-mRectVolumeDrawableMargin * index;
        mRectVolumeArc.top= mRectVolumeArc.bottom-canvas.getWidth()+mRectVolumeDrawableMargin *2 *index;
        //画板偏移
        canvas.translate(-mRectVolumeDrawableMargin,0);
        //开始角度向 360度收缩,结束角度向0度收缩,这样就可以造成弧度随着音量扩大而扩大,缩小而缩小的感觉
        canvas.drawArc(
                mRectVolumeArc,
                (float) (mRectVolumeDrawableStartAngle + ( (360 - mRectVolumeDrawableStartAngle) * (1- mCurrentDrawLoudRate))),
                (float) (mRectVolumeDrawableEndAngle - ( (mRectVolumeDrawableEndAngle-0) * (1- mCurrentDrawLoudRate))),
                false,
                mPaint);

    }
    /**
     * 将sp值转换为px值,保证文字大小不变
     */
    public int sp2px(Context context, float spValue) {
        final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
        return (int) (spValue * fontScale + 0.5f);
    }
    /**
     * 监听view视图在window里时,监听系统声音并注册广播
     */
    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        myRegisterReceiver(mContext);
    }
    /**
     * 监听view视图从window里抽离的时,取消广播的注册
     */
    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        unRegisterReceiver();//取消注册系统声音监听
    }
    /**
     * 注册音量广播-监听音量改变事件
     */
    private void myRegisterReceiver(Context context) {
        if(null != context) {
            mVolumeReceiver = new MyVolumeReceiver();
            IntentFilter intentFilter = new IntentFilter();
            intentFilter.addAction(VOLUME_CHANGED_ACTION);
            context.registerReceiver(mVolumeReceiver, intentFilter);
        }
    }
    /**
     * 取消广播
     */
    private void unRegisterReceiver(){
        if(mVolumeReceiver!=null) {
            mContext.unregisterReceiver(mVolumeReceiver);
        }
    }
    /**
     * 继承广播接受者
     */
    private class MyVolumeReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent.getAction().equals(VOLUME_CHANGED_ACTION)){
                if(intent.getIntExtra(EXTRA_VOLUME_STREAM_TYPE, -1) == mAudioManagerStreamType){
                    if(!isMeAdjustVolume) {
                        mCurrentDrawLoudRate = getCalculateLoudRate();
                        refreshUI();
                    }
                }
            }
        }
    }
    /**
     * 计算音量比例
     */
    private double getCalculateLoudRate(){
        return (double) mAudioManager.getStreamVolume(mAudioManagerStreamType)/ mMaxLoud;
    }
    /**
     * 根据手机的分辨率从 dp 的单位 转成为 px(像素)
     */
    public static int dp2px(Context context, float dpValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }

}

自定义 亮度 View

package com.lgd.factoryattendance.View;

import android.content.Context;
import android.content.Intent;
import android.content.res.TypedArray;
import android.database.ContentObserver;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.RectF;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

import androidx.annotation.Nullable;

import com.lgd.factoryattendance.R;

/**
 * @Author lgd
 * @Date 2024/8/29 14:10
 */
public class IosColumnBrightnessView extends View {
    private Context mContext;
    //日志TAG
    private static final String TAG = IosColumnBrightnessView.class.getName();
    //系统亮度监听
    private BrightnessObserver mBrightnessObserver = null;
    //当前圆心最大半径
    private float mCircleMaxRadius = 0;
    //当前圆心最小半径
    private float mCircleMinRadius = 0;
    //当前圆心边长
    private float mCircleMaxWidth = 0;
    //当前UI高度与view高度的比例
    private double mCurrentDrawBrightnessRate = 0;
    //系统最大亮度index-默认255
    private final int mMaxBrightness = 255;
    //记录按压时手指相对于组件view的高度
    private float mDownY;
    //手指移动的距离,视为亮度调整
    private float mMoveDistance;
    //系统audio管理
    private AudioManager mAudioManager;
    //当前亮度文字数字
    private String mTextLoud ="";
    //画笔
    private Paint mPaint;
    //位置
    private RectF mRectF;
    //当前Canvas LayerId
    private int layerId = 0;
    //亮度图标margin
    private int mRectBrightnessDrawableMargin =10;
    //亮度图标粗细
    private final static int mRectBrightnessDrawableWidth =4;

    /**
     * 设置圆弧度数-xml-iosColumnAudioView_setRadiusXY
     */
    private float mRXY=40;
    /**
     * 设置当前亮度颜色-xml-iosColumnAudioView_setColorLoud
     */
    private int mColorLoud = Color.parseColor("#ECECEC");
    /**
     * 设置组件背景颜色-xml-iosColumnAudioView_setColorBackground
     */
    private int mColorBackground = Color.parseColor("#898989");

    /**
     * 设置是否画亮度文字-iosColumnAudioView_setIsDrawTextVolume
     * @param context
     */
    private boolean mIsDrawTextVolume = true;
    /**
     * 设置文字大小-xml-iosColumnAudioView_setTextSize
     */
    private float mTextSize = 15;
    /**
     * 设置文字颜色-xml-iosColumnAudioView_setTextColor
     */
    private int mTextColor = Color.BLACK;
    /**
     * 设置文字高度-xml-iosColumnAudioView_setTextHeight
     * @param context
     */
    private int mTextHeight = -1;
    /**
     * 设置是否画亮度图标-iosColumnAudioView_setIsDrawDrawableVolume
     * @param context
     */
    private boolean mIsDrawDrawableVolume = true;
    /**
     * 设置亮度圆弧颜色-xml-iosColumnAudioView_setColorVolume
     */
    private int mColorVolume = Color.DKGRAY;
    //固定组件高度长度,这里不做适配,可自行修改
    private int mViewHeight = 150, mViewWeight=50;

    public IosColumnBrightnessView(Context context) {
        super(context);
        initial(context);
    }

    public IosColumnBrightnessView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mTextSize = sp2px(context,mTextSize);
        TypedArray typedArray=context.obtainStyledAttributes(attrs, R.styleable.IosColumnBrightnessView);
        mColorBackground = typedArray.getColor(R.styleable.IosColumnBrightnessView_iosColumnBrightnessView_setColorBackground,mColorBackground);
        mColorLoud = typedArray.getColor(R.styleable.IosColumnBrightnessView_iosColumnBrightnessView_setColorBright,mColorLoud);
        mRXY = typedArray.getDimension(R.styleable.IosColumnBrightnessView_iosColumnBrightnessView_setRadiusXY, mRXY);
        mTextSize = typedArray.getDimension(R.styleable.IosColumnBrightnessView_iosColumnBrightnessView_setTextSize,mTextSize);
        mTextColor = typedArray.getColor(R.styleable.IosColumnBrightnessView_iosColumnBrightnessView_setTextColor,mTextColor);
        mTextHeight = typedArray.getInt(R.styleable.IosColumnBrightnessView_iosColumnBrightnessView_setTextHeight,mTextHeight);
        mIsDrawTextVolume = typedArray.getBoolean(R.styleable.IosColumnBrightnessView_iosColumnBrightnessView_setIsDrawTextBright,mIsDrawTextVolume);
        mIsDrawDrawableVolume = typedArray.getBoolean(R.styleable.IosColumnBrightnessView_iosColumnBrightnessView_setIsDrawDrawableBright,mIsDrawDrawableVolume);
        mColorVolume = typedArray.getColor(R.styleable.IosColumnBrightnessView_iosColumnBrightnessView_setBrightnessColor, mColorVolume);
        typedArray.recycle();

        initial(context);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        mRectBrightnessDrawableMargin = MeasureSpec.getSize(widthMeasureSpec)/10;
        //固定组件高度长度,这里不做适配,可自行修改
        setMeasuredDimension(dp2px(mContext,mViewWeight),dp2px(mContext,mViewHeight));
    }

    private void initial(Context context){
        mContext=context;
        mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
        mCurrentDrawBrightnessRate = getCalculateBrightnessRate();
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mRectF = new RectF();
        setWillNotDraw(false);
        setBackgroundColor(Color.TRANSPARENT);
        mPaint.setTextSize(mTextSize);
        getPermission();
//        stopAutoBrightness(mContext);

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                mDownY=event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                mMoveDistance = mDownY - event.getY();
                calculateLoudRate();
                mDownY=event.getY();
                break;
            case MotionEvent.ACTION_UP:
                break;
            default:
                break;
        }
        refreshAll();
        return true;
    }

    /**
     * 更新所有内容-ui-系统亮度
     */
    private void refreshAll(){
        setSystemBrightness((int)(mCurrentDrawBrightnessRate * mMaxBrightness));
        refreshUI();
    }
    /**
     * 刷新UI
     */
    public void refreshUI(){
        invalidate();
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        layerId = canvas.saveLayer(0, 0, canvas.getWidth(), canvas.getHeight(), null, Canvas.ALL_SAVE_FLAG);
        onDrawBackground(canvas); //画背景
        onDrawFront(canvas); //画当前亮度前景表示当前多大亮度
        onDrawText(canvas); //画文字
        onDrawSunCircle(canvas);//画底部图标圆心
        canvas.restoreToCount(layerId);
    }
    /**
     * 计算手指移动后亮度UI占比大小,视其为亮度大小
     */
    private void calculateLoudRate(){
        mCurrentDrawBrightnessRate = ( getHeight() * mCurrentDrawBrightnessRate + mMoveDistance) /  getHeight();
        if(mCurrentDrawBrightnessRate >=1){
            mCurrentDrawBrightnessRate =1;
        }
        if(mCurrentDrawBrightnessRate <=0){
            mCurrentDrawBrightnessRate =0;
        }
    }
    /**
     * 画圆弧背景
     * @param canvas
     */
    private void onDrawBackground(Canvas canvas){
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(mColorBackground );
        mRectF.left=0;
        mRectF.top=0;
        mRectF.right=canvas.getWidth();
        mRectF.bottom=canvas.getHeight();
        canvas.drawRoundRect(mRectF,mRXY,mRXY,mPaint);
    }
    /**
     * 画亮度背景-方形-随手势上下滑动而变化用来显示亮度大小
     * @param canvas
     */
    private void onDrawFront(Canvas canvas){
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP));
        mPaint.setColor(mColorLoud);
        mRectF.left=0;
        mRectF.top=(canvas.getHeight()-(int)(canvas.getHeight() * mCurrentDrawBrightnessRate));
        mRectF.right=canvas.getWidth();
        mRectF.bottom=canvas.getHeight();
        canvas.drawRect(mRectF,mPaint);
        mPaint.setXfermode(null);
    }
    /**
     * 画文字-展示当前语音大小
     * @param canvas
     */
    private void onDrawText(Canvas canvas){
        if(mIsDrawTextVolume) { //如果开启了则开始绘制
            mPaint.setStyle(Paint.Style.FILL);
            mTextLoud = "" + (int) (mCurrentDrawBrightnessRate * 100);
            mPaint.setColor(mTextColor);
            canvas.drawText(mTextLoud, (canvas.getWidth() / 2 - mPaint.measureText(mTextLoud) / 2), mTextHeight >= 0 ? mTextHeight : getHeight() / 6, mPaint);
        }
    }
    /**
     * 画亮度图标-太阳圆心
     */
    private void onDrawSunCircle(Canvas canvas){
        if(mIsDrawDrawableVolume){ //如果开启了则开始绘制
            mPaint.setStyle(Paint.Style.FILL);
            mPaint.setStrokeWidth(mRectBrightnessDrawableWidth);
            mPaint.setColor(mColorVolume);
            mCircleMaxRadius = (float) (Math.sqrt(canvas.getWidth()) * 1.5);
            mCircleMinRadius = (float) (Math.sqrt(canvas.getWidth()) * 1);
            mCircleMaxWidth = (float) mCurrentDrawBrightnessRate * (mCircleMaxRadius-mCircleMinRadius)+mCircleMinRadius;
            canvas.drawCircle(canvas.getWidth()/2,(float) (canvas.getHeight()*0.8- mRectBrightnessDrawableMargin), mCircleMaxWidth,mPaint);
            onDrawSunRays(canvas,canvas.getWidth()/2,(float) (canvas.getHeight()*0.8- mRectBrightnessDrawableMargin));
        }
    }

    /**
     * 画亮度图标-太阳光芒
     */
    private void onDrawSunRays(Canvas canvas,float cx,float cy){
        mPaint.setStrokeCap(Paint.Cap.ROUND); // 定义线段断电形状为圆头
        //绘制时刻度
        canvas.translate(cx,cy);
        for (int i = 0; i < 10; i++) {
            canvas.drawLine(mCircleMaxWidth, mCircleMaxWidth, (float)(mCircleMaxWidth+5* mCurrentDrawBrightnessRate),(float)( mCircleMaxWidth+5* mCurrentDrawBrightnessRate), mPaint);
            canvas.rotate(36);
        }

    }

    /**
     * 监听view视图在window里时,监听系统亮度
     */
    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        if(mContext != null) {
            mBrightnessObserver = new BrightnessObserver(mContext, new Handler(Looper.getMainLooper()));
        }

    }
    /**
     * 监听view视图从window里抽离的时,取消广播的注册
     */
    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if(null != mBrightnessObserver) {
            mBrightnessObserver.unregister();
        }
    }

    /**
     * 监听亮度改变
     */
    private class BrightnessObserver extends ContentObserver {
        private Context mContext;
        public BrightnessObserver(Context context, Handler handler) {
            super(handler);
            this.mContext =context;
            register();
        }
        //注册监听
        public void register(){
            Uri brightnessUri = Settings.System.getUriFor(Settings.System.SCREEN_BRIGHTNESS);
            mContext.getContentResolver().registerContentObserver(brightnessUri, true,this);
        }
        //取消注册监听
        public void unregister(){
            mContext.getContentResolver().unregisterContentObserver(this);
        }
        @Override
        public void onChange(boolean selfChange) {
            //selfChange 一直是false,无法区分是自己手动改变还是通过系统设置调节,有bug。
            super.onChange(selfChange);
        }
    }

    /**
     * 改变当前系统亮度
     * @return
     */
    public void setSystemBrightness(int brightness) {
        if(null != mContext) {
            if(brightness>=mMaxBrightness){
                brightness=mMaxBrightness;
            }
            if(brightness<=0){
                brightness=0;
            }
            Settings.System.putInt(mContext.getContentResolver(), Settings.System.SCREEN_BRIGHTNESS,brightness);
        }
    }
    /**
     * 获取当前系统亮度
     * @return
     */
    public int getSystemBrightness(){
        if(null != mContext) {
            try {
                return Settings.System.getInt(mContext.getContentResolver(), Settings.System.SCREEN_BRIGHTNESS);
            } catch (Settings.SettingNotFoundException e) {
                e.printStackTrace();
            }

        }
        return 0;
    }
    /**
     * 计算亮度比例
     */
    private double getCalculateBrightnessRate(){
        return (double) getSystemBrightness()/ mMaxBrightness;
    }
    /**
     * 根据手机的分辨率从 dp 的单位 转成为 px(像素)
     */
    public static int dp2px(Context context, float dpValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }
    /**
     * 将sp值转换为px值,保证文字大小不变
     */
    public int sp2px(Context context, float spValue) {
        final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
        return (int) (spValue * fontScale + 0.5f);
    }
    /**
     *
     */
    private void getPermission(){
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if (!Settings.System.canWrite(mContext)) {
                Intent intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS);
                intent.setData(Uri.parse("package:" + mContext.getPackageName()));
                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                mContext.startActivity(intent);
            } else {
                // 申请权限后做的操作
            }
        }
    }
    /**
     * 停止自动亮度调节
     */
    private void stopAutoBrightness(Context context) {
        Settings.System.putInt(context.getContentResolver(),
                Settings.System.SCREEN_BRIGHTNESS_MODE,
                Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL);
    }


}

权限:<uses-permission android:name="android.permission.WRITE_SETTINGS" tools:ignore="ProtectedPermissions" />

attrs.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable
        name="IosColumnAudioView">
        <attr name="iosColumnAudioView_setColorBackground" format="color"></attr>
        <attr name="iosColumnAudioView_setColorLoud" format="color"></attr>
        <attr name="iosColumnAudioView_setRadiusXY" format="dimension"></attr>
        <attr name="iosColumnAudioView_setTextSize" format="dimension"></attr>
        <attr name="iosColumnAudioView_setTextColor" format="color"></attr>
        <attr name="iosColumnAudioView_setTextHeight" format="integer|dimension"></attr>
        <attr name="iosColumnAudioView_setIsDrawTextVolume" format="boolean"></attr>
        <attr name="iosColumnAudioView_setIsDrawDrawableVolume" format="boolean"></attr>
        <attr name="iosColumnAudioView_setVolumeColor" format="color"></attr>
        <attr name="iosColumnAudioView_setVolumeDrawable" format="reference"></attr>

    </declare-styleable>
    <declare-styleable
        name="IosColumnBrightnessView">
        <attr name="iosColumnBrightnessView_setColorBackground" format="color"></attr>
        <attr name="iosColumnBrightnessView_setColorBright" format="color"></attr>
        <attr name="iosColumnBrightnessView_setRadiusXY" format="dimension"></attr>
        <attr name="iosColumnBrightnessView_setTextSize" format="dimension"></attr>
        <attr name="iosColumnBrightnessView_setTextColor" format="color"></attr>
        <attr name="iosColumnBrightnessView_setTextHeight" format="integer|dimension"></attr>
        <attr name="iosColumnBrightnessView_setIsDrawTextBright" format="boolean"></attr>
        <attr name="iosColumnBrightnessView_setIsDrawDrawableBright" format="boolean"></attr>
        <attr name="iosColumnBrightnessView_setBrightnessColor" format="color"></attr>
    </declare-styleable>
</resources>

使用:

<LinearLayout
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintBottom_toBottomOf="parent"
    android:orientation="horizontal"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">
    <com.lgd.factoryattendance.View.IosColumnAudioView
        android:id="@+id/ios_audio"
        android:layout_margin="20dp"
        app:iosColumnAudioView_setRadiusXY="20dp"
        app:iosColumnAudioView_setVolumeDrawable="@drawable/tishi"
        android:layout_width="60dp"
        android:layout_height="200dp">
    </com.lgd.factoryattendance.View.IosColumnAudioView>

    <com.lgd.factoryattendance.View.IosColumnBrightnessView
        android:id="@+id/ios_brightness"
        android:layout_margin="20dp"
        app:iosColumnBrightnessView_setRadiusXY="20dp"
        android:layout_width="60dp"
        android:layout_height="200dp">
    </com.lgd.factoryattendance.View.IosColumnBrightnessView>

</LinearLayout>

六.自定义组合控件

自定义界面:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="50dp">

    <ImageView
        android:id="@+id/item_icon"
        android:layout_width="30dp"
        android:layout_height="30dp"
        android:layout_margin="10dp"
        android:src="@drawable/tishi"
        android:layout_centerVertical="true"/>

    <TextView
        android:id="@+id/item_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_toRightOf="@+id/item_icon"
        android:text="texttttt"
        android:textColor="#000000" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="11111"
        android:layout_centerInParent="true"/>

    <TextView
        android:id="@+id/item_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Crystal"
        android:layout_centerVertical="true"
        android:textColor="#B8860B"
        android:visibility="visible"
        android:layout_toLeftOf="@+id/item_arrow"/>

    <ImageView
        android:id="@+id/item_arrow"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:src="@drawable/down"
        android:layout_alignParentRight="true"
        android:layout_marginRight="10dp" />

    <View
        android:id="@+id/item_line"
        android:layout_width="match_parent"
        android:layout_height="0.5dp"
        android:layout_alignParentBottom="true"
        android:background="#D3D3D3"
        android:visibility="visible" />
</RelativeLayout>
自定义组合控件:
public class ItemView extends LinearLayout {

    private Boolean isBottom;
    private Boolean isItemText;
    private Boolean isArrow;
    private ImageView itemIcon;
    private ImageView rightArrow;
    private TextView leftTitle;
    private TextView itemText;
    private View itemLine;


    public ItemView(Context context, AttributeSet attrs) {
        super(context, attrs);
        LayoutInflater.from(getContext()).inflate(R.layout.item_view, this);
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ItemView);//解析布局

        isBottom = ta.getBoolean(R.styleable.ItemView_show_bottom_line, true); //底部分割线是否显示
        isItemText = ta.getBoolean(R.styleable.ItemView_show_right_text, true);//右边描述文字是否显示
        isArrow = ta.getBoolean(R.styleable.ItemView_show_right_arrow, true);  //右边箭头是否显示

        itemIcon = findViewById(R.id.item_icon);   //左边图标
        rightArrow = findViewById(R.id.item_arrow);//右边箭头
        leftTitle = findViewById(R.id.item_title); //左边标题文字
        itemText = findViewById(R.id.item_text);   //右边描述文字或数值
        itemLine = findViewById(R.id.item_line);   //底部分割线

        itemIcon.setImageDrawable(ta.getDrawable(R.styleable.ItemView_left_icon)); //图标赋值

//        leftTitle.setText(ta.getText(R.styleable.ItemView_left_title));
//        itemText.setText(ta.getText(R.styleable.ItemView_right_text));

        setLeftTitle(ta.getText(R.styleable.ItemView_left_title).toString());
        setItemText(ta.getText(R.styleable.ItemView_right_text).toString());

        itemText.setTextColor(ta.getColor(R.styleable.ItemView_right_text_color, Color.BLACK));
        itemText.setVisibility(isItemText ? VISIBLE : INVISIBLE);
        itemLine.setVisibility(isBottom ? VISIBLE : INVISIBLE);
        rightArrow.setVisibility(isArrow ? VISIBLE : GONE);


        //回收属性对象
        ta.recycle();

    }

    public void setLeftTitle(String text) {
        leftTitle.setText(text);
    }

    public void setItemText(String text) {
        itemText.setText(text);
    }

}
attrs.xml:
<declare-styleable name="ItemView">
    <attr name="left_icon" format="integer" /><!--左侧图标-->
    <attr name="left_title" format="string" /><!--左侧标题文字-->
    <attr name="right_text" format="string" /><!--右侧描述文字-->
    <attr name="right_text_color" format="color" /><!--右侧描述文字颜色-->
    <attr name="show_right_text" format="boolean" /><!--是否显示右侧描述文字-->
    <attr name="show_right_arrow" format="boolean" /> <!--是否显示右侧箭头-->
    <attr name="show_bottom_line" format="boolean" /> <!--是否显示下划线-->
</declare-styleable>

 七.自定义流式布局

方法一:

public class FlowLayout extends ViewGroup {
    private int mHorizontalSpacing = dp2px(16); //每个item横向间距
    private int mVerticalSpacing = dp2px(8); //每个item横向间距


    private List<List<View>> allLines = new ArrayList<>(); // 记录所有的行,一行一行的存储,用于layout
    List<Integer> lineHeights = new ArrayList<>(); // 记录每一行的行高,用于layout


    public FlowLayout(Context context) {
        super(context);
    }

    public FlowLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    private void clearMeasureParams() {
        allLines.clear();
        lineHeights.clear();
    }


    /*
     * 1、度量子view
     * 2、获取子view的宽、高、换行等
     * 3、向父类索要宽高,判断是哪种MeasureSpecMode,根据不同的mode给出不同的区域
     * 4、保存记录
     * */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        clearMeasureParams();

        List<View> lineViews = new ArrayList<>(); //保存一行中的所有的view
        int lineWidthUsed = 0; //记录这行已经使用了多宽的size
        int lineHeight = 0; // 一行的行高

        int selfWidth = MeasureSpec.getSize(widthMeasureSpec);  //父view给我的宽度
        int selfHeight = MeasureSpec.getSize(heightMeasureSpec); //父view给我的高度

        int flowLayoutNeedWidth = 0;  // measure过程中,FlowLayout要求的父ViewGroup的宽
        int flowLayoutNeedHeight = 0; // measure过程中,FlowLayout要求的父ViewGroup的高

        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingTop = getPaddingTop();
        int paddingBottom = getPaddingBottom();
        // 获取子view数
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            // 获取子view
            View childView = getChildAt(i);
            // 获取子view的layoutParams,通过layoutParams可得到子view的宽高具体值或者MATCH_PARENT还是WRAP_CONTENT
            LayoutParams childLP = childView.getLayoutParams();
            if (childView.getVisibility() != View.GONE) {
                //将layoutParams转变成为 measureSpec  即设置子view的measureSpec
                /*
                 * widthMeasureSpec表示父view给予FlowLayout的宽度
                 * paddingLeft + paddingRight表示父view所设置的左右padding值
                 * childLP.width  表示 子view的宽度
                 * */
                int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, paddingLeft + paddingRight, childLP.width);
                int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, paddingTop + paddingBottom, childLP.height);
                // 通过子view的measureSpec度量子view
                childView.measure(childWidthMeasureSpec, childHeightMeasureSpec);

                //获取子view的度量宽高
                int childMeasureWidth = childView.getMeasuredWidth();
                int childMeasureHeight = childView.getMeasuredHeight();

                //换行
                if(lineWidthUsed + mHorizontalSpacing + childMeasureWidth > selfWidth ){
                    //一旦换行,我们就可以判断当前行需要的宽和高,所以此时要记录下来
                    allLines.add(lineViews);
                    lineHeights.add(lineHeight);
                    //判断flowLayout到底需要多宽、多高
                    flowLayoutNeedWidth = Math.max(flowLayoutNeedWidth, lineWidthUsed);
                    flowLayoutNeedHeight = flowLayoutNeedHeight + lineHeight + mVerticalSpacing;

                    // 换行后初始化
                    // 此处不能用clear,用clear则allLines里面的item所指向的就是同一个内存地址了
                    lineViews = new ArrayList<>();
                    lineWidthUsed = 0;
                    lineHeight = 0;
                }

                //每行的设置
                lineViews.add(childView);
                lineWidthUsed = lineWidthUsed + mHorizontalSpacing + childMeasureWidth;
                lineHeight = Math.max(lineHeight, childMeasureHeight);

                //最后一行数据(因为最后一行的时候到不了换行的那句代码,所以不会显示,因此要单独判断)
                if(i == childCount -1){
                    allLines.add(lineViews);
                    lineHeights.add(lineHeight);
                    //判断flowLayout到底需要多宽、多高
                    flowLayoutNeedWidth = Math.max(flowLayoutNeedWidth, lineWidthUsed);
                    flowLayoutNeedHeight = flowLayoutNeedHeight + lineHeight + mVerticalSpacing;
                }
            }
        }

        //根据子View的度量结果,来重新度量自己ViewGroup
        // 作为一个ViewGroup,它自己也是一个View,它的大小也需要根据它的父view给它提供的宽高来度量
        //首先获取到父view的MeasureSpec的mode
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        // 如果父view的MeasureSpec的mode是EXACTLY表示宽度是确切的,则selfWidth为最终宽度,否则为
        int flowLayoutWidth = (widthMode == MeasureSpec.EXACTLY) ? selfWidth : flowLayoutNeedWidth;
        int flowLayoutHeight = (heightMode == MeasureSpec.EXACTLY) ? selfHeight : flowLayoutNeedHeight;

        // 保存记录
        setMeasuredDimension(flowLayoutWidth, flowLayoutHeight);

    }

    // 布局(每一行每一行的布局)
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        // 获取行数 最大显示3行
        int lineCount = Math.min(allLines.size(), 3);
        // 获取flowLayout所设置的pandding值,布局从左上角开始
        int curL = getPaddingLeft();
        int curT = getPaddingTop();
        for (int i = 0; i < lineCount; i++) {
            // 获取到每一行的所有view
            List<View> lineViews = allLines.get(i);

            for (int j = 0; j < lineViews.size(); j++){
                //获取单个view
                View view = lineViews.get(j);
                //设置view的视图坐标系,
                int left = curL;
                int top = curT;
                int right = left + view.getMeasuredWidth();
                int bottom = top + view.getMeasuredHeight();
                // view添加到布局
                view.layout(left,top,right,bottom);

                // 计算下一个view的宽度的开始位置
                curL = right + mHorizontalSpacing;
            }

            // 计算下一行view的高度的开始位置
            curT = curT + lineHeights.get(i) + mVerticalSpacing;
            // 宽度位置初始化
            curL = getPaddingLeft();
        }

    }

    public List<List<View>> getAllLines() {
        return allLines;
    }

    public static int dp2px(int dp) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().getDisplayMetrics());
    }
}

 方法二:

public class FlowLayout extends ViewGroup {

    private final String TAG = "FlowLayout";

    private float mRowSpacing = 0.0f;// 行间距
    private float mColumnSpacing = 0.0f;// 列间距

    public FlowLayout(Context context) {
        this(context, null);
    }

    public FlowLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public FlowLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.FlowLayout);
        mRowSpacing = typedArray.getDimensionPixelSize(R.styleable.FlowLayout_rowSpacing, 0);
        mColumnSpacing = typedArray.getDimensionPixelSize(R.styleable.FlowLayout_columnSpacing, 0);
        typedArray.recycle();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        int expectHeight = 0;// 期望高度,累加 child 的 height
        int lineWidth = 0;// 单行宽度,动态计算当前行的宽度。
        int lineHeight = 0;// 单行高度,取该行中高度最大的view
        float widthSpacing;

        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            // 测量子view 的宽高
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            int childWidth = child.getMeasuredWidth();
            int childHeight = child.getMeasuredHeight();

            widthSpacing = i == 0 ? 0 : mColumnSpacing;

            // 这里进行的是预判断。追加该child 后,行宽
            // 若未超过提供的最大宽度,则行宽需要追加child 的宽度,并且计算该行的最大高度。
            // 若超过提供的最大宽度,则需要追加该行的行高,并且更新下一行的行宽为当前child 的测量宽度。
            if (lineWidth + widthSpacing + childWidth + getPaddingLeft() + getPaddingRight() <= widthSize) {// 未超过一行
                // 追加行宽。
                lineWidth += widthSpacing + childWidth;
                // 不断对比,获取该行的最大高度
                lineHeight = Math.max(lineHeight, childHeight);

            } else {// 超过一行
                // 更新最新一行的宽度为此child 的测量宽度
                lineWidth = childWidth;
                // 期望高度追加当前行的行高。
                expectHeight += lineHeight + mRowSpacing;
            }
        }

        // 这里添加的是最后一行的高度。因为上面是在换行时才追加的行高,在不需要换行时并没有追加行高,丢失了不满足换行条件下的行高。
        // 举例说明:比如一行最多显示5个,但是当前只有1个,或者当前有6个的情况下,少了一行的行高。
        expectHeight += lineHeight;

        // 追加ViewGroup 的内边距
        expectHeight += getPaddingTop() + getPaddingBottom();
        setMeasuredDimension(widthSize, resolveSize(expectHeight, heightMeasureSpec));
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int width = r - l;
        int childLeftOffset = getPaddingLeft();// child view 的left偏移距离,用于记录左边位置。
        int childTopOffset = getPaddingTop();// child view 的top偏移距离,用于记录顶部位置。

        int lineHeight = 0;// 行高

        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            //跳过View.GONE的子View
            if (child.getVisibility() == View.GONE) {
                continue;
            }

            int childWidth = child.getMeasuredWidth();
            int childHeight = child.getMeasuredHeight();

            if (childLeftOffset + childWidth + getPaddingRight() <= width) {// 该child 加入后未超过一行宽度。
                // 行高取这一行中最高的child height
                lineHeight = Math.max(lineHeight, childHeight);

            } else {// 超过一行,换行显示。换行后的左侧偏移为初始值,顶部偏移为当前偏移量+当前行高
                childLeftOffset = getPaddingLeft();
                childTopOffset += lineHeight + mRowSpacing;
                lineHeight = childHeight;
            }
            child.layout(childLeftOffset, childTopOffset, childLeftOffset + childWidth, childTopOffset + childHeight);

            // 更新左侧偏移距离(+间距),即下一个child 的left
            childLeftOffset += childWidth + mColumnSpacing;
        }
    }

    public void setAdapter(BaseAdapter mAdapter) {
        this.removeAllViews();
        for (int i = 0; i < mAdapter.getCount(); i++) {
            View view = mAdapter.getView(i, null, this);
            this.addView(view);
        }
        requestLayout();
    }

    @Override
    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }
}

八.TextView文本的展开与折叠 

原文地址:110.Android简单的自定义折叠文本,展开与隐藏TextView,自定义视图CollapsibleTextView,自定义View,文本展示_安卓写一段折叠文字视图的layout-优快云博客

public class CollapsibleTextView extends LinearLayout {

    /**
     * 默认(展开与隐藏)文本颜色 蓝色
     */
    private ForegroundColorSpan expandAndHideColorSpan = new ForegroundColorSpan(Color.parseColor("#3b76ec"));

    /**
     * 默认(内容文本)文本颜色 黑色
     */
    private ForegroundColorSpan textColorSpan = new ForegroundColorSpan(Color.parseColor("#000000"));

    /**
     * 文本显示最大行数,默认为1
     */
    private int maxLines = 1;

    /**
     * 标记
     */
    private boolean flag, flag2, flag3;

    /**
     * 文本
     */
    private String text;

    /**
     * 文本长度
     */
    private int length;

    private TextView mCollapsibleTextView;

    public CollapsibleTextView(Context context) {
        super(context);
    }

    public CollapsibleTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        View view = inflate(context, R.layout.layout_collapsible_textview, this);
        mCollapsibleTextView = (TextView) view.findViewById(R.id.mCollapsibleTextView);
        //设置textView可点击
        mCollapsibleTextView.setMovementMethod(LinkMovementMethod.getInstance());
    }

    /**
     * 对外提供暴露的方法,为设置文本内容、文本显示最大行数、(内容文本)文本颜色、(展开与隐藏)文本颜色
     *
     * @param text               设置文本内容
     * @param maxLines           设置文本显示最大行数
     * @param textColor          (内容文本)文本颜色
     * @param expandAndHideColor (展开与隐藏)文本颜色
     */
    public void setText(String text, int maxLines, int textColor, int expandAndHideColor) {
        this.text = text;
        //赋值文本显示最大行数,最小是1
        this.maxLines = Math.max(maxLines, 1);
        //得到字符串的长度
        length = text.length();
        //setText调用就设置flag为true
        flag = true;
        //flag3为false时 设置一次最大行数
        if (!flag3) {
            //设置默认最大行数
            mCollapsibleTextView.setMaxLines(maxLines);
        }
        //设置文本
        mCollapsibleTextView.setText(text);
        //设置(内容文本)文本颜色
        textColorSpan = new ForegroundColorSpan(ContextCompat.getColor(getContext(), textColor));
        //设置(展开与隐藏)文本颜色
        expandAndHideColorSpan = new ForegroundColorSpan(ContextCompat.getColor(getContext(), expandAndHideColor));
        //重新请求布局
        requestLayout();
    }

    /**
     * 文本设置
     * 设置展开或隐藏
     *
     * @param count
     * @param str
     * @return
     */
    private SpannableString getClickableSpan(int count, String str) {
        //新建一个spannableString(设置字体颜色,监听点击,字体大小)
        SpannableString span = new SpannableString(str);
        //count == 0时设置文本颜色并返回
        if (count == 0) {
            //内容文本 设置字体大小
            span.setSpan(new RelativeSizeSpan(1f), 0, str.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            //内容文本 设置字体可点击并监听
            span.setSpan(new TextListener(), 0, str.length(), Spanned.SPAN_MARK_MARK);
            //内容文本 设置字体颜色
            span.setSpan(textColorSpan, 0, str.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            //返回span
            return span;
        }
        int maxLines = mCollapsibleTextView.getMaxLines();
        if (maxLines == this.maxLines) {
            //内容文本 设置字体大小
            span.setSpan(new RelativeSizeSpan(1f), 0, count - 5, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            //内容文本 设置字体可点击并监听
            span.setSpan(new TextListener(), 0, count - 5, Spanned.SPAN_MARK_MARK);
            //内容文本 设置字体颜色
            span.setSpan(textColorSpan, 0, count - 5, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

            //展开与隐藏 设置字体大小
            span.setSpan(new RelativeSizeSpan(1.1f), count - 5, count, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            //展开与隐藏 设置字体可点击并监听
            span.setSpan(new ExpandAndHideListener(), count - 5, count, Spanned.SPAN_MARK_MARK);
            //展开与隐藏 设置字体颜色
            span.setSpan(expandAndHideColorSpan, count - 5, count, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        } else {
            //内容文本 设置字体大小
            span.setSpan(new RelativeSizeSpan(1f), 0, count, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            //内容文本 设置字体可点击并监听
            span.setSpan(new TextListener(), 0, count, Spanned.SPAN_MARK_MARK);
            //内容文本 设置字体颜色
            span.setSpan(textColorSpan, 0, count, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

            //展开与隐藏 设置字体大小
            span.setSpan(new RelativeSizeSpan(1.1f), count, count + 3, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            //展开与隐藏 设置字体可点击并监听
            span.setSpan(new ExpandAndHideListener(), count, count + 3, Spanned.SPAN_MARK_MARK);
            //展开与隐藏 设置字体颜色
            span.setSpan(expandAndHideColorSpan, count, count + 3, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        }
        //返回span
        return span;
    }

    /**
     * 1.展开与隐藏的点击事件
     * 2.(内容文本的点击事件 与 展开与隐藏文本的点击事件  可全部使用也可只使用一个 这里全部使用)
     */
    private class ExpandAndHideListener extends ClickableSpan implements View.OnClickListener {
        //文字点击事件
        @Override
        public void onClick(@NonNull View widget) {
            //flag设置并重新请求布局
            flagSetting();
        }

        @Override
        public void updateDrawState(@NonNull TextPaint ds) {
            super.updateDrawState(ds);
            ds.setUnderlineText(false);
        }
    }

    /**
     * 1.内容文本的点击事件
     * 2.(内容文本的点击事件 与 展开与隐藏文本的点击事件  可全部使用也可只使用一个 这里全部使用)
     */
    private class TextListener extends ClickableSpan implements View.OnClickListener {
        //文字点击事件
        @Override
        public void onClick(@NonNull View widget) {
            //flag设置并重新请求布局
            flagSetting();
        }

        @Override
        public void updateDrawState(@NonNull TextPaint ds) {
            super.updateDrawState(ds);
            ds.setUnderlineText(false);
        }
    }

    /**
     * flag设置并重新请求布局
     */
    private void flagSetting() {
        flag = true;
        if (flag3) {
            //mCollapsibleTextView.getLineCount() <= defaultMaxLines返回true 反之false
            flag2 = mCollapsibleTextView.getLineCount() <= maxLines;
            //重新请求布局
            requestLayout();
        }
    }

    /**
     * 布局设置
     *
     * @param changed
     * @param l
     * @param t
     * @param r
     * @param b
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        //flag为false返回
        if (!flag) {
            return;
        }
        //设置为false
        flag = false;

        //flag2为true展开
        if (flag2) {
            //展开状态
            mCollapsibleTextView.setMaxLines(Integer.MAX_VALUE);
            //在原字符串后面加上“ 隐藏”
            String allText = text + " 隐藏";
            SpannableString clickableSpan = getClickableSpan(length, allText);
            mCollapsibleTextView.setText(clickableSpan);
            flag3 = true;
        } else {
            //flag2为false 判断mCollapsibleTextView.getLineCount() > defaultMaxLines
            if (mCollapsibleTextView.getLineCount() > maxLines) {
                //收起状态
                mCollapsibleTextView.setMaxLines(maxLines);
                //得到第一行最后一个字符的位置
                int lineEnd = mCollapsibleTextView.getLayout().getLineEnd(maxLines - 1);
                //截取第一行的字符 替换最后五个字符为“...展开”
                String hiddenString = text.substring(0, lineEnd - 5) + "...展开";
                SpannableString clickableSpan = getClickableSpan(lineEnd, hiddenString);
                mCollapsibleTextView.setText(clickableSpan);
                flag3 = true;
            } else {
                SpannableString clickableSpan = getClickableSpan(0, text);
                mCollapsibleTextView.setText(clickableSpan);
            }
        }
    }

}

xml文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">
    <TextView
        android:id="@+id/mCollapsibleTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@android:color/transparent"
        android:text="内容"
        android:textSize="14sp" />
</LinearLayout>

应用:

CollapsibleTextView view =  findViewById(R.id.mCollapsibleView);
view.setText("传入文本内容",1, R.color.red, R.color.blue);                                                   

view.setText("传入文本内容", 1, Color.parseColor("#000000"), Color.parseColor("#3b76ec"));   如此应用会报android.content.res.Resources$NotFoundException: Resource ID #0xff000000  错误

 九.switch-button  样式

参考:62.android 简单的切换状态的Button_button状态切换 android-优快云博客

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值