Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 獵豹快切App中用到的Android開發技巧探索

獵豹快切App中用到的Android開發技巧探索

編輯:關於Android編程

前景提要:

什麼是塊切?

快切是從獵豹的Clear Master中分離出來的一個懸浮窗小工具。因為對這個比較感興趣,博主斷斷續續花了2個月時間完成了一個類似塊切的版本,起了個名字叫“Well Swipe”,中文名叫“Well 劃劃”。本文會針對Well 劃劃開發中遇到的一些坑和和技巧做一個分享。來給大家揭密塊切開發過程中用到的自定義控件技術細節。在這裡還有一個叫“單手劃劃”的app不得不說,也做的很好。

問題:

如何觸發菜單如何通過手勢控制菜單的旋轉,打開,關閉旋轉的過程中如何做到item循環展示拖動item效果拖動item時的排序效果item的過渡動畫(刪除一個item之後,剩余的item會自動平移到目標位置。拖動排序時item自動平移到排序之後的位置)控件之間如何交互(旋轉菜單的時候指示器跟著轉,拖動的時候角落菜單變化狀態,拖動到垃圾箱)重寫的onItemClick,onItemLongClick事件 帶著上述問題開始我們對Well 劃劃的探索之旅。   1.如何觸發菜單 在設備處於桌面或者其他app的情況下,從屏幕地步往外劃來觸發菜單。這個真沒有別的辦法,只能用WindowManager。因為你第三方app不可能拿到桌面或者任何其他app的事件來觸發你自己的app。我的做法是在屏幕地步畫了6個矩形。一邊3個,拼接出來兩個L型的區域,不多不少剛剛夠用,再在設置裡加上調整大小的自定義功能。塊切單手劃劃都是這麼搞的。   2.如何通過手勢控制菜單的旋轉,打開,關閉

打開:打開這個手勢在底部L型的觸發區域進行。設計的時候分左右。所以寫的時候也要分左右,當手指劃過一定距離之後就開始打開菜單,手指這個時候還沒停,手指繼續滑動的時候計算一個0-1的值用來控制菜單從小到大展開的效果。我設計了這樣的一個接口,把需要的scale值回傳到菜單view來使用

 

/**
 * Created by mingwei on 3/12/16.
 *
 *
 * 微博:     明偉小學生(http://weibo.com/u/2382477985)
 * Github:   https://github.com/gumingwei
 * CSDN:     http://blog.csdn.net/u013045971
 * QQ&WX:   721881283
 *
 *
 */
public interface OnScaleChangeListener {

    /**
     * 當scale發生變化的時候回傳這個值
     * 

* 1.用於在手指拖動時: CatchView.OnEdgeSlidingListener * 2.松開手指時自動打開和關閉的過程中: AngleLayout.OnOffListener * 3.點擊Back鍵關閉動畫的過程中 * <改變背景SwipeBackgroundLayout的透明度> * * @param scale */ void change(float scale); }


public interface OnEdgeSlidingListener extends OnScaleChangeListener {
        /**
         * 打開
         */
        void openLeft();//左邊打開

        void openRight();//右邊打開

        /**
         * true速度滿足自動打開
         * false速度不滿足根據抬手時的狀態來判斷是否打開
         * @param view
         * @param flag
         */
        void cancel(View view, boolean flag);

    }

滑動的過程中持續不斷的回傳一個scale值。

 

@Override
public void change(float scale) {
        if (mSwipeLayout.hasView()) {
            if (mSwipeLayout.isSwipeOff()) {
                mSwipeLayout.getAngleLayout().setAngleLayoutScale(scale);
                mSwipeLayout.setSwipeBackgroundViewAlpha(scale);
            }
        }
}

 

旋轉:菜單打開之後,介個時候,手指在菜單的父容器中滑動,回傳一個角度值,角度值通過三角函數就可以獲得到,菜單跟著旋轉。旋轉的處理要考慮菜單的打開方式是左邊還是右邊。後面還加了一個功能,手指往角落方向滑動時關閉菜單,所以還要處理華東時的角度問題。在滑動的過程中回傳角度值angle來旋轉菜單,松開手指後自動轉到目標角度。

 

public interface OnAngleChangeListener {

        /**
         * 角度發生變化時傳遞當前的所顯示的數據索引值&當前的百分比
         * 用於改變Indicator的選中狀態,百分比則用來渲染過渡效果
         *
         * @param cur 正在顯示的是數據index
         * @param p   百分比
         */
        void onAngleChanged(int cur, float p);

    }
回傳的角度用來旋轉菜單

 

 

@Override
public void onAngleChanged(int cur, float p) {
    mIndicator.onAngleChanged2(cur, p);
    mIndicatorTheme.changeStartAngle(cur, p);
}

關閉:關閉有點擊菜單外時,點擊角落X時,往角落滑動手指時分別都可以關閉菜單

1).點擊角落XX按鈕域時

 

@Override
    public void cornerEvent() {
        if (mEditState == STATE_EDIT) {
            setEditState(AngleLayout.STATE_NORMAL);
            return;
        }
        if (mEditState == STATE_NORMAL) {
            off();//點擊外部空白區域的時候關閉
        }
    }
2).點擊外部區域

 

 

if (mAngleView.isLeft()) {
                    float upDistance = (float) Math.sqrt(Math.pow((upX - 0), 2) + Math.pow((upY - mHeight), 2));
                    if (Math.abs(upX - mLastMotionX) < 8 && Math.abs(upY - mLastMotionY) < 8 &&
                            (upTime - mLastTime) < 200 && (upDistance > mAngleView.getMeasuredHeight())) {
                        if (mEditState == STATE_EDIT) {
                            setEditState(AngleLayout.STATE_NORMAL);
                        } else {
                            off();
                        }
                    }
                } else if (mAngleView.isRight()) {
                    float upDistance = (float) Math.sqrt(Math.pow((upX - mWidth), 2) + Math.pow((upY - mHeight), 2));
                    if (Math.abs(upX - mLastMotionX) < 8 && Math.abs(upY - mLastMotionY) < 8 &&
                            (upTime - mLastTime) < 200 && (upDistance > mAngleView.getMeasuredHeight())) {
                        if (mEditState == STATE_EDIT) {
                            setEditState(AngleLayout.STATE_NORMAL);
                        } else {
                            off();
                        }
                    }
                }
3).快速滑動結束後

 

 

if (MOVE_TYPE == TYPE_OFF && upTime - mLastTime < 400) {
     off();
}

3.旋轉的過程中如何做到item循環展示

 

如何做好循環展示?很簡單,每次轉90度之後刷新界面,把item重新排序。如初始值是1 2 3

第一次旋轉後 2 3 1

第二次旋轉後 3 1 2

第三次旋轉後 1 2 3 這時又回到了原位,每四次是一個循環,這樣規律久很容易總結出來了。

當前的數據索引index

 

/**
     * 根據index獲取當先index所需要的數據索引
     * 比如: 11->1,10->2,9->0,8->1,7->2,6->0  像這樣一直循環
     *
     * @param index 轉動結束後根據BaseAngle的值除以90得出的范圍0-11
     *              3,4的最小公倍數的是12
     * @return
     */
    private int getViewsIndex(int index) {
        return (COUNT_12 - index) % COUNT_3;
    }
上一組的索引

 

 

/**
     * 上一個數據索引
     *
     * @param index 傳入的是getViews()的返回值
     * @return
     */
    public int getPreViewsIndex(int index) {
        return index == 0 ? 2 : (index - 1);
    }
下一組索引

 

 

/**
     * 下一個數據索引
     *
     * @param index 傳入的是getViews()的返回值
     * @return
     */
    public int getNextViewsIndex(int index) {
        return index == 2 ? 0 : (index + 1);
    }
求出因該取那一組數據之後,還要求出哪個限象,即,把第X組數據放在第Y限象。然後一只重復起來就造成了循環的感覺

 

當前限象currentQua

 

/**
     * 根據當前的index獲取當前顯示限象index
     * 比如11->1,10->2,9->3,8->0
     *
     * @param index 轉動結束後根據BaseAngle的值除以90得出的范圍0-11
     *              3,4的最小公倍數的是12
     * @return
     */
    public int getQuaIndex(int index) {
        return index == 0 ? 0 : (COUNT_12 - index) % COUNT_4;
    }
上一個限象preQua

 

 

/**
     * 獲取當前index的上一個index
     *
     * @param index 傳入的是getIndex()的返回值
     * @return 得到上一個index
     */
    public int getPreQuaIndex(int index) {
        return index == 0 ? COUNT_3 : (index - 1);
    }
下一個限象nextQua

 

 

/**
     * 獲取當前index的下一個index
     *
     * @param index 傳入的是getIndex()的返回值
     * @return 得到下一個index
     */
    private int getNextQuaIndex(int index) {
        return index == COUNT_3 ? 0 : (index + 1);
    }
最後拿求得的數據喝限象來布局item

 

布局子item的位置

 

/**
     * 布局子控件
     * 通過一個當前值,計算上一個,下一個值
     *
     * @param index
     */
    private void itemLayout(int index) {
        mCurrentIndex = getRealIndex(index);
        itemLayout(mMap.get(getPreViewsIndex(getViewsIndex(index))), getPreQuaIndex(getQuaIndex(index)));//上一組
        itemLayout(mMap.get(getViewsIndex(index)), getQuaIndex(index));//當前組
        itemLayout(mMap.get(getNextViewsIndex(getViewsIndex(index))), getNextQuaIndex(getQuaIndex(index)));//下一組
    }

4.拖動item效果

 

拖動Item這個其實也沒啥難的,就是長按的時候根據按下的位置和item布局的時候求得位置來得到一個Item的對象,拿到item的數據,在父容器中創建一個view跟著手指移動就可以了。有人就問了,直接onItemLongClick不就可以了,為啥還要求啊,這個還真是要求的,控件角度發生變化後的onClick,onItem等事件觸發是有問題的,因為畫布發生了變化,點擊能找到子Item,但是找的不對,item已經轉走了,事件還可以觸發,這樣就不合適了不是,所以我們要重寫一系列事件。

拖拽的時候傳遞item的數據信息,包括視圖view,坐標信息,offsetLeft,offsetTop

 

public interface OnEditModeChangeListener {
        /**
         * 進入編輯模式
         *
         * @param view
         */
        void onEnterEditMode(View view);

        /**
         * 退出編輯模式
         */
        void onExitEditMode();

        /**
         * 進入拖拽模式
         *
         * @param view       判定進行拖拽的當前view
         * @param left       在父控件中的left值
         * @param top        在父控件中的top值
         * @param offsetLeft 觸摸點在當前進行拖拽的view的left距離
         * @param offsetTop  觸摸點在當前進行拖拽的view的top距離
         */
        void onStartDrag(AngleItemCommon view, float left, float top, float offsetLeft, float offsetTop);

        /**
         * 拖拽取消
         */
        void onCancelDrag();
    }
拖拽結束的時候調用

 

 

public interface OnItemDragListener {

        /**
         * 拖拽結束時調用
         *
         * @param index 返回當前的數據索引index
         */
        void onDragEnd(int index);
    }

5.拖動item時的排序效果

 

6.item的過渡動畫(刪除一個item之後,剩余的item會自動平移到目標位置。拖動排序時item自動平移到排序之後的位置)

5和6可以放在一起說,不管是刪除還是排序,基本上就是數據發生變化之後再做視圖View變換效果的過程,計算位置的算法即全面已經寫好的,在這裡取掉限象的計算就可以使用了。

例如:123456 這幾個數據初始化完成之後會有一個自身的位置。然後把位置信息保存在一個list中。

比如現在刪除掉3,就剩下12456,這時候快速的用位置計算算法再算出只有5個item時的新的一個位置信息list,計算完成後遍歷12456,遍歷平移對應的位置信息(這裡用屬性動畫就可以),位置沒有發生變化的就不會動,位置信息變化的就有平移效果。

位置平移在排序和刪除的時候的應用時一樣的。

平移動畫

 

/**
     * 移除item之後的過渡動畫
     * 從原始坐標移動到新坐標
     *
     * @param resource   移除控件之後計算產生的新的item坐標
     * @param targetView 原始坐標
     */
    public void transAnimator(final ArrayList resource, final ArrayList targetView) {
        ValueAnimator translation = ValueAnimator.ofFloat(0f, 1f);
        translation.setDuration(250);
        translation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float values = (float) animation.getAnimatedValue();
                for (int i = 0; i < targetView.size(); i++) {
                    float x = (float) (resource.get(i).x - targetView.get(i).getParentX()) * values;
                    float y = (float) (resource.get(i).y - targetView.get(i).getParentY()) * values;
                    targetView.get(i).setTranslationX(x);
                    targetView.get(i).setTranslationY(y);
                    requestLayout();
                }
            }
        });
        translation.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {

            }

            @Override
            public void onAnimationEnd(Animator animation) {
                for (int i = 0; i < targetView.size(); i++) {
                    targetView.get(i).setTranslationX(0);
                    targetView.get(i).setTranslationY(0);
                }
                getData().remove(mTargetItem);
                /**
                 * 判斷最後一個Item,也就是只剩加號的時候退出編輯狀態
                 */
                if (getData().size() == 1) {
                    mOnEditModeChangeListener.onExitEditMode();
                }

                isRemoveFinish = true;
            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });
        translation.start();
    }
交換動畫

 

 

/**
     * 交換動畫
     *
     * @param resource   源坐標,也就是動畫的起始坐標
     * @param targetView 目標坐標,也就是動畫的終點坐標
     * @param index      當前view交換之後的目標位置的index索引,主要用來屏蔽動畫,因為松手之後有動畫,不需要這這裡再加動畫了
     */
    public void exchangeAnimator(final Coordinate resource, final ArrayList targetView, final int index) {
        ValueAnimator translation = ValueAnimator.ofFloat(0f, 1f);
        translation.setDuration(250);
        translation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float values = (float) animation.getAnimatedValue();
                float x = (float) (resource.x - targetView.get(index).getParentX()) * values;
                float y = (float) (resource.y - targetView.get(index).getParentY()) * values;
                targetView.get(index).setTranslationX(x);
                targetView.get(index).setTranslationY(y);
                requestLayout();
            }
        });
        translation.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {

            }

            @Override
            public void onAnimationEnd(Animator animation) {
                for (int i = 0; i < targetView.size(); i++) {
                    targetView.get(i).setTranslationX(0);
                    targetView.get(i).setTranslationY(0);
                }
                putData(targetView);
                isExChangeFinish = true;

            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });
        translation.start();
    }
7.控件之間如何交互

AngleView 菜單 (關鍵代碼近2000行)

AngleLayout菜單容器

AngleViewTheme菜單主題

AngleIndicatorView菜單指示器

AnglrIndicatorTheme菜單指示器主題

CornerView角落控制按鈕

CornerThemeView角落控制按鈕主題

LoadingView進度條

 

各個控件都定義了接口和其他接口進行值的傳遞和交互

 




    

    

    

    

    

    

    


8.重寫的onItemClick,onItemLongClick事件

 

為什麼要重寫onClick,onItemClick,onItemLongClick呢?原生的好好的為什麼不用?

這個問題其實前面已經給出了答案,當控件的畫布發生變化之後,這些“on事件”是可以觸發的,但是看不到實際的item控件,因為這些控件已經跟著畫布轉走了,再使用肯定不是辦法,所以要重寫,重寫的時候根據子item布局時候的位置加上onTouch的位置信息即可計算出來點擊的是哪個item。所以自定義這些"on事件"也不是什麼難事。

 

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved