Android自定义控件:如何使view动起来?

来源:互联网 发布:开淘宝店铺照片要求 编辑:程序博客网 时间:2024/05/20 15:41

本文发表于CSDN《程序员》杂志2016年8月期,未经允许不得转载!

摘要

Android中的很多控件都有滑动功能,但是很多时候原生控件满足不了需求时,就需要自定义控件,那么如何能让控件滑动起来呢?本文主要总结几种可以使控件滑动起来的方法

实现

其实能让view动起来的方法,要么就是view本身具备滑动功能,像listview那样可以上下滑动;要么就是布局实现滑动功能,像ScrollView那样使内测的子view滑动;要么就直接借助动画或者工具类实现view滑动,下面从这几方面给出view滑动的方法

view本身实现移动:

  • offsetLeftAndRight(offsetX) or offsetTopAndBottom(offsetY)
  • layout方法

offsetLeftAndRight(offsetX) or offsetTopAndBottom(offsetY)

看到这两个方法的名字基本就知道它是做什么的,下面先看一下源码,了解一下实现原理

public void offsetLeftAndRight(int offset) {    if (offset != 0) {        final boolean matrixIsIdentity = hasIdentityMatrix();        if (matrixIsIdentity) {            if (isHardwareAccelerated()) {                invalidateViewProperty(false, false);            } else {                final ViewParent p = mParent;                if (p != null && mAttachInfo != null) {                    final Rect r = mAttachInfo.mTmpInvalRect;                    int minLeft;                    int maxRight;                    if (offset < 0) {                        minLeft = mLeft + offset;                        maxRight = mRight;                    } else {                        minLeft = mLeft;                        maxRight = mRight + offset;                    }                    r.set(0, 0, maxRight - minLeft, mBottom - mTop);                    p.invalidateChild(this, r);                }            }        } else {            invalidateViewProperty(false, false);        }        mLeft += offset;        mRight += offset;        mRenderNode.offsetLeftAndRight(offset);        if (isHardwareAccelerated()) {            invalidateViewProperty(false, false);            invalidateParentIfNeededAndWasQuickRejected();        } else {            if (!matrixIsIdentity) {                invalidateViewProperty(false, true);            }            invalidateParentIfNeeded();        }        notifySubtreeAccessibilityStateChangedIfNeeded();    }}

判断offset是否为0,也就是说是否存在滑动距离,不为0的情况下,根据是否在矩阵中做过标记来操作。如果做过标记,没有开启硬件加速则开始计算坐标。先获取到父view,如果父view不为空,在offset<0时,计算出左侧的最小边距,在offset>0时,计算出右侧的最大值,其实分析了这么多主要的实现代码就那一句 mRenderNode.offsetLeftAndRight(offset),由native实现的左右滑动,以上分析的部分主要计算view显示的区域。
最后总结一下,offsetLeftAndRight(int offset)就是通过offset值改变了ViewgetLeft()getRight()实现了View的水平移动。

offsetTopAndBottom(int offset)方法实现原理与offsetLeftAndRight(int offset)相同,offsetTopAndBottom(int offset)通过offset值改变ViewgetTop()getBottom()值,同样给出核心代码mRenderNode.offsetTopAndBottom(offset),这个方法也是有native实现

在实现自定义view的时候,可以直接使用这两个方法,简单,方便

layout方法

layout方法是如何实现view移动呢?talk is cheap show me the code

public void layout(int l, int t, int r, int b) {    if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {        onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);        mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;    }    int oldL = mLeft;    int oldT = mTop;    int oldB = mBottom;    int oldR = mRight;    boolean changed = isLayoutModeOptical(mParent) ?            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {        onLayout(changed, l, t, r, b);        mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;        ListenerInfo li = mListenerInfo;        if (li != null && li.mOnLayoutChangeListeners != null) {            ArrayList<OnLayoutChangeListener> listenersCopy =                    (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();            int numListeners = listenersCopy.size();            for (int i = 0; i < numListeners; ++i) {                listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);            }        }    }    mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;    mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;}

先计算mPrivateFlags3PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT的与运算,先来看一下mPrivateFlags3赋值的过程:

if (cacheIndex < 0 || sIgnoreMeasureCache) {    // measure ourselves, this should set the measured dimension flag back    onMeasure(widthMeasureSpec, heightMeasureSpec);    mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;} else {    long value = mMeasureCache.valueAt(cacheIndex);    // Casting a long to int drops the high 32 bits, no mask needed    setMeasuredDimensionRaw((int) (value >> 32), (int) value);    mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;}

以上代码摘自measure方法中,如果当前的if条件成立,就走onMeasure方法,给mPrivateFlags3赋值,跟PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT与运算为0,也就是说layout方法的第一个if不成立,不执行onMeasure方法,如果measure方法中的if条件不成立,那个mPrivateFlags3PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT作与运算时就不为0,在layout方法中的第一个if成立,执行onMeasure方法。
如果左上右下的任何一个值发生改变,都会触发onLayout(changed, l, t, r, b)方法,到这里应该明白View是如何移动的,通过Layout方法给的l,t,r,b改变View的位置。

layout(int l, int t, int r, int b)

  • 第一个参数 view左侧到父布局的距离
  • 第二个参数 view顶部到父布局之间的距离
  • 第三个参数 view右侧到父布局之间的距离
  • 第四个参数 view底端到父布局之间的距离

通过改变父布局实现view移动

  • scrollTo or scrollBy
  • LayoutParams

### scrollTo or scrollBy

先看一下scrollTo 的源码

public void scrollTo(int x, int y) {    if (mScrollX != x || mScrollY != y) {        int oldX = mScrollX;        int oldY = mScrollY;        mScrollX = x;        mScrollY = y;        invalidateParentCaches();        onScrollChanged(mScrollX, mScrollY, oldX, oldY);        if (!awakenScrollBars()) {            postInvalidateOnAnimation();        }    }}

判断当前的坐标是否是同一个坐标,不是的话,把当前坐标点赋值给旧的坐标点,把即将移动到的坐标点赋值给当前坐标点,通过onScrollChanged(mScrollX, mScrollY, oldX, oldY)方法移动到坐标点(x,y)处。

public void scrollBy(int x, int y) {    scrollTo(mScrollX + x, mScrollY + y);}

scrollBy方法简单粗暴,调用scrollTo 方法,在当前的位置继续偏移(x , y)

这里把它归类到通过改变父布局实现view移动是有原因,如果在view中使用这个方法改变的是内容,不是改变view本身,如果在ViewGroup使用这个方法,改变的是子view的位置,相对来说这个实用的概率比较大. 

注:以上例子继承自LinearLayout,如果在view中使用,想改变view自身的话,就要先获得外层布局了,想改变view的内容的话,直接写就OK了

LayoutParams

LayoutParams保存布局参数,通过改变局部参数里面的值改变view的位置,如果布局中有多个view,那么多个view的位置整体移动

@Override    public boolean onTouchEvent(MotionEvent event) {        int x = (int) event.getX();        int y = (int) event.getY();        switch (event.getAction()) {            case MotionEvent.ACTION_DOWN:                lastX = x;                lastY = y;                break;            case MotionEvent.ACTION_MOVE:                int offsetX = x - lastX;                int offsetY = y - lastY;                LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) getLayoutParams();                params.leftMargin = getLeft() + offsetX;                params.topMargin = getTop() + offsetY;                setLayoutParams(params);                break;        }        return true;    }

借助 Android 提供的工具实现移动

  • 动画
  • Scroller
  • ViewDragHelper

动画

说到借助工具实现view的移动,相信第一个出现在脑海中的就是动画,动画有好几种,属性动画,帧动画,补间动画等,这里只给出属性动画的实例,属性动画就能实现以上几种动画的所有效果

直接在代码中写属性动画或者写入xml文件,这里给出一个xml文件的属性动画

<?xml version="1.0" encoding="utf-8"?><set xmlns:android="http://schemas.android.com/apk/res/android">    <objectAnimator        android:duration="5000"        android:propertyName="translationX"        android:valueFrom="100dp"        android:valueTo="200dp"/>    <objectAnimator        android:duration="5000"        android:propertyName="translationY"        android:valueFrom="100dp"        android:valueTo="200dp"/></set>

然后在代码中读取xml文件

animator = AnimatorInflater.loadAnimator(MainActivity.this,R.animator.translation);animator.setTarget(image);animator.start();

Scroller

Android 中的 Scroller 类封装了滚动操作,记录滚动的位置,下面看一下scroller的源码

public Scroller(Context context) {    this(context, null);}public Scroller(Context context, Interpolator interpolator) {    this(context, interpolator,            context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB);}public Scroller(Context context, Interpolator interpolator, boolean flywheel) {    mFinished = true;    if (interpolator == null) {        mInterpolator = new ViscousFluidInterpolator();    } else {        mInterpolator = interpolator;    }    mPpi = context.getResources().getDisplayMetrics().density * 160.0f;    mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());    mFlywheel = flywheel;    mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning}

一般直接使用第一个构造函数,interpolator默认创建一个ViscousFluidInterpolator,主要就是初始化参数

public void startScroll(int startX, int startY, int dx, int dy) {    startScroll(startX, startY, dx, dy, DEFAULT_DURATION);}public void startScroll(int startX, int startY, int dx, int dy, int duration) {    mMode = SCROLL_MODE;    mFinished = false;    mDuration = duration;    mStartTime = AnimationUtils.currentAnimationTimeMillis();    mStartX = startX;    mStartY = startY;    mFinalX = startX + dx;    mFinalY = startY + dy;    mDeltaX = dx;    mDeltaY = dy;    mDurationReciprocal = 1.0f / (float) mDuration;}

使用过Scroller的都知道要调用这个方法,它主要起到记录参数的作用,记录下当前滑动模式,是否滑动结束,滑动时间,开始时间,开始滑动的坐标点,滑动结束的坐标点,滑动时的偏移量,插值器的值,看方法名字会造成一个错觉,view要开始滑动了,其实这是不正确的,这个方法仅仅是记录而已,其他事什么也没做

Scroller还有一个重要的方法就是computeScrollOffset(),它的职责就是计算当前的坐标点

public boolean computeScrollOffset() {    if (mFinished) {        return false;    }    int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);    if (timePassed < mDuration) {        switch (mMode) {        case SCROLL_MODE:            final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);            mCurrX = mStartX + Math.round(x * mDeltaX);            mCurrY = mStartY + Math.round(x * mDeltaY);            break;        case FLING_MODE:            final float t = (float) timePassed / mDuration;            final int index = (int) (NB_SAMPLES * t);            float distanceCoef = 1.f;            float velocityCoef = 0.f;            if (index < NB_SAMPLES) {                final float t_inf = (float) index / NB_SAMPLES;                final float t_sup = (float) (index + 1) / NB_SAMPLES;                final float d_inf = SPLINE_POSITION[index];                final float d_sup = SPLINE_POSITION[index + 1];                velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);                distanceCoef = d_inf + (t - t_inf) * velocityCoef;            }            mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;                        mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));            // Pin to mMinX <= mCurrX <= mMaxX            mCurrX = Math.min(mCurrX, mMaxX);            mCurrX = Math.max(mCurrX, mMinX);                        mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));            // Pin to mMinY <= mCurrY <= mMaxY            mCurrY = Math.min(mCurrY, mMaxY);            mCurrY = Math.max(mCurrY, mMinY);            if (mCurrX == mFinalX && mCurrY == mFinalY) {                mFinished = true;            }            break;        }    }    else {        mCurrX = mFinalX;        mCurrY = mFinalY;        mFinished = true;    }    return true;}

当前时间减去开始的时间小于滑动时间,也就是当前还没有滑动结束,利用插值器的值计算当前坐标点的值。

其实Scroller并不会使View动起来,它起到的作用就是记录和计算的作用,通过invalidate()刷新界面调用onDraw方法,进而调用computeScroll()方法完成实际的滑动。

ViewDragHelper

ViewDragHelper封装了滚动操作,内部使用了Scroller滑动,所以使用ViewDragHelper也要实现computeScroll()方法,这里不再给出实例,最好的实例就是Android的源码,最近有看DrawerLayout源码,DrawerLayout滑动部分就是使用的ViewDragHelper实现的,先了解更多关于ViewDragHelper的内容请看DrawerLayout源码分析

注:ViewDragHelper比较重要的两点,一是ViewDragHelper.callback方法,这里面的方法比较多,可以按照需要重写,另一个就是要把事件拦截和事件处理留给ViewDragHelper,否则写的这一推代码,都没啥价值了。

总结

熟练掌握以上这几种方法,完美的使view动起来,然后在onMeasure方法中准确的去计算view的宽高,完美的自定义view就出自你手了!再熟悉一下onLayout方法,自定义ViewGroup也就熟练掌握了,当然自定义view或者自定义ViewGroup写的越多越熟练。本文如果有不正确的地方,欢迎指正!

本文与已发布的文章有些许出入,详情见《程序员》杂志2016年8月期

3 0
原创粉丝点击