1. 自定义属性
<declare-styleable name="QQStepView">
<attr name="outerColor" format="color" />
<attr name="innerColor" format="color" />
<attr name="borderWidth" format="dimension" />
<attr name="stepTextSize" format="dimension" />
<attr name="stepTextColor" format="color" />
</declare-styleable>
2. 使用自定义组件,设置自定义属性
<com.tom.architect02.ui10.day03.QQStepView
android:id="@+id/step_view"
app:outerColor="@color/purple_200"
app:innerColor="@color/teal_200"
app:borderWidth="20dp"
app:stepTextColor="@color/purple_500"
app:stepTextSize="30sp"
android:background="@color/teal_700"
android:layout_width="match_parent"
android:layout_height="match_parent" />
3. 实现QQStepView
package com.tom.architect02.ui10.day03;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;
import com.tom.architect02.R;
// 仿QQ运动计步
public class QQStepView extends View {
private int mOuterColor = Color.RED;
private int mInnerColor = Color.BLUE;
private int mBorderWidth = 20;// 20px
private int mStepTextSize;
private int mStepTextColor;
private Paint mOutPaint, mInnerPaint, mTextPaint;
// 总共的步数,当前的步数
private int mStepMax = 0;
private int mCurrentStep = 0;
public QQStepView(Context context) {
this(context, null);
}
public QQStepView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public QQStepView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 1.分析效果;
// 2.确定自定义属性,编写attrs.xml
// 3.在布局中使用
// 4.在自定义View中获取自定义属性
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.QQStepView);
mOuterColor = array.getColor(R.styleable.QQStepView_outerColor, mOuterColor);
mInnerColor = array.getColor(R.styleable.QQStepView_innerColor, mInnerColor);
mBorderWidth = (int) array.getDimension(R.styleable.QQStepView_borderWidth, mBorderWidth);
mStepTextSize = array.getDimensionPixelSize(R.styleable.QQStepView_stepTextSize, mStepTextSize);
mStepTextColor = array.getColor(R.styleable.QQStepView_stepTextColor, mStepTextColor);
array.recycle();
mOutPaint = new Paint(); // 初始化外圆的画笔
mOutPaint.setAntiAlias(true);
mOutPaint.setStrokeWidth(mBorderWidth);
mOutPaint.setColor(mOuterColor);
mOutPaint.setStrokeCap(Paint.Cap.ROUND);
mOutPaint.setStyle(Paint.Style.STROKE);// 画笔空心
mInnerPaint = new Paint();
mInnerPaint.setAntiAlias(true);
mInnerPaint.setStrokeWidth(mBorderWidth);
mInnerPaint.setColor(mInnerColor);
mInnerPaint.setStrokeCap(Paint.Cap.ROUND);
mInnerPaint.setStyle(Paint.Style.STROKE);// 画笔空心
mTextPaint = new Paint();
mTextPaint.setAntiAlias(true);
mTextPaint.setColor(mStepTextColor);
mTextPaint.setTextSize(mStepTextSize);
// 5.onMeasure()
// 6.画外圆弧 ,内圆弧 ,文字
// 7.其他
}
// 5.onMeasure()
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 调用者在布局文件中可能 wrap_content
// 获取模式 AT_MOST 40dp
// 宽度高度不一致 取最小值,确保是个正方形
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
int size = width > height ? height : width;
setMeasuredDimension(size, size);
}
// 6.画外圆弧 ,内圆弧 ,文字
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 6.1 画外圆弧,确定矩形的区域【坐标系】. 分析:圆弧闭合了 思考:边缘没显示完整. 描边有宽度:mBorderWidth 圆弧
// int center = getWidth()/2; // 中心点
// int radius = getWidth()/2 - mBorderWidth/2; // 半径值
// RectF rectF = new RectF(center-radius,center-radius, center+radius,center+radius);
RectF rectF = new RectF(mBorderWidth / 2, mBorderWidth / 2
, getWidth() - mBorderWidth / 2, getHeight() - mBorderWidth / 2);
// 研究研究,圆弧开始角度,扫过的角度。
canvas.drawArc(rectF, 135, 270, false, mOutPaint);
if (mStepMax == 0) return;
// 6.2 画内圆弧 怎么画肯定不能写死 百分比 是使用者设置的从外面传
float sweepAngle = (float) mCurrentStep / mStepMax;
canvas.drawArc(rectF, 135, sweepAngle * 270, false, mInnerPaint);
// 6.3 画文字
String stepText = mCurrentStep + "";
Rect textBounds = new Rect(); // 给bounds赋值。
mTextPaint.getTextBounds(stepText, 0, stepText.length(), textBounds);
int dx = getWidth() / 2 - textBounds.width() / 2; // 文字的起始x坐标= 控件的一半 - 文字的一半
// 确定基线 baseLineY
Paint.FontMetricsInt fontMetrics = mTextPaint.getFontMetricsInt();
int dy = (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom;
int baseLineY = getHeight() / 2 + dy;
canvas.drawText(stepText, dx, baseLineY, mTextPaint);
}
// 7.其他: 写几个方法动起来 synchronized 保证多线程操作不出问题。
public synchronized void setStepMax(int stepMax) {
this.mStepMax = stepMax;
}
public synchronized void setCurrentStep(int currentStep) {
this.mCurrentStep = currentStep;
invalidate(); // 不断重新绘制:onDraw()
}
}
4. invalidate()源码分析-- 最终调用了onDraw()方法。
invalidate()
--> View.invalidate()
--> invalidate(true)
--> invalidateInternal()
--> skipInvalidate()
--> p.invalidateChild(this, damage);
--> ViewGroup::invalidateChild()
--> parent = parent.invalidateChildInParent(location, dirty);
do-while(parent != null) 不断循环到最外层ViewGroup的 invalidateChildInParent()
最外层由于没有parent,调用
ViewRootImpl::invalidateChildInParent()
ViewRootImpl::checkThread()
--> invalidateRectOnScreen()
--> scheduleTraversals()
--> mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
--> TraversalRunnable:: doTraversal()
--> performTraversals() // UI绘制中最重要的方法:performMeasure() performLayout() performDraw()
--> performDraw()
--> draw()
--> drawSoftware()
--> mView.draw(canvas); // mView是最外面的容器view。递归调用。
invalidate 流程:一路往上跑,跑到最外层父容器 调用draw()方法,--> dispatchDraw() 一路往下画,
最终调用invalidate的view 的onDraw() 方法。
invalidate 刷新,牵连着整个layout布局中的view。
// ViewGroup.java
parent = parent.invalidateChildInParent(location, dirty);
if (view != null) {
// Account for transform on current parent4.
Matrix m = view.getMatrix();
if (!m.isIdentity()) {
RectF boundingRect = attachInfo.mTmpTransformRect;
boundingRect.set(dirty);
m.mapRect(boundingRect);
dirty.set((int) Math.floor(boundingRect.left),
(int) Math.floor(boundingRect.top),
(int) Math.ceil(boundingRect.right),
(int) Math.ceil(boundingRect.bottom));
}
}
} while (parent != null);
}
为什么不能再子线程中更新UI?
开子线程获取数据,更新UI,一般会调用setText,setImageView,会调用 ViewRootImpl::checkThread() 用来检测线程。
1.if (mThread != Thread.currentThread()) {
Thread.currentThread()是子线程【当前线程】。
mThread 在构造函数中初始化的 Thread.currentThread() 主线程mainThread. ViewRootImpl 在哪里初始化的?
只要2个线程不相等,都会抛异常。
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
5.如何防止过度渲染
自己写一些界面会非常复杂 QQ空间/WX朋友圈 列表嵌套列表(Item里面布局可能嵌套布局)
1.网上的解决方案
尽量不要嵌套
能不设置背景不要设置背景
........
2.最好的解决方案(蛋疼)
获取到数据去设置 setText()/setImageView 其实, 最终会调用onInvalidate()
最好是自己画,不要用系统的嵌套布局. 运行效率高,但是实现功能效率低(抉择问题)
网友评论