感谢巨人的肩膀-------coder任玉刚+Tomcat的猫
(一)继承View重写onDraw方法
这种方法主要用于实现一些不规则的效果,一般需要重写onDraw方法,采用这种方式需要自己支持wrap_content,并且padding也需要自己处理。
先写一个标准的菜鸟级别的自定义View:
CircleView.java
public class CircleView extends View {
private int mColor = Color.GREEN;
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
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();
}
private void init() {
mPaint.setColor(mColor);
}
@Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
int radius = Math.min(width, height)/2;
canvas.drawCircle(width / 2, height / 2, radius, mPaint);
}
}
代码逻辑非常简单,就不在赘述了;
activity_main.xml :
<com.example.coustomview.CircleView
android:id="@+id/my_circleView"
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#000000"/>
运行结果像这样:
然后,向activity_main.xml中加入margin边距,像下面这样:
<com.example.coustomview.CircleView
android:id="@+id/my_circleView"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_margin="20dp"
android:background="#000000"/>
运行结果:
可以看到margin已经生效,为什么呢,因为magin是由父控件来控制的,不懂的搜下,很简单,咱们继续,向xml中加入padding后,如下:
<com.example.coustomview.CircleView
android:id="@+id/my_circleView"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_margin="20dp"
android:padding="20dp"
android:background="#000000"/>
运行结果:
padding表示内边距,即圆形与外围的矩形间应该有20dp的距离,现在你没有看错,它居然还是内切圆!说明我们的padding属性已经失效了~23333 , 可是这是为什么呢?这就要从我们的宽高说起了,应为我们定义的属性一个是match_parent ,一个是100dp ,so执行的是精确测量模式,默认是不对padding做处理的,so可怜的padding君就被废弃了,至于如何自定义View?方法及其步骤,自己百度下就ok了,这里就不在赘述了,咱们废话不多说,那么如何解决呢?既然,OnMeasure( )这个渣渣不给咱做处理,那咱就自己弄吧~走起!
试着把android:layout_width="match_parent"
的属性值改为android:layout_width="wrap_content"
你会发现有卵用,运行结果并没有任何改变,原因是在自定义的View(特指直接继承自View的类)中,你如果不对wrap_content做特殊处理,它就跟match_parent没什么区别了,so效果就是一样的,至于原因自己百度下,这里就不在赘述了,那么怎么让padding生效呢?很简单只需要在onDraw()中做处理就ok了,就像这样:
@Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
//对padding做特殊处理
final int paddingLeft = getPaddingLeft();
final int paddingRight = getPaddingRight();
final int paddingTop = getPaddingTop();
final int PaddingButtom = getPaddingBottom();
int width = getWidth() - paddingLeft - paddingRight;
int height = getHeight() - paddingTop - PaddingButtom;
int radius = Math.min(width, height)/2;
canvas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2, radius, mPaint);
}
运行结果:
响应的圆心跟半径也做了响应的处理,不懂的画画图,带入几个数字,画个数轴,很简单;
很多时候,我们希望加入自己的自定义属性,可以这样,在values目录下新建attrs.xml(名字而已随意),当然为了规范最好是attrs_XXX这样就很明显了,对吧~~attrs.xml如下:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircleView">
<attr name="circle_color" format="color" />
</declare-styleable>
</resources>
然后我们加入如下代码:
public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//加载自定义属性集合CircleView
TypedArray taArray = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
//解析CircleView属性结合中的属性circle_text 跟 text_size;
mColor = taArray.getColor(R.styleable.CircleView_circle_color, Color.GREEN);
taArray.recycle();
init();
}
就可以看到,我们自定的属性生效了,哇哈哈~完整代码如下:
package com.example.coustomview;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
/**
* 继承View重写onDraw方法
* 这种方法主要用于实现一些不规则的效果,一般需要重写onDraw方法,
* 采用这种方式需要自己支持wrap_content,并且padding也需要
* 自己处理。
*
* @author Eillot
*
*/
public class CircleView extends View {
private int mColor = Color.GREEN;
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); ;
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);
//加载自定义属性集合CircleView
TypedArray taArray = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
//解析CircleView属性结合中的属性circle_text 跟 text_size;
mColor = taArray.getColor(R.styleable.CircleView_circle_color, Color.GREEN);
taArray.recycle();
init();
}
private void init() {
mPaint.setColor(mColor);
}
/**
* 在自定义view中需要支持宽高属性为wrap_content的情况,则需要重写View测量方法OnMeasure();
*
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpaceMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpaceMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpaceSize = MeasureSpec.getSize(heightSpaceMode);
if (widthSpaceMode == MeasureSpec.AT_MOST && heightSpaceMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, 200);
}else if (widthSpaceMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, heightSpaceSize);
}else if ( heightSpaceMode == MeasureSpec.AT_MOST)
{
setMeasuredDimension(widthSpaceSize, 200);
}
}
@Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
//对padding做特殊处理
final int paddingLeft = getPaddingLeft();
final int paddingRight = getPaddingRight();
final int paddingTop = getPaddingTop();
final int PaddingButtom = getPaddingBottom();
int width = getWidth() - paddingLeft - paddingRight;
int height = getHeight() - paddingTop - PaddingButtom;
int radius = Math.min(width, height)/2;
canvas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2, radius, mPaint);
}
}
activity_main.xml中的代码如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:coustom="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:background="#ffffff"
android:orientation="vertical"
tools:context="com.example.coustomview.MainActivity" >
<eliot.wakfo.com.coustomview2.CircleView
android:id="@+id/my_circleView"
android:layout_width="wrap_content"
android:layout_height="100dp"
android:layout_margin="20dp"
coustom:circle_color="@color/circle_color"
android:padding="20dp"
android:background="#000000"/>
</LinearLayout>
运行结果如下:
简直就是DXX了,高级新技能已经get !继续~~
(二)继承ViewGroup派生特殊的Layout
这是真真实实的造”轮子“呀!需要你自己写view的OnMeasure()跟OnLayout()过程的逻辑,如果想写一个listView+Scrollview的变异Layout,那你还要处理滑动冲突的问题,我原来一直不明白,为什么我一定要懂事件分发机制(内部是一个树形结构),它有什么用?我现在知道了,那就是几乎所有的自定义相关的View或ViewGroup它在写处理的逻辑的时候的基础就是事件分发,比如:自定义Draw(),它内部有一个dispatchDraw()方法,一看名字是不是非常熟悉,哈哈~没错,跟Event的分发很类似,然后,我又想到——-为什么我要学习《离散数学》了?如果当时老师告诉我学它是干嘛用的,我一定好好学习,学渣表示已经还给老师了~好了,我们废话不多说,走起!
我们先来看自定义ViewGroup的OnMeasure()方法(为什么先从它开始,因为这个渣渣很容易出错,而且很关键);
思路:
以前我们自定义View的时候,我只需要考虑它自己的测量就可以了,现在ViewGroup中放了很多个View ,你说怎么测量?当然是把它(即遍历)出来,然后测出宽高,然后求和,就是我要画的总宽高了呀!当然,这里有个假设必须成立—–那就是子View的宽高均相等;结合具体情况具体使用呀~比如:我要定义一个类似水平的LinearLayout ,这时候我的高就等于第一个子View的高,而宽则等于所以子View的总和,why ?因为你看手机图片的时候是不是左右滑动,而不是上下滑动,对否~上代码:
创建CoustomViewGroup.java
/**
* view测量原理:
* 主要是MeasureSpace代表一个int 32位的值,高俩位分别为spaceMode(即测量模式),spaceSize(即测量大小)
* 那3中测试模式这里就赘述了,自己搜一下,主要针对说下宽高属性为wrap_content的情况,加入任一属性为wrap_content时,
* 高(宽)需要在onMeasure()方法中做特殊处理,不复杂,就是给一个默认值比如:200dp ,我在网上看到很多人都喜欢使用这个数字,
* 不知道为什么?若二者都为wrap_content ,简单那就在OnMeasure()中都做处理给个默认值呗,就这么简单!
*
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//定义俩个保存子Viw的测量宽跟高的变量measureWidth和measureHeight
int measureChildWidth = 0;
int measureChildheight = 0;
//获取子view的个数
final int childCount = getChildCount();
//测量ziView的宽高;
measure(widthMeasureSpec , heightMeasureSpec);
//下来就是套路了,确定测量的模式跟大小
int widthSpaceMode = MeasureSpec.getMode(widthMeasureSpec);
int widSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpaceMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
//下来就是写自己的逻辑判断了,这里必须感谢下大牛----coder任玉刚 ,解决了我许久的困惑,
// 比如:为什么自定义view时宽高设置为warp_content时,不重写OnMeasure()方法,View的效果等同于math_parent?
//先来判断下有没有子元素,没有就不用测了直接置0
if (childCount == 0){
setMeasuredDimension(0,0);
}else if ( (widthSpaceMode == MeasureSpec.AT_MOST) && (heightSpaceMode == MeasureSpec.AT_MOST) ){
//还记得我们自定义View的时候的处理规范吗------setMeasuredDimension(200 , 200);
//获取第一个子View的对象
final View childView = getChildAt(0);
measureChildWidth = childView.getMeasuredWidth() * childCount;
measureChildheight = childView.getMeasuredHeight() * childCount;
setMeasuredDimension(measureChildWidth , measureChildheight);
}else if ( widthSpaceMode == MeasureSpec.AT_MOST ){
//当宽属性为wrap_content时,需要所有子View的宽之和(记得我们是水平的呀)
final View childView = getChildAt(0);
measureChildWidth = childView.getMeasuredWidth() * childCount;
setMeasuredDimension(measureChildWidth , heightSpaceSize);
}else if ( heightMeasureSpec == MeasureSpec.AT_MOST ){
//注意:当高属性为wrap_content时,仅仅需要任一子View的高即可(记得我们是左右滑动的,高是不变得)
final View childView = getChildAt(0);
measureChildheight = childView.getMeasuredHeight() ;
setMeasuredDimension(widSpaceSize , measureChildheight);
}
}
代码中的注释已经很完整了,这里就不在解释代码了~~我们继续,
NPC “任教主”的温馨提示:
上面的OnMeasure()方法有俩点不规范:
NO .1 当没有子元素的时候,不应该直接把宽高置为0 ,而应该根据LayoutParams中的宽高来做相应的处理;
No.2 在测量CoustomViewGroup的时候没有考虑它的padding 跟子View的margin会影响到CoustomViewGroup的宽高,why ?因为不管是自己的padding或者是子View的margin占用的都是CoustomViewGroup的空间;
小伙伴们,可以自己实现下,我们主要学习流程就不走细节了,咱们继续,来看看自定义ViewGroup的OnLayout()的过程,走起!
/**
* onLayout()过程主要用于确定view在ViewGroup中的摆放位置,通过确定View的L , T,R ,B四个坐标点;
* @param b
* @param i
* @param i1
* @param i2
* @param i3
*/
@Override
protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
int childLeft = 0;
final int childCount = getChildCount();
//mChildrenSize = childCount;// 这里没搞懂为什么要把childCount赋值给mChildrenSize ?
//接下来就是你熟悉的套路了,遍历每个子View并获取它们的位置, 从左向右
for ( int n = 0 ; n < childCount ; n++){
final View childView = getChildAt(n);
//View可见
if ( (childView.getVisibility()) != View.GONE){
final int childWidth = childView.getMeasuredWidth();
mChildWidth = childWidth;
childView.layout(childLeft , 0 , childLeft + childWidth , childView.getMeasuredHeight());
childLeft += childWidth;
}
}
}
同样任教主温馨提示时间到:
No.2 在测量CoustomViewGroup的时候没有考虑它的padding 跟子View的margin会影响到CoustomViewGroup的宽高,why ?因为不管是自己的padding或者是子View的margin占用的都是CoustomViewGroup的空间;
至于怎么处理让它变得规范,即具有处理padding及margin 的能力,就看法宝 吧~下面咱们继续分析:
我们都知道有View的监听方法有一个叫OnTechEvent()的,它里面有3个Action 分别为Action_Down , Action_Move ,ACtion_Up ,那么,我们从左向右滑动图片的时候,必然会涉及到这3个“怪”,我们要做的就是加入自己的逻辑判断规则,来劝化他们,怎么劝化呢?
我们可以这样:
1)手指落下后会触发—-Down , 滑动会触发—-Move ,手指收起会触发—-Up ;
2) 1怪跟3怪不足为虑,主要是2怪,它会产生一个滑动事件,我想让它把这个事件交给onTechEvent()的来处理,好那就重写onTechEvent()然而并没有什么卵用,这就涉及到一个事件分发的问题,至于事件分发的机制,网上多如牛毛,自己搜下吧~我们切入正题,事件分发机制遵循—–谁拦截谁负责到底的原则,当然,前提是onTechEvent()返回True (表示这个事件我来处理了) ,onInterceptTouchEvent()返回Treue(表示事件被截断,不在传递); 那么方法就是重写ViewGroup的onInterceptTouchEvent()方法,加入自己的逻辑判断;就下这样:
/**
* 为什么要重写ViewGroup的事件分发呢?
* 若不重写onInterceptTouchEvent(),你会发现即使你的OnTechEvent()方法返回的是True(即应该处理事件),
* 然而,它却并没有处理事件,why ? 因为事件已经被拦截了!所以,要加入自己的逻辑判断,让onInterceptTouchEvent()
* 知道什么时候进行拦截,什么时候不进行拦截~~
*
* @param event
* @return
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN: {
intercepted = false;
//手指落下时若View平滑滚动还未完成,则打断动画,并对Down事件进行拦截
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
intercepted = true;
}
break;
}
case MotionEvent.ACTION_MOVE:{
intercepted = false;
int deltaX = x - mLastInterceptX;//View的滑动后距离的X坐标
int deltaY = y - mLastInterceptY;//View的滑动后距离的Y坐标
//用于判断是否正在由左向右进行滑动,若是则intercepted = true对事件进行拦截;
if ( (Math.abs( deltaX)) > (Math.abs(deltaY)) ){
intercepted = true;
}else {
intercepted = false;
}
break;
}
case MotionEvent.ACTION_UP:{
intercepted = false;
break;
}
default:
break;
}
Log.d( TAG , "intercepted= " + intercepted);
mLastX = x;
mLastY = y;
mLastInterceptX = x;
mLastInterceptY = y;
//不在使用supper来继续使用父类的拦截方法
return intercepted;
}
好了,已经成功劝化这3怪,接下来通过使用OnTechEvent()方法来让他们记住我的指令,比如:我发出ACTION_UP的指令,它马上知道,我的手指离开屏幕了,不需要对事件进行处理了,好了,重写OnTechEvent( )方法如下:
/**
* 重写onTouchEvent
* @param event
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
mVelocityTracker.addMovement(event);//表示追踪当前点击事件的速度;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
scrollBy(-deltaX, 0);
break;
}
case MotionEvent.ACTION_UP:{
int scrollX = getScrollX();
/**
* 表示计算速度,比如:时间间隔为1000 ms ,在1秒内,
* 手指在水平方向从左向右滑过100像素,那么水平速度就是100;
* 计算速度+获取速度----三步曲
* mVelocityTracker.computeCurrentVelocity(1000);
float xVelocity = mVelocityTracker.getXVelocity(); //获取水平方向的滑动速度
* float yVelocity = mVelocityTracker.getYVelocity();//获取垂直方向的滑动速度
* 由于我们需要的是xVelocity,
* 这里只是提一下,不计入代码;
* 注意:这里的速度指的是一段时间内手指所滑过的像素数!像素数!像素数!重要事说3遍;
*/
mVelocityTracker.computeCurrentVelocity(1000);
float xVelocity = mVelocityTracker.getXVelocity(); //获取水平方向的滑动速度
/**
*当你滑动手机相册中的照片的时候有没有发现,必须滑动到一定距离它才会切到下张图片,
* 否则,它就回退回原来的照片了,原来,它是通过“速度”来进行控制的~
* 还有就是"速度“可以为负值,很好理解,就像我们规定车前进的方向为正,反向为负;
*
*/
if (Math.abs(xVelocity) >= 50) {
mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
} else {
mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;
}
mChildIndex = Math.max(0, Math.min(mChildIndex, mChildSize - 1));
int dx = mChildIndex * mChildIndex - scrollX;//缓慢地滑动到目标的x坐标;
smoothScrollBy(dx, 0);
mVelocityTracker.clear();//对速度跟踪进行回收
break;
}
default: {
break;
}
}
mLastX = x ;
mLastY = y ;
return true;
}
代码中的关键部分我已经给出了注释,这里就不做代码解析了~~
好了,至此我们就完成了“造轮子”的90%的工作,剩下的就是一些优化跟善后工作了,使用Scroller使用我们的滑动看起来更加平滑,然后把速度跟踪,在View的绘制结束后,在onDetachedFromWindow中进行回收,可能大家不知道Scroller的作用,这里简单提一下:
它的本质就是一个View不断重新绘制的过程,直到没有View需要绘制为止,很好理解,就好比是,把一个人的每一个连续动作画下来后,然后叠起来,快速翻页,是不是好像那个人“动”起来了,哇哈哈~~更多详细内容自己搜下,网上教程很多,好了,完整代码如下:
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Scroller;
/**
* Created by Eillot on 2016/7/12.
*/
public class CoustomViewGroup extends ViewGroup{
private static final String TAG = "CoustomViewGroup";
private int mChildSize;
private int mChildWidth;
private int mChildIndex;
//分别记录上次滑动的坐标
private int mLastX = 0;
private int mLastY = 0;
//分别记录上次滑动的坐标对于(onInterceptTouchEvent)
private int mLastInterceptX = 0;
private int mLastInterceptY = 0;
//Scroller类可以让View平滑滚动的一个Helper类;
private Scroller mScroller;
//VelocityTracker主要用跟踪触摸屏事件(flinging事件和其他gestures手势事件)的速率;
private VelocityTracker mVelocityTracker;
public CoustomViewGroup(Context context) {
super(context);
init();
}
public CoustomViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public CoustomViewGroup(Context context, AttributeSet attrs,
int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init() {
if ( mScroller == null){
mScroller = new Scroller(getContext());
//获取滑动速率对象
mVelocityTracker = VelocityTracker.obtain();
}
}
/**
* 为什么要重写ViewGroup的事件分发呢?
* 若不重写onInterceptTouchEvent(),你会发现即使你的OnTechEvent()方法返回的是True(即应该处理事件),
* 然而,它却并没有处理事件,why ? 因为事件已经被拦截了!所以,要加入自己的逻辑判断,让onInterceptTouchEvent()
* 知道什么时候进行拦截,什么时候不进行拦截~~
*
* @param event
* @return
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN: {
intercepted = false;
//手指落下时若View平滑滚动还未完成,则打断动画,并对Down事件进行拦截
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
intercepted = true;
}
break;
}
case MotionEvent.ACTION_MOVE:{
intercepted = false;
int deltaX = x - mLastInterceptX;//View的滑动后距离的X坐标
int deltaY = y - mLastInterceptY;//View的滑动后距离的Y坐标
//用于判断是否正在由左向右进行滑动,若是则intercepted = true对事件进行拦截;
if ( (Math.abs( deltaX)) > (Math.abs(deltaY)) ){
intercepted = true;
}else {
intercepted = false;
}
break;
}
case MotionEvent.ACTION_UP:{
intercepted = false;
break;
}
default:
break;
}
Log.d( TAG , "intercepted= " + intercepted);
mLastX = x;
mLastY = y;
mLastInterceptX = x;
mLastInterceptY = y;
//不在使用supper来继续使用父类的拦截方法
return intercepted;
}
/**
* 重写onTouchEvent
* @param event
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
mVelocityTracker.addMovement(event);//表示追踪当前点击事件的速度;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
scrollBy(-deltaX, 0);
break;
}
case MotionEvent.ACTION_UP:{
int scrollX = getScrollX();
/**
* 表示计算速度,比如:时间间隔为1000 ms ,在1秒内,
* 手指在水平方向从左向右滑过100像素,那么水平速度就是100;
* 计算速度+获取速度----三步曲
* mVelocityTracker.computeCurrentVelocity(1000);
float xVelocity = mVelocityTracker.getXVelocity(); //获取水平方向的滑动速度
* float yVelocity = mVelocityTracker.getYVelocity();//获取垂直方向的滑动速度
* 由于我们需要的是xVelocity,
* 这里只是提一下,不计入代码;
* 注意:这里的速度指的是一段时间内手指所滑过的像素数!像素数!像素数!重要事说3遍;
*/
mVelocityTracker.computeCurrentVelocity(1000);
float xVelocity = mVelocityTracker.getXVelocity(); //获取水平方向的滑动速度
/**
*当你滑动手机相册中的照片的时候有没有发现,必须滑动到一定距离它才会切到下张图片,
* 否则,它就回退回原来的照片了,原来,它是通过“速度”来进行控制的~
* 还有就是"速度“可以为负值,很好理解,就像我们规定车前进的方向为正,反向为负;
*
*/
if (Math.abs(xVelocity) >= 50) {
mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
} else {
mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;
}
mChildIndex = Math.max(0, Math.min(mChildIndex, mChildSize - 1));
int dx = mChildIndex * mChildIndex - scrollX;//缓慢地滑动到目标的x坐标;
smoothScrollBy(dx, 0);
mVelocityTracker.clear();//对速度跟踪进行回收
break;
}
default: {
break;
}
}
mLastX = x ;
mLastY = y ;
return true;
}
/**
* view测量原理:
* 主要是MeasureSpace代表一个int 32位的值,高俩位分别为spaceMode(即测量模式),spaceSize(即测量大小)
* 那3中测试模式这里就赘述了,自己搜一下,主要针对说下宽高属性为wrap_content的情况,加入任一属性为wrap_content时,
* 高(宽)需要在onMeasure()方法中做特殊处理,不复杂,就是给一个默认值比如:200dp ,我在网上看到很多人都喜欢使用这个数字,
* 不知道为什么?若二者都为wrap_content ,简单那就在OnMeasure()中都做处理给个默认值呗,就这么简单!
*
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//定义俩个保存子Viw的测量宽跟高的变量measureWidth和measureHeight
int measureChildWidth = 0;
int measureChildheight = 0;
//获取子view的个数
final int childCount = getChildCount();
//测量ziView的宽高;
measure(widthMeasureSpec , heightMeasureSpec);
//下来就是套路了,确定测量的模式跟大小
int widthSpaceMode = MeasureSpec.getMode(widthMeasureSpec);
int widSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpaceMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
//下来就是写自己的逻辑判断了,这里必须感谢下大牛----coder任玉刚 ,解决了我许久的困惑,
// 比如:为什么自定义view时宽高设置为warp_content时,不重写OnMeasure()方法,View的效果等同于math_parent?
//先来判断下有没有子元素,没有就不用测了直接置0
if (childCount == 0){
setMeasuredDimension(0,0);
}else if ( (widthSpaceMode == MeasureSpec.AT_MOST) && (heightSpaceMode == MeasureSpec.AT_MOST) ){
//还记得我们自定义View的时候的处理规范吗------setMeasuredDimension(200 , 200);
//获取第一个子View的对象
final View childView = getChildAt(0);
measureChildWidth = childView.getMeasuredWidth() * childCount;
measureChildheight = childView.getMeasuredHeight() * childCount;
setMeasuredDimension(measureChildWidth , measureChildheight);
}else if ( widthSpaceMode == MeasureSpec.AT_MOST ){
//当宽属性为wrap_content时,需要所有子View的宽之和(记得我们是水平的呀)
final View childView = getChildAt(0);
measureChildWidth = childView.getMeasuredWidth() * childCount;
setMeasuredDimension(measureChildWidth , heightSpaceSize);
}else if ( heightMeasureSpec == MeasureSpec.AT_MOST ){
//注意:当高属性为wrap_content时,仅仅需要任一子View的高即可(记得我们是左右滑动的,高是不变得)
final View childView = getChildAt(0);
measureChildheight = childView.getMeasuredHeight() ;
setMeasuredDimension(widSpaceSize , measureChildheight);
}
}
/**
* onLayout()过程主要用于确定view在ViewGroup中的摆放位置,通过确定View的L , T,R ,B四个坐标点;
* @param b
* @param i
* @param i1
* @param i2
* @param i3
*/
@Override
protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
int childLeft = 0;
final int childCount = getChildCount();
//mChildrenSize = childCount;// 这里没搞懂为什么要把childCount赋值给mChildrenSize ?
//接下来就是你熟悉的套路了,遍历每个子View并获取它们的位置, 从左向右
for ( int n = 0 ; n < childCount ; n++){
final View childView = getChildAt(n);
//View可见
if ( (childView.getVisibility()) != View.GONE){
final int childWidth = childView.getMeasuredWidth();
mChildWidth = childWidth;
childView.layout(childLeft , 0 , childLeft + childWidth , childView.getMeasuredHeight());
childLeft += childWidth;
}
}
}
/**
* 缓慢滑动到自定位置
*/
private void smoothScrollBy(int dx, int dy){
//500ms内滑向dx , 效果就是慢慢地滑动
mScroller.startScroll(getScrollX() , 0 , dx , 0 , 500 );
invalidate();
}
/**
* 用于计算出当前滑动的X,Y坐标,即ScrollX ,跟 ScrollY
*/
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()){
scrollTo( mScroller.getCurrX() , mScroller.getCurrY() );
postInvalidate();
}
}
/**
* api原话:
*将视图从窗体上分离的时候调用该方法。这时视图已经不具有可绘制部分。
* 即我们已经没有需要绘制的View ,可以回收资源了;
* 很好理解,你画完图了是不是会保存,然后退出软件;
*
*/
@Override
protected void onDetachedFromWindow() {
mVelocityTracker.recycle();
super.onDetachedFromWindow();
}
}
代码注释已经写了,这里就不在赘述了~activity_main.xml代码如下:
<LinearLayout 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"
android:orientation="vertical" >
<com.android.example.CoustomViewGroup
android:id="@+id/my_CoustomViewGroup"
android:layout_width="wrap_content"
android:layout_height="match_parent" />
</LinearLayout>
运行结果如下:
目测什么都没有呀~哈哈,你可以加入自己自定的属性,比如:text文本等,就是那个attrs.xml,然后使用TypeArray加载自定义属性并解析赋值的套路呀!自己试试~~
下一节我们说说,继承特定的View,比如TextView ,继承特定的ViewGroup,比如:LinearLayout