仿专题订阅功能

在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下载链接

你可能感兴趣的:(android,专题订阅,标签拖动替换)