美文网首页Android 自定义viewandroidAndroid自定义View
Android自定义控件:如何使view动起来?

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

作者: 子墨_guo | 来源:发表于2016-08-07 17:38 被阅读550次

    本文发表于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月期

    相关文章

      网友评论

      • 捡淑:子墨?是干货群里的墨墨吗?墨码家?
        子墨_guo:@捡淑 不是

      本文标题:Android自定义控件:如何使view动起来?

      本文链接:https://www.haomeiwen.com/subject/boylsttx.html