仿專題訂閱功能

在Android開發中,有些時候會涉及到專題訂閱,訂閱專題無非是添加/移除專題。而我們的產品的訂閱功能稍微有點不同,專題數默認7個,只能替換專題,不能夠取消/新添專題,這裏給出展示如下圖:

這裏寫圖片描述

實現過程如下:
1、自定義專題訂閱容器,涉及到標籤的移動,爲了更靈活的定義標籤位置,繼承了相對佈局RelativeLayout,將自定義佈局命名爲DraggingViewGroup;

2、定義專題的寬度,專題的高度在代碼中寫死,每行定義多少個專題也是確定了(這裏是4個),通過DraggingViewGroup寬計算每個專題的寬,重寫OnMeasure方法;

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // TODO Auto-generated method stub
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        if (heightMode != MeasureSpec.EXACTLY) {
            // 指定默認高度
            height = ((WindowManager) mContext
                    .getSystemService(Context.WINDOW_SERVICE))
                    .getDefaultDisplay().getHeight() / 2;
        }
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        if (widthMode != MeasureSpec.EXACTLY) {
            // 指定默認寬度
            width = ((WindowManager) mContext
                    .getSystemService(Context.WINDOW_SERVICE))
                    .getDefaultDisplay().getWidth();
        }
        mTextViewWidth = (width - mLeftMergin - mRightMergin - (mTVCountForOneLine - 1)
                * mHorizontalBlankWithTextView)
                / mTVCountForOneLine;
        setMeasuredDimension(width, height);
    }

其中,mTextViewWidth是專題寬,width是DraggingViewGroup佈局寬,mLeftMergin,mRightMergin是佈局內偏移位置,mTVCountForOneLine是每行的專題數量,mHorizontalBlankWithTextView是專題橫向間距。

3、添加並顯示專題
這裏添加的專題是個List數組,給定專題名稱列表後,便會生成專題信息;

public void addTextLabelList(List<String> labelNames) {
        mLabelNames = labelNames;
        if (mLabelNames == null || mLabelNames.size() == 0) {
            return;
        }
        post(new Runnable() {

            @Override
            public void run() {
                // TODO Auto-generated method stub
                int startX = mLeftMergin;
                int startY = mTopMergin + mLabelImageHeight;
                int childCount = mLabelNames.size();
                int curTvCount = 1;
                for (int i = 0; i < childCount; i++, curTvCount++) {
                    TextView tv = addTextLabel(mLabelNames.get(i), startX,
                            startY);
                    tv.setId(i);
                    mLabelPos.append(i, new Point(startX, startY));
                    mLabelViews.append(i, tv);
                    startX += mTextViewWidth + mHorizontalBlankWithTextView;
                    if (curTvCount > mSelectedTopicSize) {
                        // 備選標籤
                        if (curTvCount != (mSelectedTopicSize + 1)
                                && (curTvCount + 1) % mTVCountForOneLine == 0) {

                            startX = mLeftMergin;
                            startY += mCommonTVHeight
                                    + mVericalBlankWithTextView;
                        }
                        continue;
                    }
                    if (curTvCount % mTVCountForOneLine == 0) {
                        // 已選標籤
                        startX = mLeftMergin;
                        startY += mCommonTVHeight + mVericalBlankWithTextView;
                    }
                    if (curTvCount == mSelectedTopicSize) {
                        // 已選標籤爲默認數量(7個)時,充值下個標籤的位置
                        startX = mLeftMergin;
                        startY = firstDividerLineYPos + mTopMergin
                                + mLabelImageHeight + mVericalBlankWithTextView;
                    }
                }
                mTv = createBaseTextView("", 13, Color.parseColor("#696969"));
                LayoutParams lp = new LayoutParams(mTextViewWidth,
                        mCommonTVHeight);
                lp.leftMargin = mLeftMergin + 2 * mLabelImageWidth;
                lp.topMargin = firstDividerLineYPos + mVericalBlankWithTextView;
                mTv.setLayoutParams(lp);
                mTv.setBackgroundResource(R.drawable.normal_label_bg);
                mTv.setVisibility(View.GONE);
                addView(mTv);
            }
        });
    }

其中,45行前的代碼都是在添加專題到容器,並且計算下一個專題的位置,45行-53行,是添加一個臨時的專題控件,這個控件將隨手勢移動,可以看頂部的動圖,給用戶的感覺是選擇的那個專題隨手勢移動;在這段代碼中調用了addTextLabel方法,該方法定義了專題的基本信息,並且固定了專題的顯示位置,如下代碼;

    /**
     * 添加標籤
     * 
     * @param labelName
     *            ,標籤名稱
     * @param l
     *            ,左側位置
     * @param t
     *            ,頂部位置
     * @return
     */
    public TextView addTextLabel(String labelName, int l, int t) {
        TextView tv = createBaseTextView(labelName, 13,
                Color.parseColor("#696969"));
        tv.setLayoutParams(new LayoutParams(mTextViewWidth, mCommonTVHeight));
        tv.setBackgroundResource(R.drawable.normal_label_bg);
        addView(tv);
        setPosition(tv, l, t);
        return tv;

    }

設定專題的顯示位置;

    /**
     * 設置視圖的位置
     * 
     * @param v
     *            ,被設置的視圖
     * @param l
     *            ,左邊位置
     * @param t
     *            ,頂部位置
     */
    private void setPosition(View v, int l, int t) {
        int parentWidth = this.getMeasuredWidth();
        int parentHeight = this.getMeasuredHeight();
        if (l < 0)
            l = 0;
        else if ((l + v.getMeasuredWidth()) >= parentWidth) {
            l = parentWidth - v.getMeasuredWidth();
        }
        if (t < 0)
            t = 0;
        else if ((t + v.getHeight()) >= parentHeight) {
            t = parentHeight - v.getMeasuredHeight();
        }
        int r = l + v.getMeasuredWidth();
        int b = t + v.getMeasuredHeight();
        v.layout(l, t, r, b);
        RelativeLayout.LayoutParams params = (android.widget.RelativeLayout.LayoutParams) v
                .getLayoutParams();
        params.leftMargin = l;
        params.topMargin = t;
        v.setLayoutParams(params);
    }

4、移動專題
<1>確定某個點是否選中了某個專題
要移動專題,首先要清楚點擊的位置選中的是哪個專題?如下方法inRangeOfView便是用以判斷某個點(x,y)是否在某個專題內,除了判斷當前點是否在某個專題佈局範圍內,還記錄下了該點對應的專題的viewId,並改變了對應專題的background;

    /**
     * 判斷當前點,是否在某個標籤內
     * 
     * @param x
     * @param y
     * @param action
     *            ,當前手勢是向下按下狀態?移動狀態?
     * @return
     */
    private boolean inRangeOfView(int x, int y, int action) {
        boolean isInRangeOfView = false;
        try {
            int childCount = getChildCount();
            // 這裏-1,是因爲最後添加了一個可移動的textview
            for (int i = mNewAddViewIndex; i < childCount - 1; i++) {
                View view = getChildAt(i);
                if (view.getId() != mActionDownViewId) {
                    view.setBackgroundResource(R.drawable.normal_label_bg);
                }
                Rect rect = new Rect(view.getLeft(), view.getTop(),
                        view.getRight(), view.getBottom());
                if (rect.contains(x, y)) {
                    if (action == ACTION_DOWN) {
                        mActionDownViewId = view.getId();
                        mTv.setText(((TextView) view).getText().toString());
                    } else if (action == ACTION_MOVE) {
                        mActionMoveViewId = view.getId();
                    }
                    view.setBackgroundResource(R.drawable.cross_label_bg);
                    isInRangeOfView = true;
                }
            }
        } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        }
        // 曾經有選擇的textview,現在橫過了(即沒有在某個textview上方),這時需要把值重置
        if (!isInRangeOfView) {
            mActionMoveViewId = -1;
        }
        return isInRangeOfView;
    }

<2>記錄點擊時,選中的專題
當用戶點擊屏幕上任意一點(x,y)時,如果該點恰好是某個專題控件位置內時,將mTV(上文提到這個控件用以更隨手指移動,給用戶的體驗是選中的專題隨用戶手指移動)移動到點擊的位置;

private boolean actionDownInLabel(int x, int y) {
        if (inRangeOfView(x, y, ACTION_DOWN)) {
            setPosition(mTv, x - mTv.getMeasuredWidth() / 2,
                    y - mTv.getMeasuredHeight() / 2);
            mTv.setTranslationX(0);
            mTv.setTranslationY(0);
            mTv.setVisibility(View.VISIBLE);
            return true;
        }
        mActionDownViewId = -1;
        return false;
    }

該actionDownInLabel方法的調用位置在OnTouchEvent的ACTION_DOWN條件下,如果返回true表示將由onTouchEvent消費該點擊事件,否則交由上一層處理;

@Override
    public boolean onTouchEvent(MotionEvent event) {
        // TODO Auto-generated method stub
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            return actionDownInLabel((int) event.getX(), (int) event.getY());

        case MotionEvent.ACTION_MOVE:
            int x = (int) event.getX();
            int y = (int) event.getY();
            actionMove(x - mTv.getWidth() / 2, y - mTv.getHeight() / 2);
            break;

        case MotionEvent.ACTION_UP:
            changeLabelTextViewPosition();
            break;
        }
        return true;
    }

<3>移動選中的專題
假如點擊時恰好選中了某個專題,接下來的action_move事件將繼續交由OnToucheEvent處理,這時候調用actionMove方法,該方法裏調用了setPosition方法不斷的重設mTv的位置,是mTv跟隨用戶手指移動,同時繼續調用inRangeOfView方法(上文提到該方法功能——判斷當前點是否在某個專題佈局範圍內,還記錄下了該點對應的專題的viewId,並改變了對應專題的background),記錄手指橫跨某個專題時,被橫跨專題背景將會變化,而且記錄當前橫跨的位置,以便交互兩個專題的信息;

private void actionMove(int l, int t) {
        try {
            setPosition(mTv, l, t);
            inRangeOfView(l + mTv.getMeasuredWidth() / 2,
                    t + mTv.getMeasuredHeight() / 2, ACTION_MOVE);
        } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        }
    }

5、交換選中專題的信息
若按下時已選中某個專題了,移動過程中,並未選中其他專題便鬆開手指了,這時候選中的專題將會回到自己的原來的位置上,如下代碼第8行-第41行都是在實現這個邏輯處理;
若按下時已選中某個專題了,移動過程中,選中了其他的專題,然後鬆開手指,這個時候將要實現2個專題的信息交換,如下代碼的第43行-第88行都是在實現這個效果;

/** 標籤替換,標籤替換動畫效果 */
    private void changeLabelTextViewPosition() {
        try {
            int dx = 0;
            int dy = 0;
            Point downPoint = mLabelPos.get(mActionDownViewId);
            final View downView = mLabelViews.get(mActionDownViewId);
            if (mActionMoveViewId == -1) {
                // 不需要兩兩標籤替換,回到原來的位置
                dx = downPoint.x - (int) mTv.getLeft();
                dy = downPoint.y - (int) mTv.getTop();
                mTv.animate().translationX(dx).translationY(dy)
                        .setDuration(ANIMATE_TIME).start();
                mTv.animate().setListener(new AnimatorListener() {

                    @Override
                    public void onAnimationStart(Animator animation) {
                        // TODO Auto-generated method stub

                    }

                    @Override
                    public void onAnimationRepeat(Animator animation) {
                        // TODO Auto-generated method stub

                    }

                    @Override
                    public void onAnimationEnd(Animator animation) {
                        // TODO Auto-generated method stub
                        downView.setBackgroundResource(R.drawable.normal_label_bg);
                    }

                    @Override
                    public void onAnimationCancel(Animator animation) {
                        // TODO Auto-generated method stub

                    }
                });
                return;
            }

            Point movePoint = mLabelPos.get(mActionMoveViewId);
            if (downPoint == null || movePoint == null) {
                return;
            }

            View moveView = mLabelViews.get(mActionMoveViewId);
            if (downView == null || moveView == null) {
                return;
            }
            // 將第二個tv移動到第一個tv位置
            dx = downPoint.x - movePoint.x;
            dy = downPoint.y - movePoint.y;
            moveView.setBackgroundResource(R.drawable.normal_label_bg);
            moveView.animate().translationX(dx).translationY(dy)
                    .setDuration(ANIMATE_TIME).start();
            // 將第一個tv移動到第二個tv
            dx = movePoint.x - (int) mTv.getLeft();
            dy = movePoint.y - (int) mTv.getTop();
            mTv.animate().translationX(dx).translationY(dy)
                    .setDuration(ANIMATE_TIME).start();
            mTv.animate().setListener(new AnimatorListener() {

                @Override
                public void onAnimationStart(Animator animation) {
                    // TODO Auto-generated method stub

                }

                @Override
                public void onAnimationRepeat(Animator animation) {
                    // TODO Auto-generated method stub

                }

                @Override
                public void onAnimationEnd(Animator animation) {
                    // TODO Auto-generated method stub
                    reflashTextViewPosition();
                }

                @Override
                public void onAnimationCancel(Animator animation) {
                    // TODO Auto-generated method stub

                }
            });

        } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        }
    }

6、重置專題視圖
雖然用障眼法用mTv代替了按下選中的專題標籤來實現專題移動效果,實際上,動畫效果顯示完畢後,要重置一次專題視圖,將兩個交換信息的專題控件重置回原來的顯示位置,只要將兩個控件顯示的值(setText方法)交換即可。

    /**
     * 刷新視圖中文本位置及替換後的標籤名稱
     */
    private void reflashTextViewPosition() {
        try {
            TextView tvDownView = mLabelViews.get(mActionDownViewId);
            TextView tvMoveView = mLabelViews.get(mActionMoveViewId);
            if (tvDownView != null && tvMoveView != null) {
                tvDownView.setTranslationX(0);
                tvDownView.setTranslationY(0);
                tvDownView.setText(mLabelNames.get(mActionMoveViewId));
                tvDownView.setBackgroundResource(R.drawable.normal_label_bg);
                setPosition(tvDownView, mLabelPos.get(mActionDownViewId).x,
                        mLabelPos.get(mActionDownViewId).y);

                tvMoveView.setTranslationX(0);
                tvMoveView.setTranslationY(0);
                tvMoveView.setText(mLabelNames.get(mActionDownViewId));
                tvMoveView.setBackgroundResource(R.drawable.normal_label_bg);
                setPosition(tvMoveView, mLabelPos.get(mActionMoveViewId).x,
                        mLabelPos.get(mActionMoveViewId).y);
            }
            mTv.setVisibility(View.GONE);
            String tempString = mLabelNames.get(mActionMoveViewId);
            mLabelNames.set(mActionMoveViewId,
                    mLabelNames.get(mActionDownViewId));
            mLabelNames.set(mActionDownViewId, tempString);
            mActionDownViewId = -1;
            mActionMoveViewId = -1;
        } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        }
    }

至此,整個專題訂閱的已經講完了,提供demo下載鏈接

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