自定義GridLayout控件,可以在新聞咨詢類APP中的管理頁面使用到,也可以應用在類別管理中,總之,可以幫助我們設計更加規范和炫酷的手機頁面。
新聞類app是最常見的應用之一,而頻道管理又是其必不可少的功能,該自定義控件不僅可以帶我們實現炫酷的頻道管理功能,還可以讓我們學習如何使用Android拖拽框架實現我們想要的多種功能,以及讓我們對自定義控件會有更多的理解。
知識點
-
GridLayout的使用
- 從Google官方文檔學習GridLayout的功能以及用法
- 使用GridLayout實現子控件排列顯示
-
View的拖拽功能實現
- 通過查看Google文檔,學會調用view的拖拽方法
- 拖拽事件的處理
- 使用View的拖拽框架實現實現頻道切換位置效果
-
自定義GridLayout控件
自定義GridLayout控件,實現拖拽功能,繼而實現頻道管理操作
-
Rect類的使用
使用Rect類確定被觸摸到的子控件
新聞頻道管理的多種實現
實現類似于網易新聞頻道管理有幾種方式
實現方案1:使用兩個GridView實現,這是比較早的一種解決方案,比較復雜,具體可以參考 Android 高仿 頻道管理----網易、今日頭條、騰訊視頻
實現方案2:使用兩個RecyclerView或ListView實現,這種實現方式好于于方案1
實現方案3:使用一個RecyclerView實現,這是我目前見過的比較好的方式,貌似比網易新聞等客戶端的要流暢,具體可以參考:高仿網易新聞欄目動畫效果 、 使用ItemTouchHelper高效地實現 今日頭條 、網易新聞 的頻道排序、移動
實現方案4:使用兩個GridLayout實現,也就是本文要介紹的方式
拖拽
長按item開始拖拽
private View.OnLongClickListener longClickListener = new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
//長按時,開始拖拽操作,顯示出陰影
//被拖拽的視圖其實就是v參數
dragedView = v;
v.startDrag(null, new View.DragShadowBuilder(v), null, 0);
v.setEnabled(false);
// v.startDragAndDrop(null, new View.DragShadowBuilder(v), null, 0);
return false;
}
};
拖拽方法參數說明
- startDragAndDrop() api24
- startDrag(ClipData data, DragShadowBuilder shadowBuilder, Object myLocalState, int flag)
參數1:ClipData data 拖拽過程中可以transferred的數據,可以為空
參數2:DragShadowBuilder shadowBuilder,拖拽陰影效果創建者
參數3:Object myLocalState,拖拽狀態
參數4:int flag,可以控制拖拽操作的flag,未定義,傳0即可
拖拽監聽
private View.OnDragListener dragListener = new View.OnDragListener() {
/**
* ACTION_DRAG_STARTED:當拖拽操作執行時,就會執行一次
* DragEvent.ACTION_DRAG_ENDED:當拖拽事件結束,手指抬起時,就是執行一次
* DragEvent.ACTION_DRAG_ENTERED:當手指進入設置了拖拽監聽的控件范圍內的瞬間執行一次
* DragEvent.ACTION_DRAG_EXITED:當手指離開設置了拖拽監聽的控件范圍內的瞬間執行一次
* DragEvent.ACTION_DRAG_LOCATION:當手指在設置了拖拽監聽的控件范圍內,移動時,實時會執行,執行N次
* DragEvent.ACTION_DROP:當手指在設置了拖拽監聽的控件范圍內松開時,執行一次
*
*
* @param v 當前監聽拖拽事件的view(其實就是mGridLayout)
* @param event 拖拽事件
* @return
*/
@Override
public boolean onDrag(View v, DragEvent event) {
String dragEventAction = getDragEventAction(event);
System.out.println(dragEventAction);
// Rect rect = new Rect();
// rect.contains()
switch (event.getAction()) {
//當拖拽事件開始時,創建出與子控件對應的矩形數組
case DragEvent.ACTION_DRAG_STARTED:
initRects();
break;
case DragEvent.ACTION_DRAG_LOCATION:
//手指移動時,實時判斷觸摸是否進入了某一個子控件
int touchIndex = getTouchIndex(event);
//說明觸摸點進入了某一個子控件,判斷被拖拽的視圖與進入的子控件對象不是同一個的時候才進行刪除添加操作
if (touchIndex > -1&&dragedView != null&&dragedView != mGridLayout.getChildAt(touchIndex)) {
mGridLayout.removeView(dragedView);
mGridLayout.addView(dragedView,touchIndex);
}
break;
case DragEvent.ACTION_DRAG_ENDED:
//拖拽事件結束后,讓被拖拽的view設置為可用,否則背景變紅,并且長按事件會失效
if (dragedView != null) {
dragedView.setEnabled(true);
}
break;
}
return true;
}
};
DragEvent
拖拽事件 | 說明 |
---|---|
ACTION_DRAG_STARTED | 當拖拽操作執行時,就會執行一次 |
DragEvent.ACTION_DRAG_ENDED | 當拖拽事件結束,手指抬起時,就是執行一次 |
DragEvent.ACTION_DRAG_ENTERED | 當手指進入設置了拖拽監聽的控件范圍內的瞬間執行一次 |
DragEvent.ACTION_DRAG_EXITED | 當手指離開設置了拖拽監聽的控件范圍內的瞬間執行一次 |
DragEvent.ACTION_DRAG_LOCATION | 當手指在設置了拖拽監聽的控件范圍內,移動時,實時會執行,執行N次 |
DragEvent.ACTION_DROP | 當手指在設置了拖拽監聽的控件范圍內松開時,執行一次 |
當拖拽事件開始時,創建出與子控件對應的矩形數組
private Rect[] mRects;
private void initRects() {
mRects = new Rect[mGridLayout.getChildCount()];
for (int i = 0; i < mGridLayout.getChildCount(); i++) {
View childView = mGridLayout.getChildAt(i);
//創建與每個子控件對應矩形對象
Rect rect = new Rect(childView.getLeft(), childView.getTop(), childView.getRight(), childView.getBottom());
mRects[i] = rect;
}
}
手指移動時,實時判斷觸摸是否進入了某一個子控件
private int getTouchIndex(DragEvent event) {
//遍歷所有的數組,如果包含了當前的觸摸點返回索引即可
for (int i = 0; i < mRects.length; i++) {
Rect rect = mRects[i];
if (rect.contains((int)event.getX(), (int)event.getY())) {
return i;
}
}
return -1;
}
是否允許拖拽
public void setAllowDrag(boolean allowDrag) {
this.allowdrag = allowDrag;
if (this.allowdrag) {
this.setOnDragListener(odl);
} else {
this.setOnDragListener(null);
}
}
設置列數和動畫
//初始化方法
private void init() {
// android:columnCount="4"
// android:animateLayoutChanges="true"
this.setColumnCount(columnCount);
this.setLayoutTransition(new LayoutTransition());
}
DragGridlayout
public class DragGridlayout extends GridLayout{
private static final int columnCount = 4;//列數
private boolean isAllowDrag;//記錄當前控件是否可以進行拖拽操作
public DragGridlayout(Context context) {
this(context,null);
}
public DragGridlayout(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public DragGridlayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
/* static SparseArray<String> dragEventType = new SparseArray<>();
static{
dragEventType.put(DragEvent.ACTION_DRAG_STARTED, "STARTED");
dragEventType.put(DragEvent.ACTION_DRAG_ENDED, "ENDED");
dragEventType.put(DragEvent.ACTION_DRAG_ENTERED, "ENTERED");
dragEventType.put(DragEvent.ACTION_DRAG_EXITED, "EXITED");
dragEventType.put(DragEvent.ACTION_DRAG_LOCATION, "LOCATION");
dragEventType.put(DragEvent.ACTION_DROP, "DROP");
}
public static String getDragEventAction(DragEvent de){
return dragEventType.get(de.getAction());
}*/
//初始化方法
private void init() {
// android:columnCount="4"
// android:animateLayoutChanges="true"
this.setColumnCount(columnCount);
this.setLayoutTransition(new LayoutTransition());
}
public void setItems(List<String> items) {
for (String item : items) {
addItem(item);
}
}
public void addItem(String content, int index) {
TextView tv = newItemView();
tv.setText(content);
addView(tv,index);
}
public void addItem(String content) {
TextView tv = newItemView();
tv.setText(content);
addView(tv);
}
private TextView newItemView() {
TextView tv = new TextView(getContext());
int margin = dip2px(5);
tv.setBackgroundResource(R.drawable.selector_tv_bg);
GridLayout.LayoutParams layoutParams = new GridLayout.LayoutParams();
layoutParams.width = getResources().getDisplayMetrics().widthPixels/4 - 2*margin;//寬為屏幕寬的4分之一
layoutParams.height = dip2px(25);
layoutParams.setMargins(margin,margin,margin,margin);
tv.setGravity(Gravity.CENTER);
tv.setLayoutParams(layoutParams);
if (isAllowDrag) {
//給條目設置長按點擊事件
tv.setOnLongClickListener(mLongClickListener);
} else {
tv.setOnLongClickListener(null);
}
//設置條目的點擊事件
tv.setOnClickListener(onClickListener);
return tv;
}
/** dip轉換px */
public int dip2px(int dip) {
final float scale = getResources().getDisplayMetrics().density;
return (int) (dip * scale + 0.5f);
}
private View dragedView;//被拖拽的視圖
private View.OnLongClickListener mLongClickListener = new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
//長按時,開始拖拽操作,顯示出陰影
//被拖拽的視圖其實就是v參數
dragedView = v;
v.startDrag(null, new View.DragShadowBuilder(v), null, 0);
v.setEnabled(false);
//v.startDragAndDrop(null, new View.DragShadowBuilder(v), null, 0); // api24
return true;
}
};
private OnClickListener onClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
if(onDragItemClickListener != null){
onDragItemClickListener.onDragItemClick((TextView) v);
}
}
};
public void setAllowDrag(boolean isAllowDrag) {
this.isAllowDrag = isAllowDrag;
if (this.isAllowDrag) {
this.setOnDragListener(mDragListener);
} else {
this.setOnDragListener(null);
}
}
private View.OnDragListener mDragListener = new View.OnDragListener() {
/**
* ACTION_DRAG_STARTED:當拖拽操作執行時,就會執行一次
* DragEvent.ACTION_DRAG_ENDED:當拖拽事件結束,手指抬起時,就是執行一次
* DragEvent.ACTION_DRAG_ENTERED:當手指進入設置了拖拽監聽的控件范圍內的瞬間執行一次
* DragEvent.ACTION_DRAG_EXITED:當手指離開設置了拖拽監聽的控件范圍內的瞬間執行一次
* DragEvent.ACTION_DRAG_LOCATION:當手指在設置了拖拽監聽的控件范圍內,移動時,實時會執行,執行N次
* DragEvent.ACTION_DROP:當手指在設置了拖拽監聽的控件范圍內松開時,執行一次
*
* @param v 當前監聽拖拽事件的view(其實就是mGridLayout)
* @param event 拖拽事件
* @return
*/
@Override
public boolean onDrag(View v, DragEvent event) {
switch (event.getAction()) {
//當拖拽事件開始時,創建出與子控件對應的矩形數組
case DragEvent.ACTION_DRAG_STARTED:
initRects();
break;
case DragEvent.ACTION_DRAG_LOCATION:
//手指移動時,實時判斷觸摸是否進入了某一個子控件
int touchIndex = getTouchIndex(event);
//說明觸摸點進入了某一個子控件,判斷被拖拽的視圖與進入的子控件對象不是同一個的時候才進行刪除添加操作
if (touchIndex > -1 && dragedView != null && dragedView != DragGridlayout.this.getChildAt(touchIndex)) {
DragGridlayout.this.removeView(dragedView);
DragGridlayout.this.addView(dragedView,touchIndex);
}
break;
case DragEvent.ACTION_DRAG_ENDED:
//拖拽事件結束后,讓被拖拽的view設置為可用,否則背景變紅,并且長按事件會失效
if (dragedView != null) {
dragedView.setEnabled(true);
}
break;
}
return true;
}
};
//手指移動時,實時判斷觸摸是否進入了某一個子控件
private int getTouchIndex(DragEvent event) {
//遍歷所有的數組,如果包含了當前的觸摸點返回索引即可
for (int i = 0; i < mRects.length; i++) {
Rect rect = mRects[i];
if (rect.contains((int)event.getX(), (int)event.getY())) {
return i;
}
}
return -1;
}
//當拖拽事件開始時,創建出與子控件對應的矩形數組
private Rect[] mRects;
private void initRects() {
mRects = new Rect[this.getChildCount()];
for (int i = 0; i < this.getChildCount(); i++) {
View childView = this.getChildAt(i);
//創建與每個子控件對應矩形對象
Rect rect = new Rect(childView.getLeft(), childView.getTop(), childView.getRight(), childView.getBottom());
mRects[i] = rect;
}
}
private OnDragItemClickListener onDragItemClickListener;
public interface OnDragItemClickListener{
public void onDragItemClick(TextView tv);
}
public void setOnDragItemClickListener(OnDragItemClickListener onDragItemClickListener) {
this.onDragItemClickListener = onDragItemClickListener;
}
}
MainActivity
布局文件
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<LinearLayout
android:id="@+id/activity_main"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:layout_width="match_parent"
android:layout_height="36dp"
android:gravity="center_vertical"
android:onClick="addItem"
android:text="添加條目"
android:textColor="?android:attr/textColorPrimary"/>
<com.github.draggridlayout.DragGridlayout
android:id="@+id/selectedChannel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:layout_marginTop="16dp"/>
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="36dp"
android:background="?attr/colorButtonNormal"
android:gravity="center_vertical"
android:padding="5dp"
android:text="點擊選擇頻道"
android:textColor="?android:attr/textColorPrimary"/>
<com.github.draggridlayout.DragGridlayout
android:id="@+id/unSelectedChannel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"/>
</LinearLayout>
</android.support.v4.widget.NestedScrollView>
代碼實現
public class MainActivity extends AppCompatActivity {
private DragGridlayout mSelectedChannel;
private DragGridlayout mUnSelectedChannel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initViews();
initData();
initEvent();
}
private void initViews() {
setContentView(R.layout.activity_main);
mSelectedChannel = (DragGridlayout) findViewById(R.id.selectedChannel);
mUnSelectedChannel = (DragGridlayout) findViewById(R.id.unSelectedChannel);
mSelectedChannel.setAllowDrag(true);
mUnSelectedChannel.setAllowDrag(true);
}
private void initData() {
List<String> selectedChannel = new ArrayList<>();
selectedChannel.add("頭條");
...
mSelectedChannel.setItems(selectedChannel);
List<String> unSelectedChannel = new ArrayList<>();
unSelectedChannel.add("NBA");
...
mUnSelectedChannel.setItems(unSelectedChannel);
}
public void initEvent(){
//設置條目點擊監聽
mSelectedChannel.setOnDragItemClickListener(new DragGridlayout.OnDragItemClickListener() {
@Override
public void onDragItemClick(TextView tv) {
//移除點擊的條目,把條目添加到下面的Gridlayout
mSelectedChannel.removeView(tv);//移除是需要時間,不能直接添加
mUnSelectedChannel.addItem(tv.getText().toString(),0);
}
});
mUnSelectedChannel.setOnDragItemClickListener(new DragGridlayout.OnDragItemClickListener() {
@Override
public void onDragItemClick(TextView tv) {
//移除點擊的條目,把條目添加到上面的Gridlayout
mUnSelectedChannel.removeView(tv);//移除是需要時間,不能直接添加
mSelectedChannel.addItem(tv.getText().toString());
}
});
}
private int index = 0;
public void addItem(View view) {
mSelectedChannel.addItem("頻道" + index++,0);
}
}