[大白裝逼]自定義YCardLayout

屁話不多說,先上個效果圖先
這裏寫圖片描述

將此控件放到RecyclerView中,並自定義LayoutManager可以有這樣的效果
這裏寫圖片描述

github:https://github.com/lewis-v/YCardLayout

使用方式

添加依賴

Add it in your root build.gradle at the end of repositories:


    allprojects {
        repositories {
            ...
            maven { url 'https://jitpack.io' }
        }
    }

Add the dependency

    dependencies {
            compile 'com.github.lewis-v:YCardLayout:1.0.1'
    }

在佈局中使用

  <com.lewis_v.ycardlayoutlib.YCardLayout
        android:id="@+id/fl"
        android:layout_marginTop="20dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">
        <ImageView
            android:id="@+id/img"
            android:layout_margin="5dp"
            android:src="@mipmap/ic_launcher"
            android:layout_width="200dp"
            android:layout_height="200dp" />
    </com.lewis_v.ycardlayoutlib.YCardLayout>

代碼中進行操作

控件中已有默認的配合參數,所以可以直接使用,不進行配置

yCardLayout = findViewById(R.id.fl);
        //yCardLayout.setMaxWidth(yCardLayout.getWidth());//設置最大移動距離
        //yCardLayout.setMoveRotation(45);//最大旋轉角度
        //yCardLayout.reset();//重置數據

        img = findViewById(R.id.img);
        img.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                yCardLayout.removeToLeft(null);
                Toast.makeText(MainActivity.this,"點擊11",Toast.LENGTH_SHORT).show();
            }
        });

實現步驟

自定義控件繼承於Framelayout及初始化

public class YCardLayout extends FrameLayout {
public void init(Context context){
        setClickable(true);
        setEnabled(true);
        minLength = ViewConfiguration.get(context).getScaledTouchSlop();//獲取設備最小滑動距離
        post(new Runnable() {
            @Override
            public void run() {
                maxWidth = getWidth();//默認移動最大距離爲控件的寬度,這裏的參數用於旋轉角度的變化做參照
                firstPoint = new Point((int) getX(),(int)getY());//獲取初始位置
                isInit = true;
            }
        });
    }
}

實現移動的動畫,還用移動時的旋轉

 @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (!isRemove && moveAble && isInit && !isRunAnim) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    //獲取點擊時的數據,並存起來
                    cacheX = event.getRawX();
                    cacheY = event.getRawY();
                    downX = event.getRawX();
                    downY = event.getRawY();
                    if (firstPoint == null) {//這個正常情況不會執行,在這裏只是以防萬一
                        firstPoint = new Point((int) getX(), (int) getY());
                    }
                    return true;
                case MotionEvent.ACTION_MOVE:
                    if ((Math.abs(downX-event.getRawX()) > minLength || Math.abs(downY-event.getRawY()) > minLength)) {//只有大於最小滑動距離纔算移動了
                        float moveX = event.getRawX();
                        float moveY = event.getRawY();

                        if (moveY > 0) {
                            setY(getY() + (moveY - cacheY));//移動Y軸
                        }
                        if (moveX > 0) {
                            setX(getX() + (moveX - cacheX));//移動X軸
                            float moveLen = (moveX - downX) / maxWidth;
                            int moveProgress = (int) ((moveLen) * 100);//移動的距離佔整個控件的比例moveProgress%
                            setRotation((moveLen) * 45f);//控制控件的旋轉
                            if (onYCardMoveListener != null) {
                                onYCardMoveListener.onMove(this, moveProgress);//觸發移動的監聽器
                            }
                        }
                        cacheX = moveX;
                        cacheY = moveY;
                    }
                    return false;
                case MotionEvent.ACTION_UP:
                    if ((Math.abs(downX-event.getRawX()) > minLength || Math.abs(downY-event.getRawY()) > minLength)) {//移動了才截獲這個事件
                        int moveEndProgress = (int) (((event.getRawX() - downX) / maxWidth) * 100);
                        if (onYCardMoveListener != null) {
                            if (onYCardMoveListener.onMoveEnd(this, moveEndProgress)) {//移動結束事件
                                return true;
                            }
                        }
                        animToReBack(this, firstPoint);//復位
                        return true;
                    }
                    break;
            }
        }
        return false;
    }

加入移動後的復位動畫

上面的代碼調用了animToReBack(this, firstPoint);來進行復位

/**
     * 復位動畫
     * @param view
     * @param point 復位的位置
     */
    public void animToReBack(View view,Point point){
        AnimatorSet animatorSet = getAnimToMove(view,point,0,getAlpha());//獲取動畫
        isRunAnim = true;//動畫正在運行的標記
        animatorSet.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {

            }

            @Override
            public void onAnimationEnd(Animator animation) {
                isRunAnim = false;
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                isRunAnim = false;
            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });
        animatorSet.start();//開始復位動畫
    }

控件裏的所有動畫都通過getAnimToMove來獲取,getAnimToMove的代碼爲

 /**
     * 移動動畫
     * @param view
     * @param point
     * @param rotation
     */
    public AnimatorSet getAnimToMove(View view, Point point, float rotation,float alpha){
        ObjectAnimator objectAnimatorX = ObjectAnimator.ofFloat(view,"translationX",point.x);
        ObjectAnimator objectAnimatorY = ObjectAnimator.ofFloat(view,"translationY",point.y);
        ObjectAnimator objectAnimatorR = ObjectAnimator.ofFloat(view,"rotation",rotation);
        ObjectAnimator objectAnimatorA = ObjectAnimator.ofFloat(view,"alpha",alpha);
        AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.playTogether(objectAnimatorR,objectAnimatorX,objectAnimatorY,objectAnimatorA);
        return animatorSet;
    }

到這裏,控件就可以移動和復位了,到了刪除動畫的實現了

刪除動畫

刪除動畫有左邊的右邊刪除,刪除的移動軌跡,需要與滑動方向相關,這樣看起來的效果才比較好
這裏寫了兩個方法,供刪除時調用

/**
     *  向左移除控件
     * @param removeAnimListener
     */
    public void removeToLeft(RemoveAnimListener removeAnimListener){
        remove(true,removeAnimListener);
    }

    /**
     * 向右移除控件
     * @param removeAnimListener
     */
    public void removeToRight(RemoveAnimListener removeAnimListener){
        remove(false,removeAnimListener);
    }

其中remove方法實現爲

/**
     * 移除控件並notify
     * @param isLeft 是否是向左
     * @param removeAnimListener
     */
    public void remove(boolean isLeft, final RemoveAnimListener removeAnimListener){
        isRemove = true;
        final Point point = calculateEndPoint(this,this.firstPoint,isLeft);//計算終點座標
        AnimatorSet animatorSet = getReMoveAnim(this,point,getRemoveRotation(this,this.firstPoint,isLeft));//獲取移除動畫
        animatorSet.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
                if (removeAnimListener != null){
                    removeAnimListener.OnAnimStart(YCardLayout.this);
                }
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                if (removeAnimListener != null){
                    removeAnimListener.OnAnimEnd(YCardLayout.this);
                }
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                Log.e("cancel","");
                reset();
                if (removeAnimListener != null){
                    removeAnimListener.OnAnimCancel(YCardLayout.this);
                }
            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });
        animatorSet.start();
    }

在動畫開始/結束/取消懂提供了回調,當然不需要時傳入null就行了
其中調用計算終點座標的方法,這個不好解釋,看看計算過程,詳細的就不說了

 /**
     * 計算移除動畫終點
     * @param view
     * @param point
     * @param isLeft
     * @return
     */
    public Point calculateEndPoint(View view, Point point, boolean isLeft){
        Point endPoint = new Point();
        if (isLeft) {
            endPoint.x = point.x - (int) (view.getWidth() * 1.5);
        }else {
            endPoint.x = point.x + (int) (view.getWidth() * 1.5);
        }
         if (Math.abs(view.getX() - point.x) < minLength &&Math.abs (view.getY()-point.y) < minLength){//還在原來位置
            endPoint.y = point.y + (int)(view.getHeight()*1.5);
        }else {
            int endY = getEndY(view,point);
            if (isLeft) {
                endPoint.y = (int) view.getY() - endY;
            }else {
                endPoint.y = (int)view.getY() + endY;
            }
        }
        return endPoint;
    }

    /**
     * 獲取終點Y軸與初始位置Y軸的距離
     * @param view
     * @param point
     * @return
     */
    public int getEndY(View view,Point point){
        return (int) ((point.y-view.getY())/(point.x-view.getX())*1.5*view.getWidth());
    }

而移除的動畫,內部其實也是調用了getAnimToMove(),只是傳入的旋轉度爲當前的旋轉度,且透明度變化結束爲0

到這裏控件已經可以有移除動畫了,但是會發現控件內的子控件的點擊事件沒有了,所以這裏需要解決點擊事件的衝突

解決點擊事件衝突

需要在onInterceptTouchEvent中,對事件進行分發處理,在down和up不截獲,在move中選擇性截獲

 @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted = super.onInterceptTouchEvent(ev);
        if (!isInit || isRunAnim){
            return false;
        }
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                downX = ev.getRawX();
                downY = ev.getRawY();
                cacheX = ev.getRawX();
                cacheY = ev.getRawY();
                if (firstPoint == null){
                    firstPoint = new Point((int) getX(),(int) getY());
                }
                intercepted = false;
                break;
            case MotionEvent.ACTION_MOVE:
                if ((Math.abs(downX-ev.getRawX()) > minLength || Math.abs(downY-ev.getRawY()) > minLength) && !isRemove && moveAble){
                    intercepted = true;
                }else {
                    intercepted = false;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercepted = false;
                break;
        }
        return intercepted;
    }

到這裏YCardLayout就基本結束了,接下來就是與RecyclerView的結合了,結合之前要加個重置方法,用於重置控件數據,因爲RecyclerView有複用的功能,不重置會被其他本控件影響

 /**
     * 重置數據
     */
    public void reset(){
        if (firstPoint != null) {
            setX(firstPoint.x);
            setY(firstPoint.y);
        }
        isRemove = false;
        moveAble = true;
        setRotation(0);
        setAlpha(1);
    }

結合RecyclerView

自定義LayoutManager

當然這裏的Manager只是做示範作用,實際中可能會出現問題

public class YCardLayoutManager extends RecyclerView.LayoutManager {
    public static final String TAG = "YCardLayoutManager";


    @Override
    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
        return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT,
                RecyclerView.LayoutParams.WRAP_CONTENT);
    }



    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (getItemCount() == 0) {//沒有Item,界面空着吧
            detachAndScrapAttachedViews(recycler);
            return;
        }
        if (getChildCount() == 0 && state.isPreLayout()) {//state.isPreLayout()是支持動畫的
            return;
        }
        detachAndScrapAttachedViews(recycler);
        setChildren(recycler);
    }

    public void setChildren(RecyclerView.Recycler recycler){
        for (int i = getItemCount()-1; i >= 0; i--) {
            View view = recycler.getViewForPosition(i);
            addView(view);
            measureChildWithMargins(view,0,0);
            calculateItemDecorationsForChild(view,new Rect());
            int width = getDecoratedMeasurementHorizontal(view);
            int height = getDecoratedMeasurementVertical(view);
            layoutDecoratedWithMargins(view,0,0,width,height);
        }
    }

    /**
     * 獲取某個childView在水平方向所佔的空間
     *
     * @param view
     * @return
     */
    public int getDecoratedMeasurementHorizontal(View view) {
        final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
                view.getLayoutParams();
        return getPaddingRight()+getPaddingLeft()+getDecoratedMeasuredWidth(view) + params.leftMargin
                + params.rightMargin;
    }

    /**
     * 獲取某個childView在豎直方向所佔的空間
     *
     * @param view
     * @return
     */
    public int getDecoratedMeasurementVertical(View view) {
        final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
                view.getLayoutParams();
        return getPaddingTop()+getPaddingBottom()+getDecoratedMeasuredHeight(view) + params.topMargin
                + params.bottomMargin;
    }
}

然後在RecyclerView中使用YCardLayoutManager加上YCardLayout就能有最開始第二個動圖那樣的效果,但這裏主要是自定義YCardLayout,在與RecyclerView使用的時候還需要對YCardLayoutManager進行相應的修改.目前使用時,在添加數據時需要使用notifyDataSetChanged()來進行刷新,刪除時需要使用notifyItemRemoved(position)和notifyDataSetChanged()一起刷新,不然可能出現問題.

The End

在自定義這個控件中,主要是解決了點擊事件的衝突,移除動畫的終點計算,還有其他的衝突問題,這裏的與RecyclerView的結合使用,其中使用的LayoutManager還有一些問題,將在完善後再加入到GitHub中.最後推薦本書《Android開發藝術探索》,這書還是挺不錯的,這裏解決點擊事件衝突的也是在此書中看來的…

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章