本节首先讲讲自定义控件套路,自定义属性,测量过程的问题,为了后面的几节能专注于实现特效,而不受本文这些惯用套路的影响
特效系列的目录
要实现界面特效,首先得掌握:
安卓特效的实现,需要借助以下四个技术点:
声明和使用自定义属性:
定义attr:在values目录下,attrs.xml
<declare-styleable name="TopBar">
<attr name="title" format="string" />
<attr name="titleTextSize" format="dimension" />
<attr name="titleTextColor" format="color" />
<attr name="titleBg" format="reference|color" />
declare-styleable>
使用attr:在任意布局文件里
<xx.xx.xx.TopBar
xmlns:custom="http://schemas.android.com/apk/res-auto"
android:id="@+id/topbar"
custom:title="title"
custom:titleTextSize="15sp"
custom:titleTextColor="#aaaaaa"
custom:titleBg="@drawable/ic_launcher"
/>
处理自定义属性
取出xml中设置的属性
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TopBar);
mTitle = ta.getString(R.styleable.TopBar_title);
mTitleTextSize = ta.getDimension(R.styleable.TopBar_titleTextSize, 10);
mTitleTextColor = ta.getColor(R.styleable.TopBar_titleTextColor, 0);
mTitleBg = ta.getDrawable(R.styleable.TopBar_titleBg);
ta.recycle();
需要知道的
如何确定测量模式:
View默认情况下,测量过程如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
//取minWidth和背景drawable的getMinimumWidth的最大值
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size; //minWidth属性或者背景宽度:这个就可以认为是空白View的内容
break;
case MeasureSpec.AT_MOST: //wrap_content,此时specSize是父控件给留的最大值
case MeasureSpec.EXACTLY: //match_parent或者具体值
result = specSize;
break;
}
return result;
}
可以看出,唯一没处理的就是wrap_content的情况
一般具体带内容的View,按下面的套路测量
* 思路也很简单
* EXACTLY:就指定我多大,那我就多大,一般这时子控件就是match_parent,或者具体数值
* AT_MOST:specSize是我的最大值,我本身也带个内容宽高,二者比较,取小的就是了,我尽量给父控件省地方
* 除非父控件放不下我了,那我就得按父控件尺寸来
* 至于calculateContentWidth和calculateContentHeight,可能会被padding,背景,内容等因素影响
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(
mearureWidth(widthMeasureSpec),
mearureHeight(heightMeasureSpec));
}
private int mearureWidth(int measureSpec){
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if(specMode == MeasureSpec.EXACTLY){
result = specSize;
}else{
result = calculateContentWidth();
if(specMode == MeasureSpec.AT_MOST){
result = Math.min(result, specSize);
}
}
return result;
}
private int mearureHeight(int measureSpec){
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if(specMode == MeasureSpec.EXACTLY){
result = specSize;
}else{
result = calculateContentHeight();
if(specMode == MeasureSpec.AT_MOST){
result = Math.min(result, specSize);
}
}
return result;
}
///你自己测量宽度:宽度wrap_content时,会走这
private int calculateContentWidth(){
return 200;
}
///你自己测量高度:宽度wrap_content时,会走这
private int calculateContentHeight(){
return 200;
}
自己处理wrap_content时应注意的问题
padding影响还挺大
模板参考:
http://www.jianshu.com/p/71e9cc942c97
http://blog.csdn.net/lmj623565791/article/details/38339817
其实ViewGroup的match_parent和固定值也好说,就是EXACTLY,主要还是wrap_content的问题
ViewGroup的onMeasure直接继承自View,所以没有实现对子控件的处理,
但是测量子控件的方法已经提供了,下面给出一个ViewGroup测量的模板
在给出模板之前,需要说明的是,关于这一节的模板和下一节的原理,不要太纠结,
一般情况下,你通过RelativeLayout或者FrameLayout的margin就可以实现任何形式的布局,
测量过程本身是很复杂的,如果不够复杂,可能你考虑的情况不够,参考LinearLayout的测量过程的代码,你就能知道为什么自己一般不要过多的干扰布局过程了
ViewGroup处理子控件的measure先不说
主要是ViewGroup在自己wrap_content时,宽高是随着子控件来的,并且和具体的布局方式还有关系,所以测量过程中计算ViewGroup自己本身宽高时,可能需要把布局算法先过一遍
模板1:不考虑子控件margin,也不考虑ViewGroup本身的wrap_content
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
final int count = getChildCount();
if (count > 0) {
measureChildren(widthMeasureSpec, heightMeasureSpec);
}
}
模板2:考虑margin,和考虑wrap_content
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int count = getChildCount();
// 临时ViewGroup大小值
int viewGroupWidth = 0;
int viewGroupHeight = 0;
if (count > 0) {
// 遍历childView
for (int i = 0; i < count; i++) {
// childView
View child = getChildAt(i);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
//测量childView包含外边距
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
// 计算父容器的期望值,下面代码我们注掉,因为这里的代码根据具体布局来
//viewGroupWidth += child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
//viewGroupHeight += child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
}
//根据子控件计算ViewGroup宽高,取决于具体布局
viewGroupWidth = calculateParentWidthBasedOnChilren();
viewGroupHeight = calculateParentHeightBasedOnChilren();
// ViewGroup内边距
viewGroupWidth += getPaddingLeft() + getPaddingRight();
viewGroupHeight += getPaddingTop() + getPaddingBottom();
//和建议最小值进行比较
viewGroupWidth = Math.max(viewGroupWidth, getSuggestedMinimumWidth());
viewGroupHeight = Math.max(viewGroupHeight, getSuggestedMinimumHeight());
}
setMeasuredDimension(resolveSize(viewGroupWidth, widthMeasureSpec), resolveSize(viewGroupHeight, heightMeasureSpec));
}
///你自己来实现,根据具体布局和子控件们的信息,算出ViewGroup的wrap_content宽度
private int calculateParentWidthBasedOnChilren(){
int viewGroupWidth = 0;
final int count = getChildCount();
for (int i = 0; i < count; i++){
View child = getChildAt(i);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
//lp.leftMargin
//lp.rightMargin
//child.getMeausredWidth()
...看你怎么算了
}
return viewGroupWidth;
}
///你自己来实现,根据具体布局和子控件们的信息,算出ViewGroup的wrap_content高度
private int calculateParentHeightBasedOnChilren(){
int viewGroupHeight = 0;
final int count = getChildCount();
for (int i = 0; i < count; i++){
View child = getChildAt(i);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
//lp.topMargin
//lp.bottomMargin
//child.getMeausredHeight()
...看你怎么算了
}
return viewGroupHeight;
}
以下代码是ViewGroup本身提供的
///遍历测量所有子控件
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
//源码中是measureChild,注意还有个measureChildWithMargins
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
public static int resolveSize(int size, int measureSpec) {
return resolveSizeAndState(size, measureSpec, 0) & MEASURED_SIZE_MASK;
}
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
case MeasureSpec.AT_MOST:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
参考开发艺术
再看ViewGroup的测量过程
思路:
注意:
对于顶级View,即DecoreView,measure过程和普通View有点不同
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
int childWidthMeasureSpec;
int childHeightMeasureSpec;
boolean windowSizeMayChange = false;
if (DEBUG_ORIENTATION || DEBUG_LAYOUT) Log.v(TAG,
"Measuring " + host + " in display " + desiredWindowWidth
+ "x" + desiredWindowHeight + "...");
boolean goodMeasure = false;
if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
// On large screens, we don't want to allow dialogs to just
// stretch to fill the entire width of the screen to display
// one line of text. First try doing the layout at a smaller
// size to see if it will fit.
final DisplayMetrics packageMetrics = res.getDisplayMetrics();
res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true);
int baseSize = 0;
if (mTmpValue.type == TypedValue.TYPE_DIMENSION) {
baseSize = (int)mTmpValue.getDimension(packageMetrics);
}
if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": baseSize=" + baseSize);
if (baseSize != 0 && desiredWindowWidth > baseSize) {
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": measured ("
+ host.getMeasuredWidth() + "," + host.getMeasuredHeight() + ")");
if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
goodMeasure = true;
} else {
// Didn't fit in that size... try expanding a bit.
baseSize = (baseSize+desiredWindowWidth)/2;
if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": next baseSize="
+ baseSize);
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": measured ("
+ host.getMeasuredWidth() + "," + host.getMeasuredHeight() + ")");
if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
if (DEBUG_DIALOG) Log.v(TAG, "Good!");
goodMeasure = true;
}
}
}
}
if (!goodMeasure) {
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
windowSizeMayChange = true;
}
}
if (DBG) {
System.out.println("======================================");
System.out.println("performTraversals -- after measure");
host.debug();
}
return windowSizeMayChange;
}
看这段
if (!goodMeasure) {
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
windowSizeMayChange = true;
}
}
desire就是屏幕宽高
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
上面产生的就是DecoreView的measureSpec
注意一个问题,这个也曾经出现在阿里面试题里:
父是AT_MOST时,高度其实是根据子View来,
但如果此时子是match_parent,所以子也只能是AT_MOST了
Child wants to be our size, but our size is not fixed. Constrain child to not be bigger than us.
实战:BlockLayout
现在需要一个BlockLayout,其子控件可以是任何控件,但不论其宽度指定成什么,
每一行都必须只能放3个子控件,而高度不论指定成什么,最后显示出来的都是个正方形
并且,每一行的中间那个控件,必须距左右各10dp的margin
行与行之间,也是10dp的margin
这估计是个最简单的Layout了
CubeSdk里的BlockLayout是继承RelativeLayout,实现比较简单比较巧妙,
利用了Relativelayout子控件的margin来控制,规避了自己measuue和layout,
我们应该学习
package in.srain.cube.views.block;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RelativeLayout;
public class BlockListView extends RelativeLayout {
public interface OnItemClickListener {
void onItemClick(View v, int position);
}
private static final int INDEX_TAG = 0x04 << 24;
private BlockListAdapter> mBlockListAdapter;
private LayoutInflater mLayoutInflater;
private OnItemClickListener mOnItemClickListener;
public BlockListView(Context context) {
this(context, null, 0);
}
public BlockListView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public BlockListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mLayoutInflater = LayoutInflater.from(context);
}
public void setAdapter(BlockListAdapter> adapter) {
if (adapter == null) {
throw new IllegalArgumentException("adapter should not be null");
}
mBlockListAdapter = adapter;
adapter.registerView(this);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (null != mBlockListAdapter) {
mBlockListAdapter.registerView(null);
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (null != mBlockListAdapter) {
mBlockListAdapter.registerView(this);
}
}
public void setOnItemClickListener(OnItemClickListener listener) {
mOnItemClickListener = listener;
}
OnClickListener mOnClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
int index = (Integer) v.getTag(INDEX_TAG);
if (null != mOnItemClickListener) {
mOnItemClickListener.onItemClick(v, index);
}
}
};
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
public void onDataListChange() {
removeAllViews();
int len = mBlockListAdapter.getCount();
int w = mBlockListAdapter.getBlockWidth();
int h = mBlockListAdapter.getBlockHeight();
int columnNum = mBlockListAdapter.getCloumnNum();
int horizontalSpacing = mBlockListAdapter.getHorizontalSpacing();
int verticalSpacing = mBlockListAdapter.getVerticalSpacing();
boolean blockDescendant = getDescendantFocusability() == ViewGroup.FOCUS_BLOCK_DESCENDANTS;
for (int i = 0; i < len; i++) {
RelativeLayout.LayoutParams lyp = new RelativeLayout.LayoutParams(w, h);
int row = i / columnNum;
int clo = i % columnNum;
int left = 0;
int top = 0;
if (clo > 0) {
left = (horizontalSpacing + w) * clo;
}
if (row > 0) {
top = (verticalSpacing + h) * row;
}
lyp.setMargins(left, top, 0, 0);
View view = mBlockListAdapter.getView(mLayoutInflater, i);
if (!blockDescendant) {
view.setOnClickListener(mOnClickListener);
}
view.setTag(INDEX_TAG, i);
addView(view, lyp);
}
requestLayout();
}
}
套路:
几个重要的回调和注意点
其他:
在这里先给出入门的例子,为后面几节做准备
效果就是画个圆,不考虑wrap_content和padding
public class CircleView extends View{
public CircleView(Context context) {
super(context);
init();
}
public CircleView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public CircleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
private int mColor = Color.RED;
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private void init(){
mPaint.setColor(mColor);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
int radius = Math.min(width, height) / 2;
canvas.drawCircle(width/2, height/2, radius, mPaint);
}
}
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
>
<org.ayo.ui.sample.view_learn.CircleView
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#000000" />
FrameLayout>
添加padding支持
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
final int paddingLeft = getPaddingLeft();
final int paddingRight = getPaddingRight();
final int paddingTop = getPaddingTop();
final int paddingBottom = getPaddingBottom();
int width = getWidth() - paddingLeft - paddingRight;
int height = getHeight() - paddingTop - paddingBottom;
int radius = Math.min(width, height) / 2;
canvas.drawCircle(paddingLeft + width/2, paddingTop + height/2, radius, mPaint);
}
<org.ayo.ui.sample.view_learn.CircleView
android:layout_width="wrap_content"
android:layout_height="100dp"
android:layout_margin="20dp"
android:padding="20dp"
android:background="#000000" />
添加wrap_content支持
//===========================================
//为了让控件支持wrap_content时,内容尺寸取200px,需要我们重写measure过程
//===========================================
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(
mearureWidth(widthMeasureSpec),
mearureHeight(heightMeasureSpec));
}
private int mearureWidth(int measureSpec){
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if(specMode == MeasureSpec.EXACTLY){
result = specSize;
}else{
result = calculateContentWidth();
if(specMode == MeasureSpec.AT_MOST){
result = Math.min(result, specSize);
}
}
return result;
}
private int mearureHeight(int measureSpec){
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if(specMode == MeasureSpec.EXACTLY){
result = specSize;
}else{
result = calculateContentHeight();
if(specMode == MeasureSpec.AT_MOST){
result = Math.min(result, specSize);
}
}
return result;
}
private int calculateContentWidth(){
return 200;
}
private int calculateContentHeight(){
return 200;
}
<org.ayo.ui.sample.view_learn.CircleView2
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:padding="20dp"
android:background="#000000" />
其实上面这段java代码,可以简化一下,参考开发艺术
///不考虑contentSize和specSize的大小关系,不考虑minWidth和minHeight
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(200, 200);
}else if(widthSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(200, heightSize);
}else if(heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSize, 200);
}
}