仿专题订阅功能

来源:互联网 发布:日常英语口语视频软件 编辑:程序博客网 时间:2024/06/06 22:56

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

0 0