自定义控件系列:
秒懂OnMeasure
秒懂OnLayout
让自定义ViewGroup里的子控件支持Margin
让自定义ViewGroup支持Padding
自定义ViewGroup的一个综合实践 FlowLayout
onDraw
最简单的自定义View:SwitchView
这次做一个SwitchView,需要使用下面的几个知识点:
知识点:
自定义View不是每次都要重写onMeasure和onLayout,为了避免手写onMeasure和onLayout,不要直接继承ViewGroup,而是继承LinearLayout、FrameLayout、RelativeLayout这种已经重写过了onMeasure和onLayout的布局
-
如何获取控件的宽高
我们知道经常因为控件还没加载出来就去获取宽高,那么得到的宽高为0,使用view.post()是获取宽高最简单的方式之一(为什么呢,看了源码后,巴拉巴拉…),当然还有其他很多方法。
post(new Runnable() { @Override public void run() { mWidth = getMeasuredWidth(); mHeight = getMeasuredHeight(); } });
-
如何动态地添加一个子控件
我们经常需要通过代码来添加子控件,而不是写到xml里,当然是addView了
addView(View child)
把子控件添加到末尾
void addView(View child, int index)
把子控件添加到指定位置
addView(View child, int width, int height)
把子控件添加到末尾,并为子控件指定宽高
addView(View child, LayoutParams params)
把子控件添加到末尾,子控件的宽高、margin、gravity等等所有xml里以“layout_xxx"开头的属性全部封装在LayoutParams里
addView(View child, int index, LayoutParams params)
把子控件添加到指定位置,子控件的宽高、margin、gravity等等所有xml里以“layout_xxx"开头的属性全部封装在LayoutParams里
上面的方法都是间接或直接调用了这个方法可见LayoutParams也很重要
- 不同的ViewGroup都有自己专属的LayoutParams,对应xml里“layout_xxx"的所有属性,我们如果想使用这些属性,必须通过设置子控件的LayoutParams来实现
下图是LinearLayout的Layout_开头的属性
下图是RelativeLayut以Layout_开头的属性
举个例子:在RelativeLayout里动态添加子控件
rl = (RelativeLayout) findViewById(R.id.rl); TextView textView = new TextView(this); textView.setText("我是一个textview"); textView.setBackgroundColor(Color.RED); textView.setId(R.id.textview);//动态设置id,见https://www.cnblogs.com/codingblock/p/5090441.html RelativeLayout.LayoutParams lp1 = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); lp1.leftMargin = 10; lp1.setMargins(100, 10, 10, 10); lp1.addRule(RelativeLayout.CENTER_IN_PARENT); textView.setLayoutParams(lp1); rl.addView(textView); ImageView imageView = new ImageView(this); imageView.setImageResource(R.mipmap.ic_launcher); RelativeLayout.LayoutParams lp2 = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); //★注意:这里new的是RelativeLayout.LayoutParams,因为你在为RelativeLayout添加子控件 lp2.addRule(RelativeLayout.BELOW, R.id.textview); rl.addView(imageView, lp2);
- 不同的ViewGroup都有自己专属的LayoutParams,对应xml里“layout_xxx"的所有属性,我们如果想使用这些属性,必须通过设置子控件的LayoutParams来实现
-
重写onTouchEvent,返回true,表示当前的view要处理事件(事件分发会详细说这个方法)
-
如何让view滑动起来
- 通过改变一个view的LayoutParams属性值(这里使用了margin),来实现改变view的位置
- 还可以通过scrollBy来实现,下篇博客举这个例子
-
如何实现弹性滑动,即松手后view自己平滑地移动
- 属性动画逐渐地改变一个属性,达到平滑移动效果
- scroller可以实现平滑移动
应用
效果
一个开关,滑块随着手指滑动,松手后,判断滑动位置是不是过半,过半的话平滑移动到最右侧,没过半平滑移动到最左侧
代码
package com.view.custom.dosometest.view;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Color;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.ImageView;
import android.widget.LinearLayout;
/**
* 描述当前版本功能
*
* @Project: DoSomeTest
* @author: cjx
* @date: 2019-12-01 10:06 星期日
*/
public class SwitchView extends LinearLayout {
private ImageView mImageView;
private LayoutParams mLayoutParams;
private int mSliderHeight;
private int mSliderWidth;
private int mWidth;
private int mHeight;
public SwitchView(Context context) {
super(context);
init(context);
}
public SwitchView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public SwitchView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
setBackgroundColor(Color.GRAY);
post(new Runnable() {
@Override
public void run() {
addSlider();//因为这里需要用到控件的宽高,所以写到post里
}
});
}
private void addSlider() {
mWidth = getMeasuredWidth();
mHeight = getMeasuredHeight();
mSliderWidth = mWidth / 2;
mSliderHeight = mHeight;
// 注意导包LayoutParams是LinearLayout.LayoutParams
mLayoutParams = new LayoutParams(mSliderWidth, mSliderHeight);
mImageView = new ImageView(getContext());
mImageView.setBackgroundColor(Color.CYAN);
mImageView.setLayoutParams(mLayoutParams);
addView(mImageView);
}
float mLastX;
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastX = x;
break;
case MotionEvent.ACTION_MOVE:
float deltaX = x - mLastX;
// 防止滑块滑出边界,矫正deltaX
if (mLayoutParams.leftMargin + mSliderWidth + deltaX > mWidth) {
deltaX = mWidth - mLayoutParams.leftMargin - mSliderWidth;
} else if (mLayoutParams.leftMargin + deltaX < 0) {
deltaX = -mLayoutParams.leftMargin;
}
// 动态改变这个控件的左边距,实现控件的滑动
mLayoutParams.leftMargin += (int) deltaX;
//别忘了通过setLayoutParams,使你的改动生效
mImageView.setLayoutParams(mLayoutParams);
mLastX = x;
break;
case MotionEvent.ACTION_UP:
// 松手后,通过属性动画改变margin实现控件的平滑移动
if (mLayoutParams.leftMargin > mSliderWidth / 2) {
smoothChangeLeftMargin(mLayoutParams.leftMargin, mSliderWidth);
} else {
smoothChangeLeftMargin(mLayoutParams.leftMargin, 0);
}
break;
}
return true;
}
/**
* 使用属性动画平滑地过度leftMargin
* @param start
* @param end
*/
private void smoothChangeLeftMargin(int start, int end) {
ValueAnimator valueAnimator = ValueAnimator.ofInt(start, end);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mLayoutParams.leftMargin = (int) animation.getAnimatedValue();
mImageView.setLayoutParams(mLayoutParams);
}
});
valueAnimator.setDuration(300);
valueAnimator.start();
}
}