1.頻道選擇器,頻道定制
??現(xiàn)在市場(chǎng)上的新聞軟件中,絕大多數(shù)都會(huì)有頻道選擇器,比如騰訊新聞、網(wǎng)易新聞、今日頭條等,頻道選擇器可以幫助用戶(hù)定制自己想要的新聞板塊,給用戶(hù)更好的體驗(yàn)。我們的項(xiàng)目正好也是一個(gè)新聞?lì)怉PP,為了更好的符合我們的產(chǎn)品,我們需要自己實(shí)現(xiàn)一套頻道選擇器,項(xiàng)目地址ChannelView,如果有需要的朋友可以看一下,先來(lái)看一下效果圖。

??從效果上看,我們的頻道選擇器已經(jīng)完全不弱于市面上的大多數(shù)主流應(yīng)用的選擇器,頻道拖動(dòng)、頻道刪除、頻道添加,動(dòng)畫(huà)效果都已經(jīng)包含,并且十分流暢沒(méi)有卡頓,下面我們就一起看看這款自定義View是如何實(shí)現(xiàn)的吧。
2.View結(jié)構(gòu)
??想實(shí)現(xiàn)這個(gè)選擇器不難,因?yàn)樗皇且恍?duì)子View的布局和位置調(diào)整,所以我們的重點(diǎn)就是確定每個(gè)子View的位置,并保存它的坐標(biāo),然后用動(dòng)畫(huà)讓子View之間可以交換位置也就是交換坐標(biāo),這些核心地方實(shí)現(xiàn)了,其他的一些拖動(dòng)、增刪功能也就不是問(wèn)題了,都是在它的基礎(chǔ)上實(shí)現(xiàn)的。
??現(xiàn)在,我們都知道要實(shí)現(xiàn)一個(gè)自定義View我們需要繼承View或者ViewGroup,這里我們一看擁有這么多子View就知道肯定要繼承ViewGroup了。但問(wèn)題又來(lái)了,在那么的多的ViewGroup中我們需要使用哪個(gè)呢?其實(shí)有很多的選擇,關(guān)鍵是哪個(gè)更方便,先讓我們來(lái)考慮一下選擇哪個(gè)吧,這是任何一個(gè)自定義View的開(kāi)始,選合適了我們可以事半功倍。
??如果我們的View繼承LinearLayout,雖然子View有一定的順序讓我們不用覆蓋它的onLayout()方法重寫(xiě),但由于橫向、豎向都有所以我們要嵌套的使用LinarLayout,這會(huì)讓View過(guò)度繪制。如果繼承RelativeLayout的話,它的子View似乎需要我們自己確定位置,我們需要在onLayout()里面進(jìn)行計(jì)算每個(gè)View,這似乎還不如直接繼承ViewGroup,其實(shí)我的第一個(gè)頻道選擇器就是繼承的ViewGroup實(shí)現(xiàn)的,功能效果跟現(xiàn)在的幾乎一樣,但代碼實(shí)現(xiàn)上慘不忍睹,所以又重新寫(xiě)了現(xiàn)在這個(gè)。好了,我們的這個(gè)頻道選擇器是繼承GridLayout實(shí)現(xiàn)的,其實(shí)一看它的布局就應(yīng)該能想到,結(jié)果我第一次實(shí)現(xiàn)的時(shí)候卻沒(méi)想到它,使用GridLayout的好處是我們不用自己去實(shí)現(xiàn)子View的位置,只需要添加子View后它就會(huì)根據(jù)根據(jù)我們對(duì)GridLayout的屬性設(shè)置自動(dòng)布局好每個(gè)子View的位置,然后我們只需要在onLayout()方法中遍歷每個(gè)子View得到它的坐標(biāo)位置保存就OK了,我們看一下整個(gè)選擇器的布局情況

??整個(gè)自定義View只有3層,最外層是繼承ScrollView的ChannelView,中間是繼承GridLayout的ChannelLayout,最里面一層是并列的TextView頻道,下面我們進(jìn)入正題吧。
3.代碼結(jié)構(gòu)

??我們的自定義View命名為ChannelView,包括兩個(gè)內(nèi)部類(lèi)和一個(gè)接口,其中ChannelView繼承ScrollView,可以實(shí)現(xiàn)上下滑動(dòng),ChannelAttr是頻道屬性,ChannelLayout繼承GridLayout是整個(gè)頻道選擇器的核心類(lèi)。
4.具體實(shí)現(xiàn)
- 初始化數(shù)據(jù)
public ChannelLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
setColumnCount(channelColumn);
setPadding(channelPadding, channelPadding, channelPadding, channelPadding);
addChannelView();
}
/**
* 設(shè)置頻道View
*/
private void addChannelView() {
if (channelContents != null) {
groupChannelColumns = new int[channelContents.size()];
int j = 0;
int startRow = 0;
for (String aKeySet : channelContents.keySet()) {//遍歷key值,設(shè)置標(biāo)題名稱(chēng)
String[] channelContent = channelContents.get(aKeySet);
if (channelContent == null) {
channelContent = new String[]{};
}
groupChannelColumns[j] = channelContent.length % channelColumn == 0 ? channelContent.length / channelColumn : channelContent.length / channelColumn + 1;
if (j == 0) {
startRow = 0;
} else {
startRow += groupChannelColumns[j - 1] + 1;
}
Spec rowSpec = GridLayout.spec(startRow);
//標(biāo)題要占channelColumn列
Spec columnSpec = GridLayout.spec(0, channelColumn);
LayoutParams layoutParams = new LayoutParams(rowSpec, columnSpec);
View view = LayoutInflater.from(mContext).inflate(R.layout.cgl_my_channel, null);
if (j == 0) {
tipEdit = view.findViewById(R.id.tv_tip_edit);
tipEdit.setVisibility(VISIBLE);
tipEdit.setOnClickListener(this);
tipFinish = view.findViewById(R.id.tv_tip_finish);
tipFinish.setVisibility(INVISIBLE);
tipFinish.setOnClickListener(this);
}
ChannelAttr channelTitleAttr = new ChannelAttr();
channelTitleAttr.type = ChannelAttr.TITLE;
channelTitleAttr.coordinate = new PointF();
//為標(biāo)題View添加一個(gè)ChannelAttr屬性
view.setTag(channelTitleAttr);
TextView tvTitle = view.findViewById(R.id.tv_title);
tvTitle.setText(aKeySet);
addView(view, layoutParams);
channelTitleGroups.add(view);
ArrayList<View> channelGroup = new ArrayList<>();
int remainder = channelContent.length % channelColumn;
for (int i = 0; i < channelContent.length; i++) {//遍歷value中的頻道
TextView textView = new TextView(mContext);
ChannelAttr channelAttr = new ChannelAttr();
channelAttr.type = ChannelAttr.CHANNEL;
channelAttr.groupIndex = j;
channelAttr.coordinate = new PointF();
if (j != 0) {
channelAttr.belong = j;
} else {
if (channelBelongs.indexOfKey(i) >= 0) {
int belongId = channelBelongs.get(i);
if (belongId > 0 && belongId < channelContents.size()) {
channelAttr.belong = belongId;
} else {
Log.w(getClass().getSimpleName(), "歸屬I(mǎi)D不存在,默認(rèn)設(shè)置為1");
}
}
}
//為頻道添加ChannelAttr屬性
textView.setTag(channelAttr);
textView.setText(channelContent[i]);
textView.setGravity(Gravity.CENTER);
textView.setBackgroundResource(channelNormalBackground);
if (j == 0 && i <= channelFixedToPosition) {
textView.setTextColor(channelFixedColor);
}
textView.setOnClickListener(this);
textView.setOnTouchListener(this);
textView.setOnLongClickListener(this);
//設(shè)置每個(gè)頻道的間距
LayoutParams params = new LayoutParams();
int leftMargin = verticalSpacing, topMargin = horizontalSpacing, rightMargin = verticalSpacing, bottomMargin = horizontalSpacing;
if (i % channelColumn == 0) {
leftMargin = 0;
}
if ((i + 1) % channelColumn == 0) {
rightMargin = 0;
}
if (i < channelColumn) {
topMargin = 0;
}
if (remainder == 0) {
if (i >= channelContent.length - channelColumn) {
bottomMargin = 0;
}
} else {
if (i >= channelContent.length - remainder) {
bottomMargin = 0;
}
}
params.setMargins(leftMargin, topMargin, rightMargin, bottomMargin);
addView(textView, params);
channelGroup.add(textView);
}
channelGroups.add(channelGroup);
j++;
}
}
}
??通過(guò)setColumnCount(channelColumn)我們?cè)O(shè)置GridView的列數(shù)為channelColumn列。
??在addChannelView()中,我們主要做了如下幾個(gè)方面:
??1. 從存儲(chǔ)頻道的集合Map<String, String[]> channelContents中獲取數(shù)據(jù)。channelContents的長(zhǎng)度代表有多少組頻道,channelContents中的key為每組頻道的標(biāo)題,比如有“我的頻道”、“推薦頻道”、“國(guó)內(nèi)頻道”、“國(guó)外頻道”等,channelContents中的value為頻道組中的具體頻道。默認(rèn)channelContents中的第0項(xiàng)為“我的頻道”,是可以拖拽排序的,其他組都為待添加的頻道,不能拖拽,如果channelContents的大小為1,也就是只有“我的頻道”,那么會(huì)默認(rèn)再添加一組頻道數(shù)量為0的頻道組,為的是可以刪除已選擇的頻道,這段邏輯沒(méi)有在上面的代碼中,上面代碼中的channelContents是已經(jīng)處理好的變量,具體處理的細(xì)節(jié)可以看工程中的代碼;
??2. 遍歷channelContents的key值,創(chuàng)建頻道的標(biāo)題View,將key的值設(shè)為頻道標(biāo)題;并讓這個(gè)View所占的列數(shù)為channelColumn,將標(biāo)題添加到channelTitleGroups集合中;
??3. 在遍歷key的同時(shí),遍歷value中的頻道,將每個(gè)頻道作為T(mén)extView添加到GridView中,并且所占的列數(shù)為1列,將頻道添加到channelGroups集合中;
??4. 為每個(gè)子View設(shè)置一個(gè)屬性ChannelAttr,屬性中包含了類(lèi)型、坐標(biāo)等:
/**
* 頻道屬性
*/
private class ChannelAttr {
static final int TITLE = 0x01;
static final int CHANNEL = 0x02;
/**
* view類(lèi)型
*/
private int type;
/**
* view坐標(biāo)
*/
private PointF coordinate;
/**
* view所在的channelGroups位置
*/
private int groupIndex;
/**
* 頻道歸屬,用于刪除頻道時(shí)該頻道的歸屬位置(推薦、國(guó)內(nèi)、國(guó)外),默認(rèn)都為1
*/
private int belong = 1;
}
??groupIndex:說(shuō)明當(dāng)前頻道所在哪個(gè)頻道組,在添加頻道或刪除頻道時(shí)會(huì)發(fā)生變化,頻道標(biāo)題沒(méi)有該屬性;
??belong:是不會(huì)變化的,在初始化數(shù)據(jù)時(shí)已經(jīng)確定,它表明了該頻道原來(lái)是屬于什么地方的,當(dāng)從“我的頻道”中刪除時(shí)我們可以根據(jù)它知道該頻道應(yīng)該到哪去,頻道標(biāo)題沒(méi)有該屬性;
??coordinate:表示當(dāng)前頻道的坐標(biāo),會(huì)隨著增、刪、移動(dòng)頻道時(shí)發(fā)生變化;
??type:表示當(dāng)前View的類(lèi)別,只有兩種,頻道標(biāo)題或者頻道。
- 測(cè)量和布局
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (isAgainMeasure) {
int width = MeasureSpec.getSize(widthMeasureSpec);//ChannelLayout的寬
//不是通過(guò)動(dòng)畫(huà)改變ChannelLayout的高度
if (!isAnimateChangeHeight) {
int height = 0;
int allChannelTitleHeight = 0;
for (int i = 0; i < getChildCount(); i++) {
View childAt = getChildAt(i);
if (((ChannelAttr) childAt.getTag()).type == ChannelAttr.TITLE) {
//計(jì)算標(biāo)題View的寬高
childAt.measure(MeasureSpec.makeMeasureSpec(width - channelPadding * 2, MeasureSpec.EXACTLY), heightMeasureSpec);
allChannelTitleHeight += childAt.getMeasuredHeight();
} else if (((ChannelAttr) childAt.getTag()).type == ChannelAttr.CHANNEL) {
//計(jì)算每個(gè)頻道的寬高
channelWidth = (width - verticalSpacing * (channelColumn * 2 - 2) - channelPadding * 2) / channelColumn;
childAt.measure(MeasureSpec.makeMeasureSpec(channelWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(channelHeight, MeasureSpec.EXACTLY));
}
}
for (int groupChannelColumn : groupChannelColumns) {
if (groupChannelColumn > 0) {
height += channelHeight * groupChannelColumn + (groupChannelColumn * 2 - 2) * horizontalSpacing;
}
}
allChannelGroupsHeight = height;
height += channelPadding * 2 + allChannelTitleHeight;//ChannelLayout的高
setMeasuredDimension(width, height);
} else {//通過(guò)動(dòng)畫(huà)改變ChannelLayout的高度
setMeasuredDimension(width, animateHeight);
}
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
if (isAgainLayout) {
super.onLayout(changed, left, top, right, bottom);
for (int i = 0; i < getChildCount(); i++) {
View childAt = getChildAt(i);
ChannelAttr tag = (ChannelAttr) childAt.getTag();
tag.coordinate.x = childAt.getX();
tag.coordinate.y = childAt.getY();
}
isAgainLayout = false;
}
}
??onMeasure()方法中測(cè)量出選擇器的寬高,寬width已經(jīng)計(jì)算出,高由子View來(lái)決定。這里首先通過(guò)measure(int widthMeasureSpec, int heightMeasureSpec)方法測(cè)量所有子View大小。子View只有兩種類(lèi)型,標(biāo)題子View和頻道子View,其中標(biāo)題View是從xml布局中獲取的,它的寬高是已經(jīng)確定的值,不需要我們自己計(jì)算(代碼中它的寬還是需要我們計(jì)算的,因?yàn)槲覀優(yōu)镃hannelView自定義的屬性中有padding,所以還需要減去padding的值)。頻道子View的高度由ChannelView的自定義屬性確定不需要計(jì)算,寬度我們可以通過(guò)ChannelView的寬除以列數(shù)再減去padding和頻道之間的間距就可以得到。最后,我們根據(jù)子View的行數(shù)和每行的高度確定ChannelView的高,然后調(diào)用setMeasuredDimension()方法就可以了。
??在onMeasure()方法中,有兩個(gè)setMeasuredDimension()方法,其中上面的是我們用來(lái)第一次計(jì)算ChannelLayout用的,下面的是動(dòng)態(tài)的改變高度時(shí)調(diào)用的,也就是頻道的行數(shù)有變化時(shí),能讓高度通過(guò)動(dòng)畫(huà)形式平滑的改變高度,而不是突然變高或者變矮。
??onLayout()方法中很簡(jiǎn)單,不需要我們自己確定子View的位置,只需要存儲(chǔ)它在布局好之后的位置坐標(biāo)就可以。
- 效果需求
??上面的兩步完成之后我們已經(jīng)得到了想要的布局,也確定了每個(gè)子View的坐標(biāo)位置,現(xiàn)在我們已經(jīng)不需要GridLayout的幫助了,忘記它,它已經(jīng)完成了它的使命。接下來(lái)我們要做的就是拖動(dòng)改變頻道順序、增刪頻道。
??我們先來(lái)考慮一下我們想要的效果,當(dāng)長(zhǎng)按“我的頻道”時(shí)我們希望能編輯它,拖動(dòng)頻道可以改變它的順序,并且它的樣式也能改變,提示用戶(hù)能刪除這個(gè)頻道,然后后面的頻道能往前移動(dòng),刪除的頻道可以回歸到它所屬于的頻道組。當(dāng)點(diǎn)擊“完成”時(shí),我們希望樣式能恢復(fù)。對(duì)于其它的頻道組,我們希望點(diǎn)擊它的時(shí)候能添加到“我的頻道”中去,后面的頻道能往前排移動(dòng),這就是我們想要的效果。
??整個(gè)代碼中不需要實(shí)現(xiàn)特別復(fù)雜的觸摸事件,所以我們只需要繼承OnTouchListener、OnLongClickListener、OnClickListener就可以了
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
downX = event.getRawX();
downY = event.getRawY();
}
if (event.getAction() == MotionEvent.ACTION_MOVE && isChannelLongClick) {
//手移動(dòng)時(shí)拖動(dòng)頻道
channelDrag(v, event);
}
if (event.getAction() == MotionEvent.ACTION_UP && isChannelLongClick) {
//手抬起時(shí)頻道狀態(tài)
channelDragUp(v);
}
return false;
}
@Override
public void onClick(View v) {
if (v == tipFinish) {//點(diǎn)擊完成按鈕時(shí)
changeTip(false);
List<String> myChannels = new ArrayList<>();
for (View view : channelGroups.get(0)) {
myChannels.add(((TextView) view).getText().toString());
}
if (onChannelListener != null) {
onChannelListener.channelFinish(myChannels);
}
} else {
ChannelAttr tag = (ChannelAttr) v.getTag();
ArrayList<View> channels = channelGroups.get(tag.groupIndex);
if (tag.groupIndex == 0) {//如果點(diǎn)擊的是我的頻道組中的頻道
if (channelClickType == DELETE && channels.indexOf(v) > channelFixedToPosition) {
forwardSort(v, channels);
//減少我的頻道
deleteMyChannel(v);
} else if (channelClickType == NORMAL) {
//普通狀態(tài)時(shí)進(jìn)行點(diǎn)擊事件回調(diào)
if (onChannelListener != null) {
onChannelListener.channelItemClick(channels.indexOf(v), ((TextView) v).getText().toString());
}
}
} else {//點(diǎn)擊的其他頻道組中的頻道
forwardSort(v, channels);
//增加我的頻道
addMyChannel(v);
}
}
}
@Override
public boolean onLongClick(View v) {
v.bringToFront();
ChannelAttr tag = (ChannelAttr) v.getTag();
if (tag.groupIndex == 0) {//判斷是否點(diǎn)擊的我的頻道組
ArrayList<View> views = channelGroups.get(0);
int indexOf = views.indexOf(v);
if (indexOf > channelFixedToPosition) {
for (int i = channelFixedToPosition + 1; i < views.size(); i++) {
if (i == indexOf) {
views.get(i).setBackgroundResource(channelFocusedBackground);
} else {
views.get(i).setBackgroundResource(channelSelectedBackground);
}
}
changeTip(true);
}
}
//要返回true,否則會(huì)出發(fā)onclick事件
return true;
}
??在各個(gè)點(diǎn)擊事件中,我們需要判斷每次點(diǎn)擊的View屬性,根據(jù)v.getTag()方法獲取到ChannelAttr,判斷它此時(shí)所在的頻道組(位于channelGroups中的位置),判斷它原來(lái)歸屬于哪個(gè)頻道組,以及它的坐標(biāo),然后做出相應(yīng)的操作,比如拖拽、增刪等,下面我們來(lái)具體看一下這部分的代碼。
- 效果實(shí)現(xiàn)
/**
* 后面的頻道向前排序
*
* @param v
* @param channels
*/
private void forwardSort(View v, ArrayList<View> channels) {
int size = channels.size();
int indexOfValue = channels.indexOf(v);
if (indexOfValue != size - 1) {
for (int i = size - 1; i > indexOfValue; i--) {
View lastView = channels.get(i - 1);
ChannelAttr lastViewTag = (ChannelAttr) lastView.getTag();
View currentView = channels.get(i);
ChannelAttr currentViewTag = (ChannelAttr) currentView.getTag();
currentViewTag.coordinate = lastViewTag.coordinate;
currentView.animate().x(currentViewTag.coordinate.x).y(currentViewTag.coordinate.y).setDuration(DURATION_TIME);
}
}
}
/**
* 增加我的頻道
*
* @param v
*/
private void addMyChannel(final View v) {
//讓點(diǎn)擊的view置于最前方,避免遮擋
v.bringToFront();
ChannelAttr tag = (ChannelAttr) v.getTag();
ArrayList<View> channels = channelGroups.get(tag.groupIndex);
ArrayList<View> myChannels = channelGroups.get(0);
View finalMyChannel;
if (myChannels.size() == 0) {
finalMyChannel = channelTitleGroups.get(0);
} else {
finalMyChannel = myChannels.get(myChannels.size() - 1);
}
ChannelAttr finalMyChannelTag = (ChannelAttr) finalMyChannel.getTag();
myChannels.add(myChannels.size(), v);
channels.remove(v);
animateChangeGridViewHeight();
final ViewPropertyAnimator animate = v.animate();
if (myChannels.size() % channelColumn == 1 || channelColumn == 1) {
if (myChannels.size() == 1) {
tag.coordinate = new PointF(finalMyChannelTag.coordinate.x, finalMyChannelTag.coordinate.y + finalMyChannel.getMeasuredHeight());
//我的頻道多一行,下面的view往下移
viewMove(1, channelHeight);
} else {
ChannelAttr firstMyChannelTag = (ChannelAttr) myChannels.get(0).getTag();
tag.coordinate = new PointF(firstMyChannelTag.coordinate.x, finalMyChannelTag.coordinate.y + channelHeight + horizontalSpacing * 2);
//我的頻道多一行,下面的view往下移
viewMove(1, channelHeight + horizontalSpacing * 2);
}
animate.x(tag.coordinate.x).y(tag.coordinate.y).setDuration(DURATION_TIME);
} else {
tag.coordinate = new PointF(finalMyChannelTag.coordinate.x + channelWidth + verticalSpacing * 2, finalMyChannelTag.coordinate.y);
animate.x(tag.coordinate.x).y(tag.coordinate.y).setDuration(DURATION_TIME);
}
animate.setListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
if (channelClickType == DELETE) {
v.setBackgroundResource(channelSelectedBackground);
animate.setListener(null);
}
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
//該頻道少一行,下面的view往上移
if (channels.size() % channelColumn == 0) {
if (channels.size() == 0) {
viewMove(tag.groupIndex + 1, -channelHeight);
} else {
viewMove(tag.groupIndex + 1, -channelHeight - horizontalSpacing * 2);
}
}
tag.groupIndex = 0;
}
/**
* 刪除我的頻道
*
* @param v
*/
private void deleteMyChannel(View v) {
//讓點(diǎn)擊的view置于最前方,避免遮擋
v.bringToFront();
if (channelClickType == DELETE) {
v.setBackgroundResource(channelNormalBackground);
}
ChannelAttr tag = (ChannelAttr) v.getTag();
ArrayList<View> beLongChannels = channelGroups.get(tag.belong);
if (beLongChannels.size() == 0) {
tag.coordinate = new PointF(((ChannelAttr) channelTitleGroups.get(tag.belong).getTag()).coordinate.x, ((ChannelAttr) channelTitleGroups.get(tag.belong).getTag()).coordinate.y + channelTitleGroups.get(tag.belong).getMeasuredHeight());
} else {
ChannelAttr arriveTag = (ChannelAttr) beLongChannels.get(0).getTag();
tag.coordinate = arriveTag.coordinate;
}
v.animate().x(tag.coordinate.x).y(tag.coordinate.y).setDuration(DURATION_TIME);
beLongChannels.add(0, v);
channelGroups.get(0).remove(v);
animateChangeGridViewHeight();
PointF newPointF;
ChannelAttr finalChannelViewTag = (ChannelAttr) beLongChannels.get(beLongChannels.size() - 1).getTag();
//這個(gè)地方要注意順序
if (channelGroups.get(0).size() % channelColumn == 0) {
//我的頻道中少了一行,底下的所有view全都上移
if (channelGroups.get(0).size() == 0) {
viewMove(1, -channelHeight);
} else {
viewMove(1, -channelHeight - horizontalSpacing * 2);
}
}
if (beLongChannels.size() % channelColumn == 1) {
//回收來(lái)頻道中多了一行,底下的所有view全都下移
if (beLongChannels.size() == 1) {
viewMove(tag.belong + 1, channelHeight);
} else {
viewMove(tag.belong + 1, channelHeight + horizontalSpacing * 2);
}
newPointF = new PointF(tag.coordinate.x, finalChannelViewTag.coordinate.y + channelHeight + horizontalSpacing * 2);
} else {
newPointF = new PointF(finalChannelViewTag.coordinate.x + channelWidth + verticalSpacing * 2, finalChannelViewTag.coordinate.y);
}
for (int i = 1; i < beLongChannels.size(); i++) {
View currentView = beLongChannels.get(i);
ChannelAttr currentViewTag = (ChannelAttr) currentView.getTag();
if (i < beLongChannels.size() - 1) {
View nextView = beLongChannels.get(i + 1);
ChannelAttr nextViewTag = (ChannelAttr) nextView.getTag();
currentViewTag.coordinate = nextViewTag.coordinate;
} else {
currentViewTag.coordinate = newPointF;
}
currentView.animate().x(currentViewTag.coordinate.x).y(currentViewTag.coordinate.y).setDuration(DURATION_TIME);
}
tag.groupIndex = tag.belong;
}
/**
* 行數(shù)變化后的gridview高度并用動(dòng)畫(huà)改變
*/
private void animateChangeGridViewHeight() {
int newAllChannelGroupsHeight = 0;
for (int i = 0; i < channelGroups.size(); i++) {
ArrayList<View> channels = channelGroups.get(i);
groupChannelColumns[i] = channels.size() % channelColumn == 0 ? channels.size() / channelColumn : channels.size() / channelColumn + 1;
}
for (int groupChannelColumn : groupChannelColumns) {
if (groupChannelColumn > 0) {
newAllChannelGroupsHeight += channelHeight * groupChannelColumn + (groupChannelColumn * 2 - 2) * horizontalSpacing;
}
}
int changeHeight = newAllChannelGroupsHeight - allChannelGroupsHeight;
if (changeHeight != 0) {
allChannelGroupsHeight = newAllChannelGroupsHeight;
ValueAnimator valueAnimator = ValueAnimator.ofInt(getMeasuredHeight(), getMeasuredHeight() + changeHeight);
valueAnimator.setDuration(DURATION_TIME);
valueAnimator.start();
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
animateHeight = (int) animation.getAnimatedValue();
isAnimateChangeHeight = true;
requestLayout();
}
});
valueAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
isAnimateChangeHeight = false;
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
}
}
/**
* 受到行數(shù)所影響的view進(jìn)行上移或下移操作
*/
private void viewMove(int position, int offSetY) {
for (int i = position; i < channelTitleGroups.size(); i++) {
View view = channelTitleGroups.get(i);
ChannelAttr tag = (ChannelAttr) view.getTag();
tag.coordinate = new PointF(tag.coordinate.x, tag.coordinate.y + offSetY);
view.animate().x(tag.coordinate.x).y(tag.coordinate.y).setDuration(DURATION_TIME);
}
for (int i = position; i < channelGroups.size(); i++) {
ArrayList<View> otherChannels = channelGroups.get(i);
for (int j = 0; j < otherChannels.size(); j++) {
View view = otherChannels.get(j);
ChannelAttr tag = (ChannelAttr) view.getTag();
tag.coordinate = new PointF(tag.coordinate.x, tag.coordinate.y + offSetY);
view.animate().x(tag.coordinate.x).y(tag.coordinate.y).setDuration(DURATION_TIME);
}
}
}
float downX, downY;
float moveX, moveY;
/**
* 頻道拖動(dòng)
*/
private void channelDrag(View v, MotionEvent event) {
moveX = event.getRawX();
moveY = event.getRawY();
v.setX(v.getX() + (moveX - downX));
v.setY(v.getY() + (moveY - downY));
downX = moveX;
downY = moveY;
ArrayList<View> myChannels = channelGroups.get(0);
ChannelAttr vTag = (ChannelAttr) v.getTag();
int vIndex = myChannels.indexOf(v);
for (int i = 0; i < myChannels.size(); i++) {
if (i > channelFixedToPosition && i != vIndex) {
View iChannel = myChannels.get(i);
ChannelAttr iChannelTag = (ChannelAttr) iChannel.getTag();
int x1 = (int) iChannelTag.coordinate.x;
int y1 = (int) iChannelTag.coordinate.y;
int sqrt = (int) Math.sqrt((v.getX() - x1) * (v.getX() - x1) + (v.getY() - y1) * (v.getY() - y1));
if (sqrt <= RANGE && !animatorSet.isRunning()) {
animatorSet = new AnimatorSet();
PointF tempPoint = iChannelTag.coordinate;
ObjectAnimator[] objectAnimators = new ObjectAnimator[Math.abs(i - vIndex) * 2];
if (i < vIndex) {
for (int j = i; j < vIndex; j++) {
TextView view = (TextView) myChannels.get(j);
ChannelAttr viewTag = (ChannelAttr) view.getTag();
ChannelAttr nextGridViewAttr = ((ChannelAttr) myChannels.get(j + 1).getTag());
viewTag.coordinate = nextGridViewAttr.coordinate;
objectAnimators[2 * (j - i)] = ObjectAnimator.ofFloat(view, "X", viewTag.coordinate.x);
objectAnimators[2 * (j - i) + 1] = ObjectAnimator.ofFloat(view, "Y", viewTag.coordinate.y);
}
} else if (i > vIndex) {
for (int j = i; j > vIndex; j--) {
TextView view = (TextView) myChannels.get(j);
ChannelAttr viewTag = (ChannelAttr) view.getTag();
ChannelAttr preGridViewAttr = ((ChannelAttr) myChannels.get(j - 1).getTag());
viewTag.coordinate = preGridViewAttr.coordinate;
objectAnimators[2 * (j - vIndex - 1)] = ObjectAnimator.ofFloat(view, "X", viewTag.coordinate.x);
objectAnimators[2 * (j - vIndex - 1) + 1] = ObjectAnimator.ofFloat(view, "Y", viewTag.coordinate.y);
}
}
animatorSet.playTogether(objectAnimators);
animatorSet.setDuration(DURATION_TIME);
isAgainMeasure = false;
animatorSet.start();
vTag.coordinate = tempPoint;
myChannels.remove(v);
myChannels.add(i, v);
break;
}
}
}
}
/**
* 頻道拖動(dòng)抬起
*
* @param v
*/
private void channelDragUp(View v) {
isAgainMeasure = true;
isChannelLongClick = false;
ChannelAttr vTag = (ChannelAttr) v.getTag();
v.animate().x(vTag.coordinate.x).y(vTag.coordinate.y).setDuration(DURATION_TIME);
v.setBackgroundResource(channelSelectedBackground);
}
??上面的代碼主要有這幾個(gè)方法:
??forwardSort(View v, ArrayList<View> channels):讓被點(diǎn)擊頻道后面的所有頻道往前移動(dòng),不管是添加頻道還是刪除頻道,只要該頻道的后面還有其它頻道,那么它們一定要往前排移動(dòng)。具體做法就是先獲取該頻道所在的頻道組,然后遍歷被點(diǎn)擊頻道后面的所有頻道,改變它們的坐標(biāo),然后通過(guò)屬性動(dòng)畫(huà)v.animate().x().y()方法改變他們的位置;
??addMyChannel(final View v):點(diǎn)擊其他頻道增加我的頻道時(shí)觸發(fā)的方法,在之前會(huì)先調(diào)用forwardSort()方法,該方法主要是讓點(diǎn)擊的頻道做位移動(dòng)畫(huà),移動(dòng)到需要到達(dá)的位置,該位置坐標(biāo)之前是不確定的,所以需要通過(guò)計(jì)算得到;
??deleteMyChannel(View v):該方法的作用同上個(gè)方法類(lèi)似,點(diǎn)擊我的頻道時(shí)刪除該頻道的操作,要讓被刪除的頻道移動(dòng)到它所屬(也就是ChannelAttr的belong值)的頻道組的第一個(gè)位置?,F(xiàn)在要注意的是,這個(gè)方法和上面的方法會(huì)發(fā)生一個(gè)問(wèn)題,行數(shù)可能會(huì)改變,行數(shù)改變整個(gè)View的高度也會(huì)發(fā)生改變,所以我們需要下面這個(gè)方法來(lái)計(jì)算到底改變了多少,如何去改變它的高度;
??animateChangeGridViewHeight():這個(gè)就是通過(guò)動(dòng)畫(huà)改變整個(gè)View高度的方法,原理很簡(jiǎn)單,通過(guò)channelGroups集合得到每次增刪后的行數(shù)(所以該方法在addMyChannel()和deleteMyChannel()方法中都需要調(diào)用),和上一次增刪操作的行數(shù)比較得到行數(shù)差就可以了,然后通過(guò)ValueAnimator動(dòng)畫(huà)改變高度值,調(diào)用requestLayout()方法重新測(cè)量高度即可,在onMeasure()方法中我們已經(jīng)說(shuō)過(guò)了為什么會(huì)有兩個(gè)不同的setMeasuredDimension()方法;
??viewMove(int position, int offSetY):行數(shù)改變除了導(dǎo)致高度發(fā)生變化外,它底部的頻道組也會(huì)發(fā)生變化。比如如果該頻道組行數(shù)增加,那么它下面的所有頻道組包括頻道標(biāo)題也都需要往下移,如果該頻道組行數(shù)減少,那它下面的需要往上移。該方法接收一個(gè)頻道組所在位置參數(shù)和一個(gè)高度變化量的參數(shù),通過(guò)這兩個(gè)參數(shù)遍歷頻道組和頻道標(biāo)題組,讓它們所有的View位置發(fā)生改變;
??channelDrag(View v, MotionEvent event):頻道拖動(dòng)方法,當(dāng)該頻道在拖動(dòng)時(shí),我們判斷該頻道的位置和離它最近的一個(gè)頻道的距離,如果該距離小于我們定義的最小距離,那就讓該頻道插入到這個(gè)位置,它之前或者之后的頻道往后或者往前排列。要注意這個(gè)時(shí)候?qū)τ谠擃l道組的順序也要相應(yīng)調(diào)整,通過(guò)List中的add()和remove()方法實(shí)現(xiàn),還要注意他們的坐標(biāo)也發(fā)生了改變;
??channelDragUp(View v):這個(gè)方法是手抬起時(shí)觸發(fā)的方法,讓該頻道通過(guò)動(dòng)畫(huà)回到它應(yīng)該呆著的位置。
5.總結(jié)
??以上就是頻道選擇器的核心代碼,其他像接口的暴露、自定義屬性的實(shí)現(xiàn),在工程中都寫(xiě)的很詳細(xì)。總之,這篇文章是介紹我們實(shí)現(xiàn)這個(gè)自定義View的思路,大家看的時(shí)候不必完全盯著代碼細(xì)看,因?yàn)橐恍?shí)現(xiàn)細(xì)節(jié)可能并不是你想寫(xiě)的,一千個(gè)人眼中有一千個(gè)哈姆雷特,每個(gè)人的思路都是不同的,遇到問(wèn)題能想到一個(gè)具體的思路比實(shí)現(xiàn)上面那些代碼細(xì)節(jié)要高明的多。如果想看代碼細(xì)節(jié),那就看一些優(yōu)秀的開(kāi)源項(xiàng)目,那些開(kāi)源項(xiàng)目中出色的設(shè)計(jì)模式、優(yōu)雅的接口、完善的內(nèi)存管理才是我們應(yīng)該學(xué)習(xí)的。
項(xiàng)目鏈接:https://github.com/chengzhicao/ChannelView