模仿手機QQ底部導(dǎo)航欄Icon拖拽效果

本文已授權(quán)微信公眾號:鴻洋(hongyangAndroid)原創(chuàng)首發(fā)捧韵。
之前玩手機QQ時發(fā)現(xiàn)下面的圖標(biāo)竟然可以拖拽市咆,發(fā)現(xiàn)還蠻好玩的。于是自己也模仿著寫了一個再来。
先上個效果圖吧

實現(xiàn)的方式有很多蒙兰,我說一下我的思路:我的思路比較簡單,無非就是上下兩層圖片可拖動的范圍和速度不一樣唄(大圖標(biāo)拖動范圍和速度小于小圖標(biāo)拖動范圍和速度)芒篷。
備注(以第一個消息圖標(biāo)為例):大圖標(biāo)指的是外面的氣泡圖標(biāo)搜变,小圖標(biāo)指的是氣泡里面的眼睛和嘴巴圖標(biāo)。切圖時將一張整體圖片切成了這兩個圖標(biāo)针炉。具體可下載Demo參考里面的圖片資源挠他。
自定義屬性

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="QQNaviView">
        <attr name="bigIconSrc" format="reference"/>
        <attr name="smallIconSrc" format="reference"/>
        <attr name="iconWidth" format="dimension"/>
        <attr name="iconHeight" format="dimension"/>
        <attr name="range" format="float"/>
    </declare-styleable>
</resources>

其中range為可拖動的范圍(其實是倍數(shù)),默認(rèn)值是1篡帕,不宜設(shè)置過大殖侵。
主要的拖動代碼

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float x = event.getX();
        float y = event.getY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                lastX = x;
                lastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                float deltaX = x - lastX;
                float deltaY = y - lastY;

                moveEvent(mBigIcon, deltaX, deltaY, mSmallRadius);
                //因為可拖動大半徑是小半徑的1.5倍贸呢, 因此這里x,y也相應(yīng)乘1.5
                moveEvent(mSmallIcon, 1.5f * deltaX, 1.5f * deltaY, mBigRadius);
                break;
            case MotionEvent.ACTION_UP:
                //抬起時復(fù)位
                mBigIcon.setX(0);
                mBigIcon.setY(0);
                mSmallIcon.setX(0);
                mSmallIcon.setY(0);
                break;
        }
        return super.onTouchEvent(event);
    }

這里先得到X軸拖動的距離deltaX和Y軸拖動的距離deltaY,大圖標(biāo)對應(yīng)小半徑拢军,小圖標(biāo)對應(yīng)大半徑楞陷。然后看moveEvent方法:

private void moveEvent(View view, float deltaX, float deltaY, float radius){

        //先計算拖動距離
        float distance = getDistance(deltaX, deltaY);

        //拖動的方位角,atan2出來的角度是帶正負(fù)號的
        double degree = Math.atan2(deltaY, deltaX);

        //如果大于臨界半徑就不能再往外拖了
        if (distance > radius){
            view.setX(view.getLeft() + (float) (radius * Math.cos(degree)));
            view.setY(view.getTop() + (float) (radius * Math.sin(degree)));
        }else {
            view.setX(view.getLeft() + deltaX);
            view.setY(view.getTop() + deltaY);
        }

    }

方法很簡單茉唉,注釋結(jié)合這張圖就一目了然了固蛾,主要是注意在抬起時圖標(biāo)復(fù)位就好了。

簡單看一下初始化
由于圖標(biāo)下面一般會帶文字度陆,因此直接繼承了LinearLayout魏铅,并且默認(rèn)設(shè)置成了垂直排列。

   public QQNaviView(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        mContext = context;

        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.QQNaviView, defStyleAttr, 0);
        mBigIconSrc = ta.getResourceId(R.styleable.QQNaviView_bigIconSrc, R.drawable.big);
        mSmallIconSrc = ta.getResourceId(R.styleable.QQNaviView_smallIconSrc, R.drawable.small);
        mIconWidth = ta.getDimension(R.styleable.QQNaviView_iconWidth, dp2px(context, 60));
        mIconHeight = ta.getDimension(R.styleable.QQNaviView_iconHeight, dp2px(context, 60));
        mRange = ta.getFloat(R.styleable.QQNaviView_range, 1);
        ta.recycle();

        //默認(rèn)垂直排列
        setOrientation(LinearLayout.VERTICAL);

        init(context);
    }

在init方法中進(jìn)行了布局文件的綁定坚芜,并且讓該view水平居中览芳。

 private void init(Context context) {
        mView = inflate(context, R.layout.view_icon, null);
        mBigIcon = (ImageView) mView.findViewById(R.id.iv_big);
        mSmallIcon = (ImageView) mView.findViewById(R.id.iv_small);

        mBigIcon.setImageResource(mBigIconSrc);
        mSmallIcon.setImageResource(mSmallIconSrc);

        setWidthAndHeight(mBigIcon);
        setWidthAndHeight(mSmallIcon);

        LayoutParams lp = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        lp.gravity = Gravity.CENTER_HORIZONTAL;
        mView.setLayoutParams(lp);
        addView(mView);
    }

這里值得注意的是onMeasure方法。由于圖標(biāo)可以往外拖動鸿竖,所以要給ImageView一個默認(rèn)的padding沧竟,不然拖動時最外面部分會消失。

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        setupView();

        final int w = resolveSize(getMeasuredWidth(), widthMeasureSpec);
        final int h = resolveSize(getMeasuredHeight(), heightMeasureSpec);

        setMeasuredDimension(w, h);
    }

    /**
     * 確定view以及拖動相關(guān)參數(shù)
     */
    private void setupView() {

        //根據(jù)view的寬高確定可拖動半徑的大小缚忧,這里要用getMeasuredWidth和getMeasuredHeight
        mSmallRadius = 0.1f * Math.min(mView.getMeasuredWidth(), mView.getMeasuredHeight()) * mRange;
        mBigRadius = 1.5f * mSmallRadius;

        //設(shè)置imageview的padding悟泵,不然拖動時圖片邊緣部分會消失
        int padding = (int) mBigRadius;
        mBigIcon.setPadding(padding, padding, padding, padding);
        mSmallIcon.setPadding(padding, padding, padding, padding);
    }

然后就沒啥好說了,直接看源碼吧闪水。
源碼:

public class QQNaviView extends LinearLayout {

    private static final String TAG = "QQNaviView";

    private Context mContext;

    /* 主view */
    private View mView;

    /* 外層icon/拖動幅度較小icon */
    private ImageView mBigIcon;

    /* 里層icon/拖動幅度較大icon */
    private ImageView mSmallIcon;

    /* 外層icon資源 */
    private int mBigIconSrc;

    /* 里面icon資源 */
    private int mSmallIconSrc;

    /* icon寬度 */
    private float mIconWidth;

    /* icon高度 */
    private float mIconHeight;

    /* 拖動幅度較大半徑 */
    private float mBigRadius;

    /* 拖動幅度小半徑 */
    private float mSmallRadius;

    /* 拖動范圍 可調(diào) */
    private float mRange;

    private float lastX;

    private float lastY;

    public QQNaviView(@NonNull Context context) {
        this(context, null);
    }

    public QQNaviView(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public QQNaviView(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        mContext = context;

        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.QQNaviView, defStyleAttr, 0);
        mBigIconSrc = ta.getResourceId(R.styleable.QQNaviView_bigIconSrc, R.drawable.big);
        mSmallIconSrc = ta.getResourceId(R.styleable.QQNaviView_smallIconSrc, R.drawable.small);
        mIconWidth = ta.getDimension(R.styleable.QQNaviView_iconWidth, dp2px(context, 60));
        mIconHeight = ta.getDimension(R.styleable.QQNaviView_iconHeight, dp2px(context, 60));
        mRange = ta.getFloat(R.styleable.QQNaviView_range, 1);
        ta.recycle();

        //默認(rèn)垂直排列
        setOrientation(LinearLayout.VERTICAL);

        init(context);
    }

    private void init(Context context) {
        mView = inflate(context, R.layout.view_icon, null);
        mBigIcon = (ImageView) mView.findViewById(R.id.iv_big);
        mSmallIcon = (ImageView) mView.findViewById(R.id.iv_small);

        mBigIcon.setImageResource(mBigIconSrc);
        mSmallIcon.setImageResource(mSmallIconSrc);

        setWidthAndHeight(mBigIcon);
        setWidthAndHeight(mSmallIcon);

        LayoutParams lp = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        lp.gravity = Gravity.CENTER_HORIZONTAL;
        mView.setLayoutParams(lp);
        addView(mView);
    }

    /**
     * 設(shè)置icon寬高
     * @param view
     */
    private void setWidthAndHeight(View view){
        FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) view.getLayoutParams();
        lp.width = (int) mIconWidth;
        lp.height = (int) mIconHeight;
        view.setLayoutParams(lp);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        setupView();

        final int w = resolveSize(getMeasuredWidth(), widthMeasureSpec);
        final int h = resolveSize(getMeasuredHeight(), heightMeasureSpec);

        setMeasuredDimension(w, h);
    }

    /**
     * 確定view以及拖動相關(guān)參數(shù)
     */
    private void setupView() {

        //根據(jù)view的寬高確定可拖動半徑的大小
        mSmallRadius = 0.1f * Math.min(mView.getMeasuredWidth(), mView.getMeasuredHeight()) * mRange;
        mBigRadius = 1.5f * mSmallRadius;

        //設(shè)置imageview的padding糕非,不然拖動時圖片邊緣部分會消失
        int padding = (int) mBigRadius;
        mBigIcon.setPadding(padding, padding, padding, padding);
        mSmallIcon.setPadding(padding, padding, padding, padding);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        int childLeft;
        int childTop = 0;
        for (int i = 0; i < getChildCount(); i ++){
            final View child = getChildAt(i);
            LayoutParams lp = (LayoutParams) child.getLayoutParams();
            if (child.getVisibility() != GONE){
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();
                //水平居中顯示
                childLeft = (getWidth() - childWidth) / 2;
                //當(dāng)前子view的top
                childTop += lp.topMargin;
                child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
                //下一個view的top是當(dāng)前子view的top + height + bottomMargin
                childTop += childHeight + lp.bottomMargin;
            }
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float x = event.getX();
        float y = event.getY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                lastX = x;
                lastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                float deltaX = x - lastX;
                float deltaY = y - lastY;

                moveEvent(mBigIcon, deltaX, deltaY, mSmallRadius);
                //因為可拖動大半徑是小半徑的1.5倍, 因此這里x,y也相應(yīng)乘1.5
                moveEvent(mSmallIcon, 1.5f * deltaX, 1.5f * deltaY, mBigRadius);
                break;
            case MotionEvent.ACTION_UP:
                //抬起時復(fù)位
                mBigIcon.setX(0);
                mBigIcon.setY(0);
                mSmallIcon.setX(0);
                mSmallIcon.setY(0);
                break;
        }
        return super.onTouchEvent(event);
    }

    /**
     * 拖動事件
     * @param view
     * @param deltaX
     * @param deltaY
     * @param radius
     */
    private void moveEvent(View view, float deltaX, float deltaY, float radius){

        //先計算拖動距離
        float distance = getDistance(deltaX, deltaY);

        //拖動的方位角球榆,atan2出來的角度是帶正負(fù)號的
        double degree = Math.atan2(deltaY, deltaX);

        //如果大于臨界半徑就不能再往外拖了
        if (distance > radius){
            view.setX(view.getLeft() + (float) (radius * Math.cos(degree)));
            view.setY(view.getTop() + (float) (radius * Math.sin(degree)));
        }else {
            view.setX(view.getLeft() + deltaX);
            view.setY(view.getTop() + deltaY);
        }

    }

    private int dp2px(Context context, float dpVal) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                dpVal, context.getResources().getDisplayMetrics());
    }

    private float getDistance(float x, float y){
        return (float) Math.sqrt(x * x + y * y);
    }

    public void setBigIcon(int res){
        mBigIcon.setImageResource(res);
    }

    public void setSmallIcon(int res){
        mSmallIcon.setImageResource(res);
    }

    public void setIconWidthAndHeight(float width, float height){
        mIconWidth = dp2px(mContext, width);
        mIconHeight = dp2px(mContext, height);
        setWidthAndHeight(mBigIcon);
        setWidthAndHeight(mSmallIcon);
    }

    public void setRange(float range){
        mRange = range;
    }

}
name format description
bigIconSrc reference 大圖標(biāo)資源
smallIconSrc reference 小圖標(biāo)資源
iconWidth dimension 圖標(biāo)寬度
iconHeight dimension 圖標(biāo)高度
range float 可拖動范圍

PS:一些手機上沒有效果應(yīng)該是setupView()方法中之前用的是mView.getWidth()和mView.getMeasuredHeight()朽肥,應(yīng)該改為mView.getMeasuredWidth和mView.getMeasuredHeight(),感謝gitkanglei的指出持钉。關(guān)于兩者的區(qū)別可參考:
http://blog.csdn.net/dmk877/article/details/49734869/

name format description
bigIconSrc reference 大圖標(biāo)資源
smallIconSrc reference 小圖標(biāo)資源
iconWidth dimension 圖標(biāo)寬度
iconHeight dimension 圖標(biāo)高度
range float 可拖動范圍

如果有其他的實現(xiàn)方式衡招,或者代碼中有什么不合理的地方,歡迎交流~
源碼地址:https://github.com/XingdongYu/QQNaviView歡迎star每强,rua~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末始腾,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子空执,更是在濱河造成了極大的恐慌浪箭,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,546評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件辨绊,死亡現(xiàn)場離奇詭異奶栖,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評論 3 395
  • 文/潘曉璐 我一進(jìn)店門驼抹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來桑孩,“玉大人拜鹤,你說我怎么就攤上這事框冀。” “怎么了敏簿?”我有些...
    開封第一講書人閱讀 164,911評論 0 354
  • 文/不壞的土叔 我叫張陵明也,是天一觀的道長。 經(jīng)常有香客問我惯裕,道長温数,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,737評論 1 294
  • 正文 為了忘掉前任蜻势,我火速辦了婚禮撑刺,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘握玛。我一直安慰自己够傍,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,753評論 6 392
  • 文/花漫 我一把揭開白布挠铲。 她就那樣靜靜地躺著冕屯,像睡著了一般。 火紅的嫁衣襯著肌膚如雪拂苹。 梳的紋絲不亂的頭發(fā)上安聘,一...
    開封第一講書人閱讀 51,598評論 1 305
  • 那天,我揣著相機與錄音瓢棒,去河邊找鬼浴韭。 笑死,一個胖子當(dāng)著我的面吹牛脯宿,可吹牛的內(nèi)容都是我干的囱桨。 我是一名探鬼主播,決...
    沈念sama閱讀 40,338評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼嗅绰,長吁一口氣:“原來是場噩夢啊……” “哼舍肠!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起窘面,我...
    開封第一講書人閱讀 39,249評論 0 276
  • 序言:老撾萬榮一對情侶失蹤翠语,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后财边,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體肌括,經(jīng)...
    沈念sama閱讀 45,696評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,888評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了谍夭。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片黑滴。...
    茶點故事閱讀 40,013評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖紧索,靈堂內(nèi)的尸體忽然破棺而出袁辈,到底是詐尸還是另有隱情,我是刑警寧澤珠漂,帶...
    沈念sama閱讀 35,731評論 5 346
  • 正文 年R本政府宣布晚缩,位于F島的核電站,受9級特大地震影響媳危,放射性物質(zhì)發(fā)生泄漏荞彼。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,348評論 3 330
  • 文/蒙蒙 一待笑、第九天 我趴在偏房一處隱蔽的房頂上張望鸣皂。 院中可真熱鬧,春花似錦暮蹂、人聲如沸寞缝。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,929評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽第租。三九已至,卻和暖如春我纪,著一層夾襖步出監(jiān)牢的瞬間慎宾,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,048評論 1 270
  • 我被黑心中介騙來泰國打工浅悉, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留趟据,地道東北人。 一個月前我還...
    沈念sama閱讀 48,203評論 3 370
  • 正文 我出身青樓术健,卻偏偏與公主長得像汹碱,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子荞估,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,960評論 2 355

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