在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下載鏈接