屬性動畫:如何自定義View

雨幕青山發表於2017-04-26

道長今天說一下自定義View的實現,因為有很大一部分自定義View都帶有動畫,所以把自定義View放到屬性動畫裡聊聊,let’s go……

一、需求明確

首先明確需求,根據需求建立佈局,這裡道長就自己定需求了:
1.拖動紅色方塊,紅色沿著中心旋轉並且顏色便為綠色,藍色沿著Y軸旋轉
2.拖動藍色方塊,紅色沿著中心旋轉並且顏色便為綠色,藍色沿著Y軸旋轉
3.拖動綠色方塊,紅色沿著中心旋轉並且顏色便為綠色,藍色沿著Y軸旋轉
4.三種方塊只可在螢幕內拖動

這裡需求對佈局的要求不明顯,我們們把整個介面作為自定義View,程式碼如下:

<?xml version="1.0" encoding="utf-8"?>
<com.yushan.animdemo.DragLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.yushan.animdemo.MainActivity">

    <TextView
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:background="#ff0000" />

    <TextView
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:background="#0000ff" />

    <TextView
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:background="#00ff00" />

</com.yushan.animdemo.DragLayout>


效果如下:
這裡寫圖片描述

二、實現自定義View

  • 首先重寫構造方法:
    public DragLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }
    public DragLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    public DragLayout(Context context) {
        super(context);
        init();
    }

    private void init(){
        scroller = new Scroller(getContext());
        viewDragHelper = ViewDragHelper.create(this, callback);
    }
  • 獲取子View
    /**
     * 當載入完佈局xml的時候會執行該方法,所以執行該方法 的時候就能夠知道當前的ViewGroup
     * 有多少個子View,但是此時並不知道子View的寬高是多少,因為還沒有測量
     */
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        redView = getChildAt(0);
        blueView = getChildAt(1);
        yellowView = getChildAt(2);
    }
  • 測量自己和子控制元件的寬高(這個不是必須的)
    /**
     * 測量自己和子控制元件的寬高
     * MeasureSpec: 測量規則,由size和mode組成
     * size:表示的是具體的大小值
     * mode:測量模式      封裝的是我們在佈局xml中的寬高引數
     *
     * MeasureSpec.AT_MOST: 對應的是wrap_content;
     * MeasureSpec.EXACTLY: 對應的是具體的dp值,match_parent;
     * MeasureSpec.UNSPECIFIED: 未定義的,一般只在adapter的測量中用到
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //構建測量規則
        //測量紅孩子
        int measureSpec = MeasureSpec.makeMeasureSpec(redView.getLayoutParams().width,MeasureSpec.EXACTLY);
//      redView.measure(measureSpec, measureSpec);
//      //測量藍精靈
//      blueView.measure(measureSpec, measureSpec);

        //更加簡單的測量子View的方法是這樣的:
        measureChild(redView, widthMeasureSpec, heightMeasureSpec);
        measureChild(blueView, widthMeasureSpec, heightMeasureSpec);
    }
  • 放置控制元件(這個是控制元件的初始位置,必須要有)
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int left = 0;
        int top = 0;

        redView.layout(left,top,left+redView.getMeasuredWidth(), top+redView.getMeasuredHeight());

        blueView.layout(left,redView.getBottom(),left+blueView.getMeasuredWidth(), redView.getBottom()+blueView.getMeasuredHeight());

        yellowView.layout(left,blueView.getBottom(),left+yellowView.getMeasuredWidth(), blueView.getBottom()+yellowView.getMeasuredHeight());

        //將某個子View提到最上面
//      bringChildToFront(redView);
    }
  • 攔截事件(這裡道長整合了NineOldAndroid並重寫了Callback類中的方法)
  @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        // 讓viewDragHelper幫助我們判斷是否應該攔截
        boolean result = viewDragHelper.shouldInterceptTouchEvent(ev);
        return result;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //將TouchEvent傳遞給viewDragHelper來處理
        viewDragHelper.processTouchEvent(event);
        return true;
    }

    private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
        /**
         * 是否捕獲view的觸控
         * child: 表示當前所觸控的子VIew
         * return: true:會捕獲      false:不會捕獲,即忽略
         */
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            return child==blueView || child==redView || child==yellowView;
        }
        /**
         * 當View被捕獲的時候會回撥
         */
        @Override
        public void onViewCaptured(View capturedChild, int activePointerId) {
            super.onViewCaptured(capturedChild, activePointerId);
//          Log.e("tag", "onViewCaptured");
        }
        /**
         * 獲取view水平方向拖拽範圍,但是目前並不起作用,但是最好還要實現下,不要
         * 返回0,它目前返回的值會用在計算view釋放移動的動畫時間計算上面
         */
        @Override
        public int getViewHorizontalDragRange(View child) {
            return DragLayout.this.getMeasuredWidth()-blueView.getMeasuredWidth();
        }
        /**
         * 控制child在水平方向的移動
         * child:當前所觸控的子View
         * left:表示ViewDragHelper幫你計算好的child的最終要變成的left值, left=child.getLeft()+dx
         * dx:表示本次水平移動的距離
         * return: 表示我們真正想讓child的left變成的值
         */
        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            if(left<0){
                left = 0;
            }
            return left;
        }
        /**
         * 控制child在垂直方向的移動
         * child:當前所觸控的子View
         * top:表示ViewDragHelper幫你計算好的child的最終要變成的top值, top=child.getTop()+dy
         * dy:表示本次垂直移動的距離
         * return: 表示我們真正想讓child的top變成的值
         */
        public int clampViewPositionVertical(View child, int top, int dy) {
            if(top<0){
                top = 0;
            }
            return top;
        }

        /**
         * 當view位置改變的回撥,一般用來實現view的伴隨移動
         * changedView:表示當前位置改變了的view
         * left:changedView的最新的left
         * top:changedView的最新的top
         * dx:changedView本次水平移動距離
         * dy:changedView本次垂直移動距離
         */
        @Override
        public void onViewPositionChanged(View changedView, int left, int top,
                                          int dx, int dy) {
            super.onViewPositionChanged(changedView, left, top, dx, dy);
            if(changedView==blueView){
                //讓redView跟隨移動
                redView.layout(redView.getLeft()+dx,redView.getTop()+dy, redView.getRight()+dx, redView.getBottom()+dy);
            }else if (changedView==redView) {
                //讓blueView跟隨移動
                blueView.layout(blueView.getLeft()+dx,blueView.getTop()+dy, blueView.getRight()+dx, blueView.getBottom()+dy);
            } else if (changedView==yellowView){
                //讓redView跟隨移動
                redView.layout(redView.getLeft()+dx,redView.getTop()+dy, redView.getRight()+dx, redView.getBottom()+dy);
                //讓blueView跟隨移動
                blueView.layout(blueView.getLeft()+dx,blueView.getTop()+dy, blueView.getRight()+dx, blueView.getBottom()+dy);
            }

            //1.計算移動的百分比
            int maxLeft = DragLayout.this.getMeasuredWidth()-blueView.getMeasuredWidth();
            float fraction = changedView.getLeft()*1f/maxLeft;
            //2.根據移動的百分比執行很多的伴隨動畫
            executeAnim(fraction);
        }
        /**
         * 當View釋放的時候執行,就是touch_up
         * releasedChild:當前抬起的子VIew
         * xvel:x方向移動的速度
         * yvel:y方向移動的速度
         */
        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            super.onViewReleased(releasedChild, xvel, yvel);
//          Log.e("tag", "xvel:"+xvel  +  "  yvel: "+yvel);
            //首先算出在正中間的left
            int centerLeft = getMeasuredWidth()/2-releasedChild.getMeasuredWidth()/2;
            if(releasedChild.getLeft()<centerLeft){
                //說明在左半邊
                viewDragHelper.smoothSlideViewTo(releasedChild,0,releasedChild.getTop());
                ViewCompat.postInvalidateOnAnimation(DragLayout.this);

//              scroller.startScroll(startX, startY, dx, dy, duration);
//              invalidate();
            }else {
                //說明在右半邊
                int finalLeft = getMeasuredWidth()-releasedChild.getMeasuredWidth();
                viewDragHelper.smoothSlideViewTo(releasedChild,finalLeft,releasedChild.getTop());
                ViewCompat.postInvalidateOnAnimation(DragLayout.this);
            }
        }
    };
  • 執行動畫方法
    /**
     * 執行動畫
     * @param fraction
     */
    private void executeAnim(float fraction){
        //旋轉
//        blueView.setRotation(360*fraction);//設定旋轉的角度
//      blueView.setRotationX(360*fraction);//設定圍繞x軸旋轉的角度
        blueView.setRotationY(360*fraction);//設定圍繞Y軸旋轉的角度

        //使用NineOldAndroid中的方法
        ViewHelper.setRotation(redView, 360*fraction);//設定旋轉的角度
//        ViewHelper.setScaleX(redView, 1+fraction*0.5f);
//        ViewHelper.setScaleY(redView, 1+fraction*0.5f);

        //進行顏色的過度變化
        redView.setBackgroundColor((Integer) ColorUtil.evaluateColor(fraction, Color.RED,Color.GREEN));

    }

到這裡就把View定義好了,當然這個View可以放到其他介面中,通過動畫自定義View的好處是很明顯的,就是實現簡單靈活。希望這篇部落格可以給你一些幫助。

原始碼下載

AnimDemo


相關文章