未经允许不得转载,我们可以把整个加载动画拆

2019-09-13 11:20栏目:大奖888官网登录
TAG:

我们修改frame中的某个值,需要进行繁琐的书写,例如:

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

转载请注明出处

. 直接设置位置大小
view.frame = CGRectMake(0, 0, 320, 150);

摘要

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

在我们的项目中,很多场景需要使用加载进度动画,如网络请求,数据加载等。

. 只修改某个值
view.frame = CGRectMake(view.frame.origin.x, 100, view.frame.size.width, view.frame.size.height);

这种写法在界面元素较多的排版中,会让程序员非常痛苦,而且可读性也会减弱

如果能够人性化一点,可以单独地修改某个值,或者再人性化一点,可以输出view中每一个点的坐标,将更有利于布局。我们可以通过建立view的类别 category 来实现这个人性化的布局操作

实现

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

view本身实现移动:

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

现在市面大多数app都有拥有自己独特风格的加载动画,而不是谷歌为我们提供的菊花圈。一个绚丽美观的加载动画可以消除用户的等待焦虑。本文主要介绍利用自定义view打造一个绚丽的加载动画。

一、建立类别

建立一个名为Layout的UIView类别,类别的代码如下:

.h 文件定义了 x, y, width, height 等方便读写的属性,代码如下:

#import <UIKit/UIKit.h>@interface UIView @property (assign, nonatomic) CGFloat top;@property (assign, nonatomic) CGFloat bottom;@property (assign, nonatomic) CGFloat left;@property (assign, nonatomic) CGFloat right;@property (assign, nonatomic) CGFloat x;@property (assign, nonatomic) CGFloat y;@property (assign, nonatomic) CGPoint origin;@property (assign, nonatomic) CGFloat centerX;@property (assign, nonatomic) CGFloat centerY;@property (assign, nonatomic) CGFloat width;@property (assign, nonatomic) CGFloat height;@property (assign, nonatomic) CGSize size;@end

.m 文件实现各个属性的setter和getter方法,代码如下:

#import "UIView+Layout.h"@implementation UIView @dynamic top;@dynamic bottom;@dynamic left;@dynamic right;@dynamic width;@dynamic height;@dynamic size;@dynamic x;@dynamic y;- top{ return self.frame.origin.y;}- setTop:top{ CGRect frame = self.frame; frame.origin.y = top; self.frame = frame;}- left{ return self.frame.origin.x;}- setLeft:left{ CGRect frame = self.frame; frame.origin.x = left; self.frame = frame;}- bottom{ return self.frame.size.height + self.frame.origin.y;}- setBottom:bottom{ CGRect frame = self.frame; frame.origin.y = bottom - frame.size.height; self.frame = frame;}- right{ return self.frame.size.width + self.frame.origin.x;}- setRight:right{ CGRect frame = self.frame; frame.origin.x = right - frame.size.width; self.frame = frame;}- x{ return self.frame.origin.x;}- setX:value{ CGRect frame = self.frame; frame.origin.x = value; self.frame = frame;}- y{ return self.frame.origin.y;}- setY:value{ CGRect frame = self.frame; frame.origin.y = value; self.frame = frame;}- origin{ return self.frame.origin;}- setOrigin:origin{ CGRect frame = self.frame; frame.origin = origin; self.frame = frame;}- centerX{ return self.center.x;}- setCenterX:centerX{ CGPoint center = self.center; center.x = centerX; self.center = center;}- centerY{ return self.center.y;}- setCenterY:centerY{ CGPoint center = self.center; center.y = centerY; self.center = center;}- width{ return self.frame.size.width;}- setWidth:width{ CGRect frame = self.frame; frame.size.width = width; self.frame = frame;}- height{ return self.frame.size.height;}- setHeight:height{ CGRect frame = self.frame; frame.size.height = height; self.frame = frame;}- size{ return self.frame.size;}- setSize:size{ CGRect frame = self.frame; frame.size = size; self.frame = frame;}@end

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的时候,可以直接使用这两个方法,简单,方便

先看效果图:

二、使用

类别创建完成以后,view的布局就显得轻松很多了例如原来你要修改y坐标时要这样写:

view.frame = CGRectMake(view.frame.origin.x, 100, view.frame.size.width, view.frame.size.height);

而现在只需要这样写:

view.y = 100;

既简洁又方便

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了

图片 1

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

从效果图中,我们可以把整个加载动画拆分成以下4个功能点:

动画

说到借助工具实现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();
  1. 画指定数目的环绕圆环
  2. 圆环旋转动画
  3. 旋转过程圆环聚拢
  4. 旋转过程圆环收缩

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()方法完成实际的滑动。

3.1 自定义属性

我们需要自定义俩个属性:圆点个数dot_count、圆点颜色dot_color代码如下:

<?xml version="1.0" encoding="utf-8"?><resources> <declare-styleable name="ProgressView"> <attr name="dot_color" format="color"/> <attr name="dot_count" format="integer"/> </declare-styleable></resources>

ViewDragHelper

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

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

3.2 获取布局文件中设置好的自定义属性

我们需要在java代码中获取在xml布局文件中设置的自定义属性:

 public ProgressView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ProgressView); mDotColor = ta.getColor(R.styleable.ProgressView_dot_color, mDotColor); mDotCount = ta.getInt(R.styleable.ProgressView_dot_count, mDotCount); ta.recycle(); }

我们的布局属性全部储存在构造器的attrs中,通过context.obtainStyledAttributes(attrs, R.styleable.ProgressView)方法即可获取到设置的自定义属性,记得获取完成后调用recycle()回收资源.

在我们的自定义view ProgressView的构造器中进行初始化工作。

 mPaint = new Paint(); mPaint.setAntiAlias; mPaint.setStyle(Style.FILL_AND_STROKE); mPaint.setColor(mDotColor); // 屏幕适配,转化圆环半径,小点半径 mRingRadius = DensityUtils.dp2px(getContext(), mRingRadius); mDotRadius = DensityUtils.dp2px(getContext(), mDotRadius); mOriginalDotRadius = mDotRadius;

初始化画笔,设置颜色,抗锯齿,通过setStyle(Style.FILL_AND_STROKE)设置画笔实心。

设置小圆离中心的距离mRingRadius,小圆半径mDotRadius,因为动画工程中小圆半径会变化,所以用一个变量mOriginalDotRadius来保存小圆半径的初始值(用来计算变化中的小圆半径)。

其中DensityUtils.dp2px()方法是根据屏幕像素密度将像dp值转化为像素。具体代码:

public static int dp2px(Context context, float dp) { return  TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics; }

初始化动画

 private void initAnimatior() { mAnimator = ValueAnimator.ofInt; mAnimator.setDuration; mAnimator.setRepeatCount; mAnimator.setRepeatMode(ValueAnimator.INFINITE); mAnimator.setInterpolator(new LinearInterpolator; }

我们用ValueAnimator来动态计算当前旋转角度mCurrentAngle,变化值从0到359更换,其他设置都很简单,比如设置动画时间,重复次数-1,注意这行代码mAnimator.setInterpolator(new LinearInterpolator,设置动画差值器为线性匀速,这个值改变后会改变动画效果

 mAnimator.addUpdateListener(new AnimatorUpdateListener() { @Override public void mAnimator(ValueAnimator animation) { mCurrentAngle =  animation.getAnimatedValue(); invalidate;

给我们的mAnimator设置监听,在mAnimator()方法中将当前计算出来的值赋值给mCurrentAngle,再调用invalidate()重绘页面,此时view会执行ondraw方法,我们这个动画的原理就是,动态更改mCurrentAngle的值,不断重绘,稍后讲解怎么根据mCurrentAngle绘图。

一个好的自定义view必须提供完美的兼容性,有时候,我们可能在布局文件了设置了view的大小,如果view的长宽小于我们代码设值得小圆点离中心点得距离mRingRadius的俩倍,小球将会绘制在视图之外,导致看不到。所以我们在onLayout方法中调整mRingRadius,这里计算宽高需要扣除内边距。

因为在view的绘制流程onLayout()中,可以获取到view的实际宽高,所以我们把调整代码放在这里,以下是具体代码:

 @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); // 重设圆环半径,防止超出视图大小 int effectiveWidth = getWidth() - getPaddingLeft() - getPaddingRight(); int effectiveHeight = getHeight() - getPaddingBottom() - getPaddingTop(); int maxRadius = Math.min(effectiveWidth / 2, effectiveHeight / 2) - mDotRadius; mRingRadius = mRingRadius > maxRadius ? maxRadius : mRingRadius; mOriginalRingRadius = mRingRadius; }

假设坐标轴y轴向上方向为0度,小球的当前角度angle可以计算出小球所在的坐标,计算方法如下图:

图片 2这里写图片描述

在我们这,圆点坐标是view的中心点,即宽高的一半。

 private void drawDot(Canvas canvas, double angle) { //根据当前角度获取x、y坐标点 float x =  (getWidth() / 2 + mRingRadius * Math.sin; float y =  (getHeight() / 2 - mRingRadius * Math.cos; //绘制圆 canvas.drawCircle(x, y, mDotRadius, mPaint); }

将小球到中心的距离mRingRadius缩小就达到了聚拢的效果,同理缩小小球半径mDotRadius就可以改变小球大小。我们根据当前旋转角度mCurrentAngle进行变化。

为了方便计算,我们封装一个估值器方法:

 private Integer evaluate(float fraction, Integer startValue, Integer endValue) { int startInt = startValue; return  (startInt + fraction * (endValue - startInt)); }

这个方法在安卓动画计算中很常用,实现还是很简单的,传入三个参数,含义如下:

  • fraction:估值器的值,大小从0-1变化,控制我们最终值变化的变量
  • startValue:起始值,当fraction为0时计算得出的值
  • endValue:最终值,当fraction为1时计算得出的值

下面讲解如何通过该方法mCurrentAngle计算mRingRadius

我们需要一个fraction变量来控制mRingRadius的最终值,前面说了,变量是当前旋转的角度mCurrentAngle。那么如何一个0-360的mCurrentAngle将转为为一个0-1的值呢?俩行代码搞定:

 float fraction = 1.0f * mCurrentAngle / 180 - 1; fraction = Math.abs;

这样,当mCurrentAngle从0到180度变化时,fraction从1到0,随着mCurrentAngle从180变化到360时fraction继续从1变化到0,如此循环往复。得到了我们估值器的估值变量fraction。我们mRingRadius的变化是从原始值减小到一半,mDotRadius的变化是从原始值减小到4/5。我们打印下mRingRadius变化情况。

图片 3这里写图片描述

public class ProgressView extends View{ private int mDotCount = 5; // 圆点个数 private int mDotColor = 0xFFFF9966;// 圆点颜色 private Paint mPaint; private int mRingRadius = 50;// 圆环半径,单位dp private int mOriginalRingRadius;// 保存的原始圆环半径,单位dp private int mDotRadius = 7; // 小点半径,单位dp private int mOriginalDotRadius; // 保存的原始小点半径,单位dp private int mCurrentAngle = 0; // 当前旋转的角度 private ValueAnimator mAnimator;// 旋转动画 public ProgressView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ProgressView); mDotColor = ta.getColor(R.styleable.ProgressView_dot_color, mDotColor); mDotCount = ta.getInt(R.styleable.ProgressView_dot_count, mDotCount); ta.recycle; } public ProgressView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public ProgressView(Context context) { this(context, null); } private void init() { mPaint = new Paint(); mPaint.setAntiAlias; mPaint.setStyle(Style.FILL_AND_STROKE); mPaint.setColor(mDotColor); // 屏幕适配,转化圆环半径,小点半径 mRingRadius = DensityUtils.dp2px(getContext(), mRingRadius); mDotRadius = DensityUtils.dp2px(getContext(), mDotRadius); mOriginalDotRadius = mDotRadius; initAnimatior(); } private void initAnimatior() { mAnimator = ValueAnimator.ofInt; mAnimator.setDuration; mAnimator.setRepeatCount; mAnimator.setRepeatMode(ValueAnimator.INFINITE); mAnimator.setInterpolator(new LinearInterpolator; mAnimator.addUpdateListener(new AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mCurrentAngle =  animation.getAnimatedValue(); invalidate; } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); // 重设圆环半径,防止超出视图大小 int effectiveWidth = getWidth() - getPaddingLeft() - getPaddingRight(); int effectiveHeight = getHeight() - getPaddingBottom() - getPaddingTop(); int maxRadius = Math.min(effectiveWidth / 2, effectiveHeight / 2) - mDotRadius; mRingRadius = mRingRadius > maxRadius ? maxRadius : mRingRadius; mOriginalRingRadius = mRingRadius; } @Override protected void onDraw(Canvas canvas) { // 根据小球总数平均分配整个圆,得到每个小球的间隔角度 double cellAngle = 360 / mDotCount; for (int i = 0; i < mDotCount; i++) { double ange = i * cellAngle + mCurrentAngle; // 根据当前角度计算小球到圆心的距离 calculateRadiusFromProgress(); // 根据角度绘制单个小球 drawDot(canvas, ange * 2 * Math.PI / 360); } } /** * 根据当前旋转角度计算mRingRadius、mDotRadius的值 * mCurrentAngle: 0 - 180 - 360 * mRingRadius: 最小 - 最大 - 最小 * @author 漆可 * @date 2016-6-17 下午3:04:35 */ private void calculateRadiusFromProgress() { float fraction = 1.0f * mCurrentAngle / 180 - 1; fraction = Math.abs; mRingRadius = evaluate(fraction, mOriginalRingRadius, mOriginalRingRadius * 2 / 4); mDotRadius = evaluate(fraction, mOriginalDotRadius, mOriginalDotRadius * 4 / 5); } // fraction:当前的估值器计算值,startValue:起始值,endValue:终点值 private Integer evaluate(float fraction, Integer startValue, Integer endValue) { return  (startValue + fraction * (endValue - startValue)); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); startAnimation(); } private void drawDot(Canvas canvas, double angle) { // 根据当前角度获取x、y坐标点 float x =  (getWidth() / 2 + mRingRadius * Math.sin; float y =  (getHeight() / 2 - mRingRadius * Math.cos; // 绘制圆 canvas.drawCircle(x, y, mDotRadius, mPaint); } public void startAnimation() { mAnimator.start(); } public void stopAnimation() { mAnimator.end(); } //销毁页面时停止动画 @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); stopAnimation(); }}

最后,奉上demo下载地址:

总结

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

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

版权声明:本文由大奖888-www.88pt88.com-大奖888官网登录发布于大奖888官网登录,转载请注明出处:未经允许不得转载,我们可以把整个加载动画拆