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