交互控件淺解析,安卓View帶入門

博主是愛奇藝員工愉阎,以上幾個都是從愛 奇藝泡泡客戶端中截取的绞蹦。

本文中一共舉出了四個栗子:內(nèi)容由簡到難,但是分析方法和基本原理都是相似的榜旦。
本文四個控件的代碼都是筆者自己手寫的幽七。希望可以給自己留下些筆記,也給后來者一些啟發(fā)溅呢。

一. 下拉回彈控件 + 收起

device-2017-11-25-120023.mp4_1511582458.gif

功能點分析

  • 下拉手勢判定 + View位移
  • 松手之后 + View位移

View位移推薦使用translationY, 建議在做位移操作時不要直接調(diào)用View.setTranslationY()
而是應(yīng)該封裝一個統(tǒng)一的方法

 public float getCurrentOffset(){
        return getTranslationY();
    }


    public void setOffset(float targetScrollX){
        //標(biāo)準(zhǔn)坐標(biāo)軸 右下為正
        //進行左右平移時澡屡,需要保證平移的scrollX 范圍是 0 - mRefreshView.width()
        targetScrollX = checkOffsetX(targetScrollX);

//        scrollTo(0,(int)targetScrollX);
        setTranslationY(targetScrollX);
    }

    private float checkOffsetX(float target) {
        if(target > getMaxOffset() /*|| Math.abs(target - getMaxOffset()) < 10*/){
            target = getMaxOffset();
        }else if(target < 0){
            target = 0;
        }
        return target;
    }

這樣的好處是:如果希望修改一種位移方式(例如使用ScrollTo)時,所做的修改量很小咐旧。

核心的事件處理部分:


/*相關(guān)變量*/


    private float mTouchSlop;//最小位移
    /*上一次的點擊位置*/
    private float mXDown;
    private float mYDown;

  
    private float mYLastMove;//上一次move事件的Y坐標(biāo)
    private float mYMove;


  @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if(mHandler!=null && mHandler.shouldForbidden() || ev.getPointerCount() > 1){
            return super.onInterceptTouchEvent(ev);
        }

        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mXDown = ev.getRawX();
                mYDown = ev.getRawY();
                mYLastMove = mYDown;
                break;

            case MotionEvent.ACTION_MOVE:
                mXMove = ev.getRawX();
                mYMove = ev.getRawY();
                float diffX = (mXMove - mXDown);
                float diffY = (mYMove - mYDown);

                if(Math.abs(diffY) < mTouchSlop || Math.abs(diffY * 0.5) < Math.abs(diffX)){// 過濾掉水平方向的手勢
                    break;
                }

                mYLastMove = mYMove;
                return true;
        }
        return super.onInterceptTouchEvent(ev);
    }

    public float getMaxOffset(){
        return mTargetView.getHeight();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                mYMove = event.getRawY();

                float deltaY =  1.2f * (mYMove - mYLastMove);//正規(guī)坐標(biāo)軸下的偏移
                setOffset(getCurrentOffset() + deltaY);
                mYLastMove = mYMove;
                break;
            case MotionEvent.ACTION_UP:
                // 當(dāng)手指抬起時驶鹉,根據(jù)當(dāng)前的滾動值來判定應(yīng)該滾動到哪個子控件的界面
                onRelease();
                break;
        }
        return true;
    }

    public float getCurrentOffset(){
        return getTranslationY();
    }

整體思路還是按照View的動作攔截機制完成的。
在onInterceptTouchEvent進行動作判別铣墨、攔截室埋。
在onTouchEvnet中完成偏移量計算、View的位移伊约、以及回彈動畫的播放姚淆。

回彈動畫

  public void onRelease(){
        final boolean hasGotPoint = Math.abs(getCurrentOffset()) >= mTriggerPoint;
        mAnimator = ValueAnimator.ofFloat(getCurrentOffset(), hasGotPoint? getMaxOffset() : 0).setDuration(ANIMATOR_DURATION);
        mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float animatedValue = (float)animation.getAnimatedValue();
                setOffset(animatedValue);
            }
        });
        mAnimator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {

            }

            @Override
            public void onAnimationEnd(Animator animation) {
                    //todo 進入詳情頁
                    if(mListener!=null && hasGotPoint) {
                        mListener.onTriggered();
                    }
            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });
        mAnimator.start();
    }

二. 視頻縮放 + View動畫

device-2017-11-25-120156.mp4_1511582541.gif

這個效果看起來稍微復(fù)雜,但是基本實現(xiàn)思路是類似的
1.找到合適的動作觸發(fā)時機
2.對View進行操作

除此之外還有幾個點需要注意:

1.從上圖可以看到視頻的主要形態(tài)有三種屡律,100%腌逢,80%以及隱藏。狀態(tài)的跳轉(zhuǎn)需要記錄超埋。
由于這個view的動畫基本上是只要觸發(fā)就會進行下去的搏讶。

  1. 內(nèi)部還有個ListView佳鳖。需要處理好和ListView的沖突。

3.另外媒惕,由于動作幾乎是立即觸發(fā)并且不可逆的(施加動作之后就會執(zhí)行形變)
所以系吩,我們只在onInterceptTouchEvnet中就可以完成主要邏輯了。

   @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (listView == null || videoLayout == null) {//子控件還未初始化
            return super.onInterceptTouchEvent(ev);
        }
        if (!enable) {//禁用開關(guān)
            return super.onInterceptTouchEvent(ev);
        }

        //操作區(qū)域在listView以上,即視頻區(qū)域內(nèi)
        int y = (int) ev.getRawY();
        int x = (int) ev.getRawX();

        int[] location = new int[2];
        listView.getLocationOnScreen(location);
        if (y < location[1]) {
            return super.onInterceptTouchEvent(ev);
        }

        if (isAnimationPlaying) {
            return true;//動畫播放期間禁止操作
        }

        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 發(fā)生down事件時,記錄y坐標(biāo)
                mLastMotionY = y;
                mLastMotionX = x;
                break;

            case MotionEvent.ACTION_MOVE:
                deltaY = y - mLastMotionY;
                if (Math.abs(deltaY) < 20) {
                    break;
                }
                if (!isVideoStop() && isListViewTopping()) {
                    //非暫停態(tài)
                    if (deltaY < 0 && videoState == VIDEO_LAYOUT_NORMAL_SIZE) {
                        zoomInVideoLayout(VIDEO_LAYOUT_HALF_SIZE);
                        return true;
                    } else if (deltaY > 0 && videoState != VIDEO_LAYOUT_NORMAL_SIZE) {
                        zoomInVideoLayout(VIDEO_LAYOUT_NORMAL_SIZE);
                        return true;
                    }
                }

                if (isVideoStop()) {
                    if (deltaY < 0 && videoState != VIDEO_LAYOUT_ZERO_SIZE) {
                        zoomInVideoLayout(VIDEO_LAYOUT_ZERO_SIZE);
                        return true;
                    } else if (deltaY > 0 && isListViewTopping()) {
                        if (videoState == VIDEO_LAYOUT_ZERO_SIZE) {
                            zoomInVideoLayout(VIDEO_LAYOUT_NORMAL_SIZE);
                            return true;
                        }
                    }
                }
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }

三. 左拉刷新

從原理上來講吓笙,這個控件其實和常見的下拉刷新控件是一樣的淑玫。只是方向變?yōu)榱讼蜃蠡瑒印?/p>

device-2017-11-25-224157.mp4_1511620973.gif

完全從零做起的,實現(xiàn)一個這個小控件也是挺有意思的面睛。

主要思路是絮蒿,在視覺區(qū)域以外的地方添加一個新View(indicate 刷新狀態(tài))
主要動作是對整個View做位移動畫。

  @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        mTargetView.layout(l,t,r,b);//在此栗子中是圖片
        mRefreshView.layout(r,t,r + mRefreshView.getMeasuredWidth(),b);//左拉提示叁鉴,旋轉(zhuǎn)指示等
    }

而動作判別又是我們熟悉的那一套代碼啦

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if(mHandler!=null && mHandler.shouldForbidden()){
            return super.onInterceptTouchEvent(ev);
        }

        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mXDown = ev.getRawX();
                mYDown = ev.getRawY();
                mXLastMove = mXDown;
                break;
            case MotionEvent.ACTION_MOVE:
                mXMove = ev.getRawX();
                mYMove = ev.getRawY();
                float diffX = (mXMove - mXDown);
                float diffY = (mYMove - mYDown);

                if(Math.abs(diffX * 0.5) < Math.abs(diffY)){
                    break;
                }

                mXLastMove = mXMove;
                // 當(dāng)手指拖動值大于TouchSlop值時土涝,認(rèn)為應(yīng)該進行滾動,攔截子控件的事件
                //向左滑動
                if (diffX < 0  && Math.abs(diffX) > mTouchSlop && !canTargetScrollLeft()) {
                    return true;
                }else if(diffX > 0 && Math.abs(diffX) > mTouchSlop && isRefreshViewDisplayed()){
                    return true;
                }
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }



    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                mXMove = event.getRawX();

                float diffX =  1.6f * (mXMove - mXLastMove);//正規(guī)坐標(biāo)軸下的偏移
                diffX = diffX * (1.2f - (getCurrentOffset()/getMaxOffset()));//阻尼修正

                float target = checkOffsetX(getCurrentOffset()- diffX);


                if(getMaxOffset() * mPercentFactor < target){
                    mRefreshView.setExplodeState(true);//爆炸特效 + 提示轉(zhuǎn)換
                }else{
                    mRefreshView.setExplodeState(false);
                }

                setOffset(target);
                mXLastMove = mXMove;
                break;
            case MotionEvent.ACTION_UP:
                // 當(dāng)手指抬起時幌墓,根據(jù)當(dāng)前的滾動值來判定應(yīng)該滾動到哪個子控件的界面
                //todo 進入詳情頁
                if(mListener!=null && mRefreshView.isHasExploded()) {
                    mListener.onTriggered();
                }
                postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        onRelease();
                    }
                }, mRefreshView.isHasExploded() ? 500 :0);
                break;
        }
        return super.onTouchEvent(event);
    }

主要動作核心代碼:

    public void setOffset(float targetScrollX){
        //標(biāo)準(zhǔn)坐標(biāo)軸 右下為正
        //進行左右平移時但壮,需要保證平移的scrollX 范圍是 0 - mRefreshView.width()
        targetScrollX = checkOffsetX(targetScrollX);
        float percent = (targetScrollX / getMaxOffset())/ mPercentFactor;
        percent = Math.min(percent,1);
        mRefreshView.updatePullPercent(percent);
        scrollTo((int)targetScrollX,0);
    }

    private float checkOffsetX(float targetScrollX) {
        if(targetScrollX > mRefreshView.getWidth() /*|| Math.abs(targetScrollX - getMaxOffset()) < 10*/){
            targetScrollX = mRefreshView.getWidth();
        }else if(targetScrollX < 0){
            targetScrollX = 0;
        }
        return targetScrollX;
    }

被刷新的View被抽象出來作為mRefreshView,相對比較簡單常侣,只要實現(xiàn)了

void  updatePullPercent(float percent);
void setExploedState(boolean explored); 

這里除了問題提示之外蜡饵,還有一個
旋轉(zhuǎn)的箭頭以及漸變的綠色背景。

箭頭是現(xiàn)成的UI圖胳施,綠色背景稍微麻煩一些溯祸,需要使用顏色漸變來完成。

下面的RotateArrowView 實現(xiàn)了這個功能舞肆,順便將箭頭也add了進來焦辅。

//只包括了這個類的核心代碼
public class RotateArrowView extends FrameLayout {


    private ArgbEvaluator argbEvaluator = new ArgbEvaluator();

    ...

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int x = getMeasuredWidth()/2;
        int y = getMeasuredHeight()/2;
        int radius = getWidth()/2;
        canvas.drawCircle(x,y,radius,mPaint);
    }

    public void updatePercent(float percent){
        int evaluateColor = (int)argbEvaluator.evaluate(percent, startColor, endColor);
        mPaint.setColor(evaluateColor);
        arrow.setRotation(180* percent);//箭頭的角度需要旋轉(zhuǎn)
        postInvalidate();
    }
}

ArgbEvaluator 是谷歌提供的一個方便的顏色漸變計算器。

之前對ViewGroup在直覺上有個誤解椿胯,就是復(fù)寫父view的onDraw要考慮和子View z-index上的層級關(guān)系筷登。
實際上ViewGroup的onDraw復(fù)寫之后,并不會影響到其子View(只是默默地在最后面畫了一個背景)哩盲。

其實思考一下也是前方,父View以及子View的z-index層級關(guān)系是在layout時就已經(jīng)確定好的。如果需要在onDraw再去費心考慮种冬,對于api使用者而言是一個災(zāi)難镣丑。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市娱两,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌金吗,老刑警劉巖十兢,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件趣竣,死亡現(xiàn)場離奇詭異,居然都是意外死亡旱物,警方通過查閱死者的電腦和手機遥缕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來宵呛,“玉大人单匣,你說我怎么就攤上這事”λ耄” “怎么了户秤?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長逮矛。 經(jīng)常有香客問我鸡号,道長,這世上最難降的妖魔是什么须鼎? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任鲸伴,我火速辦了婚禮,結(jié)果婚禮上晋控,老公的妹妹穿的比我還像新娘汞窗。我一直安慰自己,他們只是感情好赡译,可當(dāng)我...
    茶點故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布仲吏。 她就那樣靜靜地躺著,像睡著了一般捶朵。 火紅的嫁衣襯著肌膚如雪蜘矢。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天综看,我揣著相機與錄音品腹,去河邊找鬼。 笑死红碑,一個胖子當(dāng)著我的面吹牛舞吭,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播析珊,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼羡鸥,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了忠寻?” 一聲冷哼從身側(cè)響起惧浴,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎奕剃,沒想到半個月后衷旅,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體捐腿,經(jīng)...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年柿顶,在試婚紗的時候發(fā)現(xiàn)自己被綠了茄袖。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,722評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡嘁锯,死狀恐怖宪祥,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情家乘,我是刑警寧澤蝗羊,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站烤低,受9級特大地震影響肘交,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜扑馁,卻給世界環(huán)境...
    茶點故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一涯呻、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧腻要,春花似錦复罐、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至趟济,卻和暖如春乱投,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背顷编。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工戚炫, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人媳纬。 一個月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓双肤,卻偏偏與公主長得像,于是被迫代替她去往敵國和親钮惠。 傳聞我的和親對象是個殘疾皇子茅糜,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,614評論 2 353

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

  • 評《百鳥朝鳳》 有句老話叫:一生最多真?zhèn)鲀扇恕Uf的武術(shù)界素挽,為了本門派的名譽蔑赘、地位,將真本事教給最親信的徒弟以保門...
    三阿木閱讀 338評論 0 0
  • 學(xué)而時習(xí)之,不亦說乎米死?——孔丘《論語?學(xué)而》 單例模式的核心在于:** 確保一個實例锌历,并提供全局訪問贮庞。 ** 首先...
    編碼的哲哲閱讀 950評論 4 9
  • 自姑娘出生到現(xiàn)在峦筒,大大小小的家庭party搞了上十次。 7.2號窗慎,在貝好友的邀約下搞了一次音樂美食party物喷。好友...
    JC賈閱讀 155評論 0 1