[大白裝逼]自定義YCardLayout

屁話不多說辆苔,先上個效果圖先

GIF動畫錄制工具20180317161745.gif

將此控件放到RecyclerView中算灸,并自定義LayoutManager可以有這樣的效果


GIF動畫錄制工具20180317162426.gif

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>

代碼中進行操作

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

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

        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();
            }
        });

實現(xiàn)步驟

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

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

實現(xiàn)移動的動畫,還用移動時的旋轉(zhuǎn)

 @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (!isRemove && moveAble && isInit && !isRunAnim) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    //獲取點擊時的數(shù)據(jù),并存起來
                    cacheX = event.getRawX();
                    cacheY = event.getRawY();
                    downX = event.getRawX();
                    downY = event.getRawY();
                    if (firstPoint == null) {//這個正常情況不會執(zhí)行,在這里只是以防萬一
                        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);//控制控件的旋轉(zhuǎn)
                            if (onYCardMoveListener != null) {
                                onYCardMoveListener.onMove(this, moveProgress);//觸發(fā)移動的監(jiān)聽器
                            }
                        }
                        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)) {//移動結(jié)束事件
                                return true;
                            }
                        }
                        animToReBack(this, firstPoint);//復(fù)位
                        return true;
                    }
                    break;
            }
        }
        return false;
    }

加入移動后的復(fù)位動畫

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

/**
     * 復(fù)位動畫
     * @param view
     * @param point 復(fù)位的位置
     */
    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();//開始復(fù)位動畫
    }

控件里的所有動畫都通過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;
    }

到這里,控件就可以移動和復(fù)位了,到了刪除動畫的實現(xiàn)了

刪除動畫

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

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

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

其中remove方法實現(xiàn)為

/**
     * 移除控件并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();
    }

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

 /**
     * 計算移除動畫終點
     * @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());
    }

而移除的動畫,內(nèi)部其實也是調(diào)用了getAnimToMove(),只是傳入的旋轉(zhuǎn)度為當前的旋轉(zhuǎn)度,且透明度變化結(jié)束為0

到這里控件已經(jīng)可以有移除動畫了,但是會發(fā)現(xiàn)控件內(nèi)的子控件的點擊事件沒有了,所以這里需要解決點擊事件的沖突

解決點擊事件沖突

需要在onInterceptTouchEvent中,對事件進行分發(fā)處理,在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就基本結(jié)束了,接下來就是與RecyclerView的結(jié)合了,結(jié)合之前要加個重置方法,用于重置控件數(shù)據(jù),因為RecyclerView有復(fù)用的功能,不重置會被其他本控件影響

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

結(jié)合RecyclerView

自定義LayoutManager

當然這里的Manager只是做示范作用,實際中可能會出現(xiàn)問題

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進行相應(yīng)的修改.目前使用時,在添加數(shù)據(jù)時需要使用notifyDataSetChanged()來進行刷新,刪除時需要使用notifyItemRemoved(position)和notifyDataSetChanged()一起刷新,不然可能出現(xiàn)問題.

The End

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末驻啤,一起剝皮案震驚了整個濱河市菲驴,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌街佑,老刑警劉巖谢翎,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異沐旨,居然都是意外死亡森逮,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進店門磁携,熙熙樓的掌柜王于貴愁眉苦臉地迎上來褒侧,“玉大人,你說我怎么就攤上這事∶乒” “怎么了烟央?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長歪脏。 經(jīng)常有香客問我疑俭,道長,這世上最難降的妖魔是什么婿失? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任钞艇,我火速辦了婚禮,結(jié)果婚禮上豪硅,老公的妹妹穿的比我還像新娘哩照。我一直安慰自己,他們只是感情好懒浮,可當我...
    茶點故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布飘弧。 她就那樣靜靜地躺著,像睡著了一般砚著。 火紅的嫁衣襯著肌膚如雪次伶。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天赖草,我揣著相機與錄音学少,去河邊找鬼。 笑死秧骑,一個胖子當著我的面吹牛版确,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播乎折,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼绒疗,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了骂澄?” 一聲冷哼從身側(cè)響起吓蘑,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎坟冲,沒想到半個月后磨镶,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡健提,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年琳猫,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片私痹。...
    茶點故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡脐嫂,死狀恐怖统刮,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情账千,我是刑警寧澤侥蒙,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站匀奏,受9級特大地震影響鞭衩,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜攒射,卻給世界環(huán)境...
    茶點故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一醋旦、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧会放,春花似錦、人聲如沸钉凌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽御雕。三九已至矢沿,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間酸纲,已是汗流浹背捣鲸。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留闽坡,地道東北人栽惶。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像疾嗅,于是被迫代替她去往敵國和親外厂。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,802評論 2 345

推薦閱讀更多精彩內(nèi)容