Android自定义控件——控件的测量和绘制

news2024/11/21 1:32:01

控件的测量和绘制

  • 控件架构
  • setContentView()
  • MeasureSpec
  • View
    • View的测量
    • View的绘制
  • ViewGroup
    • ViewGroup的测量
    • ViewGroup的绘制
  • 自定义View
    • 对现有控件进行扩展
      • 案例一:添加背景
      • 案例二:文字闪动
    • 创建复合控件
    • 重写View实现全新控件
      • 案例一:弧线展示图
      • 案例二:音频条形图
  • 自定义ViewGroup
    • 滑动组件

控件架构

控件分为两大类,即ViewGroup和View

ViewGroup又可包含View,形成控件树,常用的fingViewById()方法依据深度优先遍历查找控件

上层控件负责下层子控件的测量和绘制,并传递交互事件

setContentView()

Android界面框架图如下

在这里插入图片描述

  • 每个Activity都包含一个PhoneWindow对象
  • PhoneWindow将一个DecorView设置为根View,其分为TitleView和ContentView(Framelayout)
  • setContentView()将activity_main.xml设置到ContentView,ActivityManagerService回调onResume()将DecorView添加到PhoneWindow显示出来
  • DecorView中的View监听事件,通过WindowManagerService接收并回调到Activity的onClickListener

将上图转为视图树如下

在这里插入图片描述

  • DecoreView以LinearLayout上下布局
  • 上面为ActionBar显示标题,下面为Content显示内容
  • 若设置requestWindowFeature(Window.FEATURE_NO_TITLE)全屏显示,则只存在Content,故其需要在setContView()之前调用才能生效

MeasureSpec

MeasureSpec是一个32位int值,高2位为测量模式,低30位为测量大小,模式分为

  • EXACTLY:layout_width、layout_height 为具体数值或match_parent
  • AT_MOST:layout_width、layout_height 为 wrap_content,在父控件宽高范围内,控件大小随子控件或内容变化而变化
  • UNSPECIFIED,不指定其大小和测量模式,View想多大就多大,用于绘制自定义View

View

View的测量

View的测量由Measure()方法完成,用于确定View的宽高,并通过setMeasuredDimension()设置

在这里插入图片描述

如上,View类的默认onMeasure方法只支持EXACTLY,当宽高设置wrap_content时会填充整个父布局,接下来自定义View实现wrap_content下设置一个默认大小

public class MyView extends View {

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

    public MyView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(measureWidthOrHeight(widthMeasureSpec), measureWidthOrHeight(heightMeasureSpec));
    }

    private int measureWidthOrHeight(int measureSpec) {
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        if (specMode == MeasureSpec.EXACTLY) {
            result = specSize;
        } else {
            result = 200;
            if (specMode == MeasureSpec.AT_MOST) {
                result = Math.min(result, specSize);
            }
        }
        return result;
    }
}

如上自定义View,其测量过程如下

  • 从MeasureSpec获取具体的模式specMode和大小specSize
  • 当若specMode为EXACTLY,使用指定的大小specSize
  • 否则设置默认大小为200,当specMode为AT_MOST(即wrap_content)需取最小值作为最终值

如下设置为wrap_content时,默认为200,而不是填充整个父布局

在这里插入图片描述

View的绘制

View的绘制由onDraw()方法完成,并在其参数canvas上调用drawXXX()方法绘制图像,canvas通过bitmap用于存储所有绘制信息

可让onDraw()中的canvas绑定bitmap并暴露出去,实现通过修改共享bitmap的方法更新UI

public class MyView extends View {

    public static Bitmap bitmap;

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

    public MyView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        bitmap = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888);	//这里未转换单位
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(measureWidthOrHeight(widthMeasureSpec), measureWidthOrHeight(heightMeasureSpec));
    }

    private int measureWidthOrHeight(int measureSpec) {
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        if (specMode == MeasureSpec.EXACTLY) {
            result = specSize;
        } else {
            result = 200;
            if (specMode == MeasureSpec.AT_MOST) {
                result = Math.min(result, specSize);
            }
        }
        return result;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawBitmap(bitmap, 0, 0, null);
        super.onDraw(canvas);
    }
}

如上,修改MyView,构造函数中创建bitmap,onDraw方法中绑定bitmap

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="替换背景" />

    <com.demo.demo0.MyView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#ff0000" />
</LinearLayout>

如上,添加按钮和自定义View

public class MainActivity extends AppCompatActivity {
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button test = findViewById(R.id.test);
        test.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Canvas canvas = new Canvas(MyView.bitmap);
                canvas.drawColor(Color.GREEN);
            }
        });
    }
}

如上,在点击事件中,根据MyView.bitmap初始化一个Canvas,执行drawColor()方法,通过改变bitmap让View重绘实现更新颜色

ViewGroup

ViewGroup的测量

  • 同上面,若需要让自定义ViewGroup支持wrap_content,需要重写onMeasure()遍历子View的大小来决定自身大小,其他模式则由具体值决定
  • 测量结束后,需重写onLayout()遍历子View,控制其显示位置逻辑

ViewGroup的绘制

ViewGroup通常情况下不需要绘制,只有在指定背景色时其onDraw()方法才会调用

但ViewGroup使用dispatchDraw()方法遍历并绘制子View

自定义View

对现有控件进行扩展

案例一:添加背景

如下继承TextView

public class MyTextView extends androidx.appcompat.widget.AppCompatTextView {

    private Paint mPaint1;
    private Paint mPaint2;

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

    public MyTextView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mPaint1 = new Paint();
        mPaint1.setColor(getResources().getColor(android.R.color.holo_blue_light));
        mPaint1.setStyle(Paint.Style.FILL);
        mPaint2 = new Paint();
        mPaint2.setColor(Color.YELLOW);
        mPaint2.setStyle(Paint.Style.FILL);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //绘制蓝色外层矩形
        canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint1);
        //绘制黄色内层矩形
        canvas.drawRect(10, 10, getMeasuredWidth() - 10, getMeasuredHeight() - 10, mPaint2);
        canvas.save();
        //将文字平移10像素,避免遮挡
        canvas.translate(10, 0);
        super.onDraw(canvas);
        canvas.restore();
    }
}

在其onDraw()绘制文字之前,绘制两个不同的矩形背景,如下

在这里插入图片描述

案例二:文字闪动

如下继承TextView

public class MyTextView extends androidx.appcompat.widget.AppCompatTextView {

    private Paint mPaint;
    private int mViewWidth;
    private LinearGradient mLinearGradient;
    private Matrix mGradientMatrix;
    private int mTranslate = -mViewWidth;

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

    public MyTextView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mGradientMatrix != null) {
            mTranslate += mViewWidth / 5;   //每隔100毫秒加1/5
            if (mTranslate > 2 * mViewWidth) {  //实现循环,超过2倍宽,从头开始
                mTranslate = -mViewWidth;
            }
            mGradientMatrix.setTranslate(mTranslate, 0);//让矩阵不断平移
            mLinearGradient.setLocalMatrix(mGradientMatrix);//将矩阵设置到Gradient
            postInvalidateDelayed(100);
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (mViewWidth == 0) {
            mViewWidth = getMeasuredWidth();
            if (mViewWidth > 0) {
                mPaint = getPaint();    //获取TextView的Paint
                mLinearGradient = new LinearGradient(0, 0, mViewWidth, 0,
                        new int[]{Color.BLUE, 0xffffffff, Color.BLUE},
                        null, Shader.TileMode.CLAMP);
                mPaint.setShader(mLinearGradient);//为其设置Shader
                mGradientMatrix = new Matrix();
            }
        }
    }
}

为TextView设置LinearGradient线性渐变,通过矩阵不断平移渐变效果,从而在绘制文字时,产生动态闪动效果

在这里插入图片描述

创建复合控件

如下创建一个标题栏TopBar,在res-values新建attrs.xml定义属性

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="TopBar">
        <attr name="title" format="string" />
        <attr name="titleTextSize" format="dimension" />
        <attr name="titleTextColor" format="color" />

        <attr name="leftTextColor" format="color" />
        <attr name="leftBackground" format="reference|color" />
        <attr name="leftText" format="string" />

        <attr name="rightTextColor" format="color" />
        <attr name="rightBackground" format="reference|color" />
        <attr name="rightText" format="string" />
    </declare-styleable>
</resources>

新建TopBar继承RelativeLayout,初始化属性-控件-点击事件-暴露方法

public class TopBar extends RelativeLayout {

    private int mLeftTextColor;
    private Drawable mLeftBackground;
    private String mLeftText;
    private int mRightTextColor;
    private Drawable mRightBackground;
    private String mRightText;
    private float mTitleTextSize;
    private int mTitleTextColor;
    private String mTitle;
    private Button mLeftButton;
    private Button mRightButton;
    private TextView mTitleView;
    private LayoutParams mLeftParams;
    private LayoutParams mRightParams;
    private LayoutParams mTitleParams;
    private topBarClickListener mListener;

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

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

    public TopBar(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public TopBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        initAttrs(context, attrs);
        initView(context);
        initListener();
    }

    public interface topBarClickListener {
        void leftClick();

        void RightClick();
    }

    public void setOnTopBarClickListener(topBarClickListener listener) {
        this.mListener = listener;
    }

    public void setButtonVisible(int id, boolean flag) {
        if (flag) {
            if (id == 0) {
                mLeftButton.setVisibility(VISIBLE);
            } else {
                mRightButton.setVisibility(VISIBLE);
            }
        } else {
            if (id == 0) {
                mLeftButton.setVisibility(GONE);
            } else {
                mRightButton.setVisibility(GONE);
            }
        }
    }

    private void initListener() {
        mLeftButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                mListener.leftClick();
            }
        });
        mRightButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                mListener.RightClick();
            }
        });
    }

    private void initView(Context context) {
        mLeftButton = new Button(context);
        mRightButton = new Button(context);
        mTitleView = new TextView(context);

        mLeftButton.setTextColor(mLeftTextColor);
        mLeftButton.setBackground(mLeftBackground);
        mLeftButton.setText(mLeftText);

        mRightButton.setTextColor(mRightTextColor);
        mRightButton.setBackground(mRightBackground);
        mRightButton.setText(mRightText);

        mTitleView.setText(mTitle);
        mTitleView.setTextColor(mTitleTextColor);
        mTitleView.setTextSize(mTitleTextSize);
        mTitleView.setGravity(Gravity.CENTER);

        mLeftParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
        mLeftParams.addRule(ALIGN_PARENT_LEFT, TRUE);
        addView(mLeftButton, mLeftParams);

        mRightParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
        mRightParams.addRule(ALIGN_PARENT_RIGHT, TRUE);
        addView(mRightButton, mRightParams);

        mTitleParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
        mTitleParams.addRule(CENTER_IN_PARENT, TRUE);
        addView(mTitleView, mTitleParams);
    }

    private void initAttrs(Context context, AttributeSet attrs) {
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TopBar);
        mLeftTextColor = ta.getColor(R.styleable.TopBar_leftTextColor, 0);
        mLeftBackground = ta.getDrawable(R.styleable.TopBar_leftBackground);
        mLeftText = ta.getString(R.styleable.TopBar_leftText);

        mRightTextColor = ta.getColor(R.styleable.TopBar_rightTextColor, 0);
        mRightBackground = ta.getDrawable(R.styleable.TopBar_rightBackground);
        mRightText = ta.getString(R.styleable.TopBar_rightText);

        mTitleTextSize = ta.getDimension(R.styleable.TopBar_titleTextSize, 10);
        mTitleTextColor = ta.getColor(R.styleable.TopBar_titleTextColor, 0);
        mTitle = ta.getString(R.styleable.TopBar_title);
        ta.recycle();
    }

}

创建topbar.xml,指定命名空间app引用自定义属性

<?xml version="1.0" encoding="utf-8"?>
<com.demo.demo0.TopBar xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/topBar"
    android:layout_width="match_parent"
    android:layout_height="40dp"
    app:leftBackground="@color/colorPrimaryDark"
    app:leftText="Back"
    app:leftTextColor="#FFFFFF"
    app:rightBackground="@color/colorPrimaryDark"
    app:rightText="More"
    app:rightTextColor="#FFFFFF"
    app:title="自定义标题"
    app:titleTextColor="#123412"
    app:titleTextSize="10sp">
</com.demo.demo0.TopBar>

通过<include>引用

在这里插入图片描述

重写View实现全新控件

案例一:弧线展示图

public class MyView extends View {
    private Paint mCirclePaint, mArcRectPaint, mArcPaint, mTextPaint;

    private float length;
    private float mRadius;
    private RectF mArcRect;
    private float mCircleXY;
    private static final float DEFAULT_SWEEP_ANGLE = 245;
    private float mSweepAngle = DEFAULT_SWEEP_ANGLE;
    private String mShowText = mSweepAngle + "°";

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

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

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

    private void initView() {
        mCirclePaint = new Paint();
        mCirclePaint.setColor(Color.parseColor("#55FF0000"));

        mArcRectPaint = new Paint();
        mArcRectPaint.setColor(Color.GRAY);

        mArcPaint = new Paint();
        mArcPaint.setColor(Color.parseColor("#AAFF0000"));
        mArcPaint.setStrokeWidth(100);
        mArcPaint.setStyle(Paint.Style.STROKE);

        mTextPaint = new Paint();
        mTextPaint.setColor(Color.BLACK);
        mTextPaint.setTextSize(50);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        length = w;
        mCircleXY = length / 2;         //圆心 = (length / 2, length / 2)
        mRadius = (float) (length * 0.5 / 2);   //半径 = length / 4
        mArcRect = new RectF((float) (length * 0.1), (float) (length * 0.1),
                (float) (length * 0.9), (float) (length * 0.9));    //圆弧的内切矩形,上下去掉0.1length
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //mArcPaint.setStrokeWidth(10);
        //canvas.drawRect(mArcRect, mArcRectPaint);   //用于展示内切矩形,了解原理
        canvas.drawCircle(mCircleXY, mCircleXY, mRadius, mCirclePaint); //画圆,传入圆心半径

        //画弧线,传入内切正方形,从270度即0点(0度位置在3点)开始顺时针转mSweepAngle角度,true会连接圆心,false只画边界
        canvas.drawArc(mArcRect, 270, mSweepAngle, false, mArcPaint);
        //画文字,传入文字,传入开始、结束位置、绘制在(x,y)处
        float textWidth = mTextPaint.measureText(mShowText); //测量文字宽度,为了让其居中
        canvas.drawText(mShowText, 0, mShowText.length(), mCircleXY - (textWidth / 2), mCircleXY + (textWidth / 4), mTextPaint);
    }

    public void setSweepAngle(float angle) {
        if (angle != 0) {
            mSweepAngle = angle;
        } else {
            mSweepAngle = DEFAULT_SWEEP_ANGLE;
        }
        invalidate();
    }
}

由一个圆、圆弧和文字组成,可通过setSweepAngle()设置显示比例

在这里插入图片描述

案例二:音频条形图

public class MyView extends View {

    private double mRandom;
    private int mWidth;
    private int mRectHeight;
    private int mRectWidth;
    private double mRectCount = 10;
    private Paint mPaint;
    private LinearGradient mLinearGradient;

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

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

    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mPaint = new Paint();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = getWidth();        //View宽度
        mRectHeight = getHeight();  //View高度
        mRectWidth = (int) (mWidth * 0.6 / mRectCount); //第一个长方形的left坐标,为总宽度60%除以个数
        mLinearGradient = new LinearGradient(
                0,
                0,
                mRectWidth,
                mRectHeight,
                Color.YELLOW,
                Color.BLUE,
                Shader.TileMode.CLAMP); //左上角(0,0)到右下角(mRectWidth,mRectHeight)从黄到蓝的线性渐变
        mPaint.setShader(mLinearGradient);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int offset = 5;
        for (int i = 0; i < mRectCount; i++) {
            mRandom = Math.random();
            float currentHeight = (float) (mRectHeight * mRandom);
            canvas.drawRect(
                    (float) (mWidth * 0.4 / 2 + mRectWidth * i + offset),   //left, mWidth * 0.4 / 2 为了居中, mRectWidth * i 计算每个长方形left, offset为空隙
                    currentHeight,  //top, 随机值
                    (float) (mWidth * 0.4 / 2 + mRectWidth * (i + 1)),  //right, mWidth * 0.4 / 2 为了居中, mRectWidth * (i + 1)计算每个长方形right
                    mRectHeight,    //bottom, 以左上角为原点计算
                    mPaint);
        }
        postInvalidateDelayed(300); //300ms后重绘实现动态显示
    }
}

onDraw()通过不断创建长方形并平移,实现模拟音频条的跳动

在这里插入图片描述

自定义ViewGroup

滑动组件

当子View上拉超过一定距离,松开后自动滑到下一个子View,下滑同理,布局如下

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ScrollViewActivity">

    <com.demo.demo0.MyScrollView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#ff0000" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#00ff00" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#0000ff" />
    </com.demo.demo0.MyScrollView>
</LinearLayout>

通过触摸事件获取Y值坐标判断滑动

public class MyScrollView extends ViewGroup {

    private static final String TAG = MyScrollView.class.getSimpleName();
    private int mScreenHeight;
    private Scroller mScroller;
    private int mLastY;
    private int mStart;
    private int mEnd;

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

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

    public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        initView(context);
    }

    private void initView(Context context) {
        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics dm = new DisplayMetrics();
        wm.getDefaultDisplay().getMetrics(dm);
        mScreenHeight = dm.heightPixels;    //上面获取屏幕的高度
        mScroller = new Scroller(context);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childCount = getChildCount();
        MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
        mlp.height = mScreenHeight * childCount;    //父View实际高度=子View高度之和
        setLayoutParams(mlp);
        for (int i = 0; i < childCount; i++) {  //将图片依次拼接,摆放子View
            View child = getChildAt(i);
            if (child.getVisibility() != View.GONE) {
                child.layout(
                        l,
                        i * mScreenHeight,
                        r,
                        (i + 1) * mScreenHeight);
            }
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int count = getChildCount();
        for (int i = 0; i < count; ++i) {   //每个子View宽高等于ViewGroup显示宽高,即占满屏幕
            View childView = getChildAt(i);
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);
        }
        setMeasuredDimension(widthMeasureSpec, heightMeasureSpec * count);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastY = y;
                mStart = getScrollY();  //ScrollY是top向Y轴方向滚动的距离
                break;
            case MotionEvent.ACTION_MOVE:
                /*if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }*/
                int dy = mLastY - y;
                Log.d(TAG, "onTouchEvent: dy = " + dy);
                if (getScrollY() < 0) { //已经滑到第一个
                    dy = 0;
                }
                if (getScrollY() > (getChildCount() - 1) * mScreenHeight) {  //已经滑到最后一个
                    dy = 0;
                }
                scrollBy(0, dy);
                mLastY = y;     //实时更新滑动坐标,计算偏移量
                break;
            case MotionEvent.ACTION_UP:
                mEnd = getScrollY();
                int dScrollY = mEnd - mStart;
                Log.d(TAG, "onTouchEvent: getScrollY() = " + getScrollY());
                Log.d(TAG, "onTouchEvent: dScrollY = " + dScrollY);
                if (dScrollY > 0) {//向下滑动
                    //超过屏幕高度的1/3,则展示上一张图片,否则反弹显示当前图片
                    if (dScrollY < mScreenHeight / 3) {
                        mScroller.startScroll(
                                0, getScrollY(),
                                0, -dScrollY);
                    } else {
                        mScroller.startScroll(
                                0, getScrollY(),
                                0, mScreenHeight - dScrollY);
                    }
                } else {
                    if (-dScrollY < mScreenHeight / 3) {
                        mScroller.startScroll(
                                0, getScrollY(),
                                0, -dScrollY);
                    } else {
                        mScroller.startScroll(
                                0, getScrollY(),
                                0, -mScreenHeight - dScrollY);
                    }
                }
                break;
        }
        postInvalidate();
        return true;
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {
            scrollTo(0, mScroller.getCurrY());
            postInvalidate();
        }
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/540917.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

FLEXPART--空气块轨迹-扩散模式

模式简介&#xff1a; FLEXPART(Flexible Particle Dispersion Model)模式是由挪威大气研究所(Norwegian Institute for Air Research)和德国慕尼黑工业大学(Technical University of Munich)联合开发的空气块轨迹&#xff0d;扩散模式, 其通过计算区域内所有气块的运动轨迹进…

PLE - 多任务模型(腾讯)

文章目录 1、动机2、模型结构3、代码实现细节&#xff1a; Progressive Layered Extraction (PLE): A Novel Multi-Task Learning (MTL) Model for Personalized Recommendations论文发表在 RecSys-2020&#xff0c; best paper。ple主要缓解了mtl里两大问题&#xff1a;负迁移…

如何在海量、庞杂、混合的数据中发现价值?

数字时代&#xff0c;数据上升为国家战略&#xff0c;数据成为重要的生产要素和资产&#xff0c;得到了越来越多企业的重视&#xff0c;也成为企业数字化转型的重要抓手。据IDC中国预测&#xff0c;2025年中国大数据生产量有望增长至48.6ZB。 随着越来越大的数据量&#xff0c…

【C++从0到王者】第六站:类和对象(下)

文章目录 一、再谈构造函数1.构造函数体赋值2.初始化列表1>初始化列表的使用2>初始化列表的注意事项 3.explicit关键词 二、static成员1.如何统计当前程序中变量的个数2.static的特性3.从1加到n4.设计一个类&#xff0c;只能在栈或者堆上开辟空间 三、友元1.友元函数2.友…

一个简化、落地的实时数据仓库解决方案

从传统的经验来讲&#xff0c;数据仓库有一个很重要的功能是记录数据变化历史。通常&#xff0c;数据仓库都希望从业务上线的第一天开始有数据&#xff0c;然后一直记录到现在。但实时处理技术&#xff0c;又是强调当前处理状态的一门技术&#xff0c;所以当这两个相互对立的方…

聚合数据证件识别接口-基于PHP示例代码

1、开通接口 以下代码示例基于聚合数据提供的证件识别接口&#xff0c;使用前需要先注册申请本接口&#xff0c;获得请求密钥key。 接口文档地址&#xff1a;https://www.juhe.cn/docs/api/id/153 2、功能介绍 通过自动识别的方式获取常规证件文字内容&#xff0c;免去用户…

react函数式组件转化为string---renderToString

需求 使用aggrid的过程中&#xff0c;某个自定义的图标需要传dom的字符串。 但在react的开发中&#xff0c;一般都是组件的概念&#xff08;ReactNode&#xff0c;JSX.Element&#xff09; 因此需要一个方法将dom组件转化为字符串 收获 找到了官网的API——renderToString 作…

虚拟主机部署ssl证书(https)流程

注意事项&#xff1a; 1、域名要做别名解析指向二级域名 2、证书已经申请完成&#xff0c;其他公司的证书要下载导入到西部数码。 虚拟主机部署教程如下&#xff1a; 部署证书 首先要将域名绑定到主机上&#xff0c;在主机控制面板找到【SSL部署】按钮。 在西部数码申请过证…

FFmpeg 媒体文件播放 格式变化流程简述

例如&#xff0c;要播放一个 MP4 文件&#xff0c;这个文件要经过啥呢&#xff1f; 一个 MP4 文件中包含了&#xff1a;视频压缩数据流&#xff08;如H.264、H.265&#xff09;和音频压缩数据&#xff08;如aac、MP3&#xff09; 首先需要经过解码成为原始数据&#xff0c;视频…

数据库原理及应用上机实验一

✨作者&#xff1a;命运之光 ✨专栏&#xff1a;数据库原理及应用上机实验报告整理 目录 ✨一、实验目的和要求 ✨二、实验内容与步骤 &#x1f353;&#x1f353;前言&#xff1a; 数据库原理及应用上机实验报告的一个简单整理后期还会不断完善&#x1f353;&#x1f353;…

HarmonyOS低代码开发-创建新工程方式

使用低代码开发应用或服务有以下两种开发方式&#xff1a; 创建一个支持低代码开发的新工程&#xff0c;开发应用或服务的UI界面。在已有工程中&#xff0c;创建Visual文件来开发应用或服务的UI界面。ArkTS工程和JS工程使用低代码的步骤相同&#xff0c;接下来以JS工程为例讲解…

Web自动化框架中验证码识别处理全攻略,自动化测试神器,让测试更得心应手。

目录 前言&#xff1a; 一. Web自动化测试框架搭建 1. 选择一个自动化测试工具 2. 安装环境 3. 初始化一个Selenium驱动 二. 添加自动化测试用例 1. 编写测试脚本 2. 运行测试脚本 三. 验证码识别与处理 1. 验证码种类 2. 验证码处理方式 四. 结语 Web自动化测试&#…

指针不可怕,请爱它呵护它(狗头)

&#x1f929;本文作者&#xff1a;大家好&#xff0c;我是paper jie&#xff0c;感谢你阅读本文&#xff0c;欢迎一建三连哦。 &#x1f970;内容专栏&#xff1a;这里是《C知识系统分享》专栏&#xff0c;笔者用重金(时间和精力)打造&#xff0c;基础知识一网打尽&#xff0c…

opencv_c++学习(十一)

一、绘制基础图形 绘制直线&#xff1a; line(InputOutputArray img. Point pt1, Point pt2, const Scalar & color, int thickness 1,llineType int LINE_8, int shift 0pt1:直线起始点在图像中的坐标。 pt2:直线终点在图像中的坐标。 color:直线的颜色&#xff0c;…

《2023新一代数字办公白皮书》正式发布!| 爱分析报告

2023年5月12日&#xff0c;受北京市数字办公安全创新联合体委托&#xff0c;由自主可控新鲜事和爱分析联合撰写的《2023新一代数字办公白皮书》&#xff08;以下简称《白皮书》&#xff09;在“2023通明湖论坛 信创基础底座创新发展分论坛”上正式发布。 《白皮书》详细阐述了新…

box的符号距离函数

序 能用解析的方法算的&#xff0c;叫符号距离函数。只能数值解的&#xff0c;叫符号距离场。 它就是横平竖直的几个平面&#xff0c;点到平面的距离是很好算的。 初步认识 有个网页&#xff0c;可以玩一玩&#xff1a; About | Physics Simulation in Visual Computing (…

抖音seo源码开发,开源技术保姆式搭建

抖音seo源码优化逻辑 抖音SEO是通过一系列的技术手段和优化策略来提升视频内容在抖音平台内的曝光率和排名。其中主要包括以下几个方面&#xff1a; 1.关键词优化。通过对视频的标题、描述等元素的关键词进行优化&#xff0c;提高相关性和匹配度&#xff0c;让用户更容易搜索到…

文件权限属性八进制表示法,chmod(文件权限属性更改),chown,chgrp指令,默认/实际起始权限与umask权限掩码关系,文件删除与粘滞位问题等

文件权限属性的八进制表示法 这个其实非常简单&#xff0c;因为文件权限属性也就三种&#xff0c;也就是可读&#xff0c;可写或者可执行。然后对于这三种文件权限属性都是2元式的&#xff0c;要么行&#xff0c;要么不行&#xff0c;因此可以用0和1来表示&#xff0c;0表示没…

SAML协议— 理解SAML2 协议和联合身份验证流程

1、SAML协议概念 安全断言标记语言&#xff0c;简称SAML&#xff0c;是一个基于 XML 的开源标准数据格式&#xff0c;它在当事方之间交换身份验证和授权数据&#xff0c;尤其是在身份提供者和服务提供者之间交换。SAML解决的最主要的需求是基于Web的单点登录&#xff08;SSO&a…

100 个 Go 错误以及如何避免:1~4

协议&#xff1a;CC BY-NC-SA 4.0 译者&#xff1a;飞龙 本文来自【OpenDocCN 饱和式翻译计划】&#xff0c;采用译后编辑&#xff08;MTPE&#xff09;流程来尽可能提升效率。 真相一旦入眼&#xff0c;你就再也无法视而不见。——《黑客帝国》 一、GO&#xff1a;学起来简单&…