浅谈android官方文档中自定义view的demo

官方文档上有个不错自定义view的例子,仔细分析一下可以加深对动画、Scroller、 GestureDetector等以及自定义View的理解

效果如下,就是自定义了一个比较酷炫可以转动的扇形图,官方文档链接https://developer.android.com/training/custom-views/index.html

本文只会描述我认为实现的时候需要考虑的一些关键的点(实际要完整地写出来需要考虑的东西还是挺多的),有些基本的、细节的或机制的东西限于篇幅原因不太可能详细讲,可以根据给出的链接查看详细或者自行搜索

完整的demo代码在文末给出


1.关于view的onDraw(Canvas canvas)

用于绘制图像,其中Canvas(画布)可以当成具体画到的地方,由图形的具体形状确定绘画方法需要的传递的参数(如line需要传递startX、startY、endX、endY),Paint对象可以当成画笔,维护着color、size等属性,通过canvas相关方法就可以绘制不同的图像 ,如canvas.drawLine(startX, startY, endX, endY, Paint)


2. 关于画每一个扇形

demo中每一个扇形对应的model用Item表示,Item主要有label、value、color三个属性,还保存有三个计算出来的属性(startAngle、endAngle:起始角度和结束角度,画扇形的时候需要用到,highlight:使用color计算出来的高亮颜色),然后在onDraw方法中遍历items中的每一个item使用canvas.drawArc(RectF,  startAngle, endAngle, useCenter, Paint)就可以画出扇形


3.关于图像的旋转

可以通过View提供setRotation(degree)方法来实现旋转,滑动手势的检测可以通过一个叫的GestureDetector的helper class来方便地实现,GestureDetector的构造器需要传递Context和一个叫OnGestureListener的回调接口,OnGestureListener提供了很多回调方法,比如onScroll、onLongPress、onFling(表示飞速滑动的意思),我们只需要在这些方法里写我们需要的应用逻辑就可以了,简便起见可以扩展SimpleOnGestureListener类,覆盖其他的一个或多个以及onDown方法并注意在onDown中返回true就可以了

        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            // Set the pie rotation directly.
            float scrollTheta = vectorToScalarScroll(
                    distanceX,
                    distanceY,
                    e2.getX() - mPieBounds.centerX(),
                    e2.getY() - mPieBounds.centerY());
            setPieRotation(getPieRotation() - (int) scrollTheta / FLING_VELOCITY_DOWNSCALE);
            return true;
        }


这里我们覆盖了 boolean onScroll(MotionEvent e1, MotionEvent e2,floatdistanceX,floatdistanceY)方法,并根据e1、e2、distanceX、distanceY计算出滑动路线对应的圆周角(demo里面不是计算圆周角的,但是我觉得计算圆周角比较合适),再调用setRotation(degree)即可实现旋转

最后在onTouchEvent中将触摸事件转交给GestureDetector处理就可以了,关于onTouchEvent返回true和false的问题(view事件机制)请自行百度谷歌或查阅官方文档

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // Let the GestureDetector interpret this event
        boolean result = mDetector.onTouchEvent(event);

        // If the GestureDetector doesn't want this event, do some custom processing.
        // This code just tries to detect when the user is done scrolling by looking
        // for ACTION_UP events.
        if (!result) {
            if (event.getAction() == MotionEvent.ACTION_UP) {
                // User is done scrolling, it's now safe to do things like autocenter
                stopScrolling();
                result = true;
            }
        }
        return result;
    }



4.关于fling(飞速滑动)

   fling可以想象成惯性滑动,通过实现OnGestureListener回调接口的 boolean onFling(MotionEvent e1, MotionEvent e2, floatvelocityX,floatvelocityY) 方法可以实现相关的惯性滑动逻辑,这里我们使用到了Scroller和ValueAnimator(android3.0推出的一种动画形式)来实现圆顺地滑动

        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            // Set up the Scroller for a fling
            float scrollTheta = vectorToScalarScroll(
                    velocityX,
                    velocityY,
                    e2.getX() - mPieBounds.centerX(),
                    e2.getY() - mPieBounds.centerY());
            mScroller.fling(
                    0,
                    (int) getPieRotation(),
                    0,
                    (int) scrollTheta / FLING_VELOCITY_DOWNSCALE,
                    0,
                    0,
                    Integer.MIN_VALUE,
                    Integer.MAX_VALUE);

            // Start the animator and tell it to animate for the expected duration of the fling.
            if (Build.VERSION.SDK_INT >= 11) {
                mScrollAnimator.setDuration(mScroller.getDuration());
                mScrollAnimator.start();
            }
            return true;
        }

Scroller类似于GestureDetector,也是一个Helper class,只是封装了滑动相关的动作需要实现的数学逻辑,本身并不执行view的滑动操作,因此需要Animator的配合:

View创建时对Aniamtor和Scroller的初始化

        if (Build.VERSION.SDK_INT < 11) {
            mScroller = new Scroller(getContext());
        } else {
            mScroller = new Scroller(getContext(), null, true);
        }

            mScrollAnimator = ValueAnimator.ofFloat(0, 1); //0、1随便取的,不用纠结取值问题
            mScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                public void onAnimationUpdate(ValueAnimator valueAnimator) {
                    tickScrollAnimation(); //每次value更新的时候都会回调这个方法
                }
            });

    private void tickScrollAnimation() {
        if (!mScroller.isFinished()) {
            mScroller.computeScrollOffset(); //
            setPieRotation(mScroller.getCurrY());
        } else {
            if (Build.VERSION.SDK_INT >= 11) {
                mScrollAnimator.cancel();
            }
            onScrollFinished();
        }
    }

以及在onFling中开始滑动

                mScrollAnimator.setDuration(mScroller.getDuration());
                mScrollAnimator.start();

Scroller的另外一种常见使用方式是重写view的onComputeScroll方法,内容和tickScrollAnimation中差不多,具体不详细介绍

还有另一种Scroller叫OverScroller,额外提供了springBack(超过回弹),ScrollerView中也使用了OverScroller,在自定义View中使用的频率还是挺高的,具体不详细介绍


5.关于自动滑动到扇形中间

代码实现如下:
    /**
     * Kicks off an animation that will result in the pointer being centered in the
     * pie slice of the currently selected item.
     */
    private void centerOnCurrentItem() {
        Item current = mData.get(getCurrentItem());
        int targetAngle = current.mStartAngle + (current.mEndAngle - current.mStartAngle) / 2;
        targetAngle -= mCurrentItemAngle;
        if (targetAngle < 90 && mPieRotation > 180) targetAngle += 360;

        if (Build.VERSION.SDK_INT >= 11) {
            // Fancy animated version
            mAutoCenterAnimator.setIntValues(targetAngle);
            mAutoCenterAnimator.setDuration(AUTOCENTER_ANIM_DURATION).start();
        } else {
            // Dull non-animated version
            //mPieView.rotateTo(targetAngle);
        }
    }

先是需要计算出centerItem需要旋转的targetAngle值,然后使用值动画mAutoCenterAnimator(android 3.0以上版本的时候)进行滑动或者是直接...作者直接注释掉了

mAutoCenterAnimator的初始化:

<span style="white-space:pre">	</span>if (Build.VERSION.SDK_INT >= 11) {
            mAutoCenterAnimator = ObjectAnimator.ofInt(PieChart.this, "PieRotation", 0);

            // Add a listener to hook the onAnimationEnd event so that we can do
            // some cleanup when the pie stops moving.
            mAutoCenterAnimator.addListener(new Animator.AnimatorListener() {
                public void onAnimationStart(Animator animator) {
                }

                public void onAnimationEnd(Animator animator) {
                    mPieView.decelerate();
                }

                public void onAnimationCancel(Animator animator) {
                }

                public void onAnimationRepeat(Animator animator) {
                }
            });
        }

实际使用值动画的时候为了兼容到API8一般的做法是使用nineoldandroids开源兼容动画框架http://nineoldandroids.com/,就不用像demo中需要写那么多检测API版本的烦人代码了

centerOnCurrentItem方法需要在滑动结束后调用(如下的onScrollFinished代码和上面tickScrollAnimation中在滑动结束后对onScrollFinished的)

    /**
     * Called when the user finishes a scroll action.
     */
    private void onScrollFinished() {
        if (mAutoCenterInSlice) {
            centerOnCurrentItem();
        } else {
            mPieView.decelerate(); //关闭硬件加速,这一行可以忽略
        }
    }
但是setPieRotation(在每次检测到滑动动作的时候都会调用到,很频繁)也会间接调用到centerOnCurrentItem方法,具体为什么调我有点迷茫...


6.关于onSizeChanged方法

自定义ViewGroup的时候都会重写onMeasure(不然layoutParams为wrap_content时会得不到我们想要的效果)和onLayout方法,demo中相应的方法如下所示:
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // Try for a width based on our minimum
        int minw = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth();

        int w = Math.max(minw, MeasureSpec.getSize(widthMeasureSpec));

        // Whatever the width ends up being, ask for a height that would let the pie
        // get as big as it can
        int minh = (w - (int) mTextWidth) + getPaddingBottom() + getPaddingTop();
        int h = Math.min(MeasureSpec.getSize(heightMeasureSpec), minh);

        setMeasuredDimension(w, h);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        // Do nothing. Do not call the superclass method--that would start a layout pass
        // on this view's children. PieChart lays out its children in onSizeChanged().
    }
有点让我意外的是onLayout方法是空的,对子View进行布局的逻辑都移到了onSizeChanged里,后来一想,因为子元素都是固定的,和LinearLayout之类的布局不一样,而且通过log打印发现onSizeChanged只被调用了一次,onMeasure和onLayout方法被调用了好几次,所以这样还是挺合理的吧
onSizeChanged方法(好长啊啊):
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        //
        // Set dimensions for text, pie chart, etc
        //
        // Account for padding
        float xpad = (float) (getPaddingLeft() + getPaddingRight());
        float ypad = (float) (getPaddingTop() + getPaddingBottom());

        // Account for the label
        if (mShowText) xpad += mTextWidth;

        float ww = (float) w - xpad;
        float hh = (float) h - ypad;

        // Figure out how big we can make the pie.
        float diameter = Math.min(ww, hh);
        mPieBounds = new RectF(
                0.0f,
                0.0f,
                diameter,
                diameter);
        mPieBounds.offsetTo(getPaddingLeft(), getPaddingTop());

        mPointerY = mTextY - (mTextHeight / 2.0f);
        float pointerOffset = mPieBounds.centerY() - mPointerY;

        // Make adjustments based on text position
        if (mTextPos == TEXTPOS_LEFT) {
            mTextPaint.setTextAlign(Paint.Align.RIGHT);
            if (mShowText) mPieBounds.offset(mTextWidth, 0.0f);
            mTextX = mPieBounds.left;

            if (pointerOffset < 0) {
                pointerOffset = -pointerOffset;
                mCurrentItemAngle = 225;
            } else {
                mCurrentItemAngle = 135;
            }
            mPointerX = mPieBounds.centerX() - pointerOffset;
        } else {
            mTextPaint.setTextAlign(Paint.Align.LEFT);
            mTextX = mPieBounds.right;

            if (pointerOffset < 0) {
                pointerOffset = -pointerOffset;
                mCurrentItemAngle = 315;
            } else {
                mCurrentItemAngle = 45;
            }
            mPointerX = mPieBounds.centerX() + pointerOffset;
        }

        mShadowBounds = new RectF(
                mPieBounds.left + 10,
                mPieBounds.bottom + 10,
                mPieBounds.right - 10,
                mPieBounds.bottom + 20);

        // Lay out the child view that actually draws the pie.
        mPieView.layout((int) mPieBounds.left,
                (int) mPieBounds.top,
                (int) mPieBounds.right,
                (int) mPieBounds.bottom);
        mPieView.setPivot(mPieBounds.width() / 2, mPieBounds.height() / 2);

        mPointerView.layout(0, 0, w, h);
        onDataChanged();
    }
代码还是挺好理解的,不过有点长是真的...
其他关于attrs定义之类的基础的东西可以自行搜索


  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值