RecyclerView-Adapter用スライドメニューサポートを追加(4編)
16272 ワード
効果図
スライドメニュー
簡単に述べる
前の記事では、現在の
これらの前提があると、スライドメニューにはどのような表現が必要なのか、どのような機能が必要なのかを考え始めました.クライアントの使用はできません.私たちは です.スライドの効果はできるだけIosの効果に頼って、なぜIosと違うのか(誰がAndroidを比較させたのか) をテストしないようにします. itemメニューが開いている間に、他の開いているメニューのitemがメニューを閉じる必要があるかどうかは、スイッチ機能 が必要である.左メニュー、右メニュー(現在の実装では両者が同時に存在することはできない) をサポートする.メニューの個数は柔軟で、数量に制限をしない メニューにはイベントコールバック が必要です.その他のメニュー操作api 上記のニーズがあった後、私たちは開発に着手し始めました.まず、イベント処理が欠かせないと同時に、
1.メニュー関連処理インタフェース宣言
まず、操作インタフェースを定義します.
メニューパッケージクラス
メニュー作成インタフェース
必要に応じて2つの方法を提供しますが、どちらの方法もデータが返される場合は組み合わせられるので、場合によってはどちらかを選ぶことをお勧めします.
メニュークローズインタフェースおよび構成インタフェース
メニュークリックイベントコールバックインタフェース
2.Adapterの拡張開始
操作インタフェースが定義され、拡張が開始されます.
これで私たちは、
viewholderの作成
各itemはメニューを追加する必要があります.私たちが必要とする効果は、元のitemがスライドしている間にメニューが徐々に現れ、自分のメニューが動いていないことです.このように元のitemがメニューの上に完全に覆われているので、ここでは
上記のコードの処理には、a.
メニューイベント処理
以下、メニューのクリックイベントがどのように追加されたかを知るだけでよい.メニューとitemの関連関係
同時に親クラスのイベント応答処理を複写する必要があり、現在のitemのメニューが閉じている場合にのみイベントに応答でき、コードは貼られません.
3. SwipeLayout
上記の説明では、この
メニューの追加と元のitem viewの関連付け
初期化
イベント処理(ACTION_DOWN操作)を指で押すと、 メニューが開いている間(ACTION_MOVE操作)イベントをブロックする必要はありません.これらのイベントは に渡す必要があります.指を持ち上げる(ACTION_UP操作)場合、ここでは比較的複雑であり、 という効果が期待される.
ここでイベント処理のコントロールは
4. SwipeDragHelperDelegate
イベントを
スライドコントロールの設定
メニューコントロールではなく
スライド境界
コントロールの実際の動作をスライド操作で処理し、予想される範囲内であることを保証する必要があります.例えば、右メニューの例を挙げると、スライドできる最大距離はメニューの幅で、縦はスライドできません.
スライド動作
スライド可能な境界が設定された後、demoを実行すると、スライド可能な境界内でどこまでスライドするかがわかります.これは明らかに私たちが望んでいるものではありません.ここでの効果は次のように定義されています.指を離すと、スライドする前にメニューが閉じると、スライドの距離がメニュー幅の20%を超えると、そのままメニューが開き、そうでないとユーザがメニューを開きたくないと判断すると、メニュー が閉じる.指を離すと、スライドする前にメニューが開いている場合は、メニュー を直接閉じる.
ここで補足すると、上記のコードでは
上記の説明およびコードによれば、スライド時に境界条件(ここでは左右の境界を指す)が制限されていることがわかります.つまり、スライド時に最近どの境界に達したかを知る必要があります.
スライド領域
消費可能なイベントのviewが存在する場合、スライド効果は無効になります.この場合、メニューが横スライドのみを定義しているため、ドラッグ可能な領域を限定するには、次の方法を繰り返します(なぜ自分でgoogleしてください):
スライドメニューのサポートはここまでで紹介しましたが、書いてあるのは雲の中の霧の中のもので、一部の場所はあいまいであるべきで、見るだけでは全面的に理解するのは難しいと思います.結局、この機能は処理する細部が多いので、まず実現する効果が何なのかを知ってから読んでください.最後はやはりソースコードに移動して、見ればわかるはずです.
スライドメニュー
簡単に述べる
前の記事では、現在の
Adapter
は機能的に強く、いつでも拡張可能な準備ができていることが明らかになりました.この記事では、既存の機能に基づいてスライドメニューのサポートを追加します.まず、この説明の機能は、ヘッダーとfooter以外の機能について説明します.これらの前提があると、スライドメニューにはどのような表現が必要なのか、どのような機能が必要なのかを考え始めました.
Adapter
のみに対して、RecyclerView
に対して、クライアントにカスタムコントロールを使用するように強制しません.いつも良いRecyclerView
をカスタマイズするべきではありません.さて次の操作は、この機能を実現するために欠かせないツールViewDragHelper
が登場します.これはAndroidシステムに追加されたイベント処理を簡略化するためのツールクラスですが、ここではあまり紹介しないので、知らない自分でgoogleします. :
1.メニュー関連処理インタフェース宣言
まず、操作インタフェースを定義します.
メニューパッケージクラス
public class MenuItem {
//
private int menuLayoutId;
//
@MenuItem.EdgeTrackWhere
private int edgeTrack;
// id
private int menuId;
@Retention(RetentionPolicy.SOURCE)
@IntDef({EdgeTrack.LEFT, EdgeTrack.RIGHT})
public @interface EdgeTrackWhere {}
/**
*
*/
public interface EdgeTrack{
int LEFT = 0;
int RIGHT = 1;
}
}
メニュー作成インタフェース
必要に応じて2つの方法を提供しますが、どちらの方法もデータが返される場合は組み合わせられるので、場合によってはどちらかを選ぶことをお勧めします.
public interface ICreateMenus {
/**
*
* @param viewType item
* @return
*/
List
メニュークローズインタフェースおよび構成インタフェース
public interface ICloseMenus {
/**
*
*/
void closeMenuItem();
/**
* item
*/
void closeOtherMenuItems();
/**
* item ( item)
* @return
*/
boolean hasOpendMenuItems();
}
public interface IMenuSupport {
/**
* menu items
* @return
*/
boolean isCloseOtherItemsWhenThisWillOpen();
}
メニュークリックイベントコールバックインタフェース
public interface OnItemMenuClickListener {
/**
*
* @param swipeItemView
* @param itemView itemview
* @param menuView
* @param position item ( )
* @param menuId item id
*/
void onMenuClick(SwipeLayout swipeItemView, View itemView, View menuView, int position, int menuId);
}
2.Adapterの拡張開始
操作インタフェースが定義され、拡張が開始されます.
SwipeAdapter
という名前を付けて、BaseAdapter
から継承させます.これにより、上記のいくつかの機能がすべて備えられます.このようにまずこのAdapter
で私たちが何をする必要があるかを考えて、目測は以下の2つの方面の内容を処理する必要があります.その他は必要ありません.a.viewholderの作成は私たちがしなければなりません.元のitemはメニューを追加する必要があるので、itemはb.クリックイベントが必要です.メニューもクリックイベントが必要です.また、私たちがクリックしたこのitemがメニューが開いている状態であれば、閉じる必要があるので、クリック時間は複写する必要があります.これで私たちは、
public BaseViewHolder onCreateHolder(ViewGroup parent, int viewType)
を複写する方法から始めます(前の文章を見たら、この方法がどのように来たのか分かります).viewholderの作成
各itemはメニューを追加する必要があります.私たちが必要とする効果は、元のitemがスライドしている間にメニューが徐々に現れ、自分のメニューが動いていないことです.このように元のitemがメニューの上に完全に覆われているので、ここでは
FrameLayout
の容器でメニューと元のitemコントロールを包みます.このFrameLayout
から継承されたコントロールはSwipeLayout
と名付けられています.新しいitemとしてviewholderを作成します.したがって、onCreateHolder()
で処理する必要がある内容は、新しいitemコントロールの作成、メニューの作成、メニュークリックイベントの処理です.SwipeLayout
の論理は後で話しましょう.上記の説明に従って、次のonCreateHolder()
の論理を参照してください.@Override
public BaseViewHolder onCreateHolder(ViewGroup parent, int viewType) {
View itemView = inflater.inflate(viewType, parent, false);
MenuItem mi = this.onCreateSingleMenuItem(viewType);
List
上記のコードの処理には、a.
itemView.setClickable(true);
ここのitemViewはクライアントが作成した最も原始的なviewを指し、クリック可能にするのは、このitemViewがイベントを消費しなければならないためであり、そうしないと、このitemはクリックをキャプチャできないからである.b.メニューのあるviewHolderについてnew BaseViewHolder(swipeLayout, itemView);
という構造方法を使いましたが、なぜこのように使うのでしょうか.私たちのクリックイベントは、新しく作成されたSwipeLayout itemではなく、クライアントが作成した最も原始的なitemにロードされているということを最初に述べたので、私たちは別のイベントを処理するviewパラメータが必要です.BaseViewHolder
のコンストラクタをこのように改造する必要があります// ( )
public View eventItemView;
public BaseViewHolder(View itemView) {
super(itemView);
this.eventItemView = itemView;
}
public BaseViewHolder(View itemView, View eventItemView) {
super(itemView);
this.eventItemView = eventItemView;
}
HeaderFooterAdapter
のinitItemListener
のイベント処理でクリックイベントを処理するのはitemView
ではなく、eventItemView
であり、ダミーコードは以下の通りである.protected void initItemListener(final BaseViewHolder holder/*, final int viewType*/){
holder.eventItemView.setOnClickListener(xxxx);
holder.eventItemView.setOnLongClickListener(xxxx);
}
メニューイベント処理
以下、メニューのクリックイベントがどのように追加されたかを知るだけでよい.メニューとitemの関連関係
List> menus = swipeLayout.getMenus();
はSwipeLayout
に記載される./**
*
* @param holder
*/
private void initMenusListener(final BaseViewHolder holder) {
if (! (holder.itemView instanceof SwipeLayout)) {
return;
}
final SwipeLayout swipeLayout = (SwipeLayout) holder.itemView;
List> menus = swipeLayout.getMenus();
if (null == menus || menus.isEmpty()) {
return;
}
if (null == this.onItemMenuClickListener) {
return;
}
for (final Pair pair:menus) {
pair.first.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int hAll = getHeaderViewCount() + getSysHeaderViewCount();
final int position = holder.getAdapterPosition() - hAll;
onItemMenuClickListener.onMenuClick(swipeLayout, holder.eventItemView, v, position, pair.second.getMenuId());
}
});
}
}
同時に親クラスのイベント応答処理を複写する必要があり、現在のitemのメニューが閉じている場合にのみイベントに応答でき、コードは貼られません.
3. SwipeLayout
上記の説明では、この
SwipeLayout
が新しいitem view(元のitem viewとメニューを載せる)であることがわかり、同時にスライドの操作もその上に作用している(イベントの処理が欠かせない).また、ViewDragHelper
で事件を処理すると前にも言いました.では、SwipeLayout
は次の作業を完了する必要があります.メニューの追加と元のitem viewの関連付け
SwipeAdapter
AdapterのonCreateHolder
では、swipeLayout.setUpView(parent, itemView, menuItems);
を呼び出してSwipeLayout
の初期化を行いました.ここではメニューの操作を簡単に最適化しました.前にメニューが複数サポートされていると言いましたが、ここではメニューコントロールの追加操作はこのように処理されています.メニューが1つしかない場合は、直接追加します.複数のメニューであれば、では、メニューの外側に線形容器が包装されています.メニュー処理のこの部分のコードは比較的に多くて、あまりにも紙面を占めて、しかも技術の含有量がないので、すべて貼るのではなくて、ただ流れを貼るだけでしょう:private List> leftMenus;
private List> rightMenus;
public void setUpView(ViewGroup viewGroup, View itemView, List menuItems) {
this.viewGroup = viewGroup;
this.itemView = itemView;
if (null == menuItems || menuItems.isEmpty()) {
return;
}
//
//1.
//2.
//3. item view
....
}
初期化
ViewDragHelper
同様に初期化方法では初期化が行われ、各SwipeLayout
はジェスチャー操作を処理する必要があるため、ViewDragHelper
を関連付けながら左右メニューに対してViewDragHelper
境界処理を行う必要があるpublic void setUpView(ViewGroup viewGroup, View itemView, List menuItems) {
//
...
delegate = new SwipeDragHelperDelegate(this);
this.helper = ViewDragHelper.create(this, 1.0f, delegate);
delegate.init(helper);
if (this.EdgeTracking == MenuItem.EdgeTrack.LEFT) {
helper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
}else if (this.EdgeTracking == MenuItem.EdgeTrack.RIGHT) {
helper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_RIGHT);
}
}
イベント処理
SwipeLayout
のイベントをViewDragHelper
に依頼して処理する必要があります.ここでの論理は次のとおりです.setIsCloseOtherItemsWhenThisWillOpen()
というインタフェースの方法がありました.つまり、この時点で他の開いているメニューのitemを閉じることを望むなら、このイベントでは閉じる操作をする必要があります. :
.一部のコードは次のとおりです:if (isCloseOtherItemsWhenThisWillOpen) {
if (MotionEvent.ACTION_DOWN == action) {
if (hasOpendMenuItems()) {
closeOtherMenuItems();
}
}
}
ViewDragHelper
処理a. , item ( 、 ), view
b. , item view, , item button ,
c. , ,
d. ACTION_UP
ここでイベント処理のコントロールは
SwipeLayout
であることを知っているので、サブviewがイベントに応答できるかどうかは、イベントを消費する能力があるかどうかに依存する一方で、親コントロールがコントロールをブロックしているかどうかに依存します.以上の説明により、SwipeLayout
のイベントに対する処理ロジックが明確になった.private boolean isCloseOtherItemsWhenThisWillOpen = false;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);
switch (action) {
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
helper.cancel();
RectF f = calcViewScreenLocation(itemView);
boolean isIn = f.contains(ev.getRawX(), ev.getRawY());
if (isIn && delegate.getMenuStatus() == SwipeDragHelperDelegate.MenuStatus.OPEN) {
delegate.closeMenuItem();
return true;
}
return false;
}
if (isCloseOtherItemsWhenThisWillOpen) {
if (MotionEvent.ACTION_DOWN == action) {
if (hasOpendMenuItems()) {
closeOtherMenuItems();
}
}
}
return helper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
helper.processTouchEvent(event);
return true;
}
public static RectF calcViewScreenLocation(View view) {
int[] location = new int[2];
view.getLocationOnScreen(location);
return new RectF(location[0], location[1], location[0] + view.getWidth(), location[1] + view.getHeight());
}
4. SwipeDragHelperDelegate
イベントを
ViewDragHelper
に依頼した後、ビューの操作をコールバックで処理します.読んでいるあなたがこのクラスの使い方を知っていると仮定します(まだよく分からないのは自分でgoogleできます).まず、私たちが望んでいる効果について説明します.スライドコントロールの設定
メニューコントロールではなく
SwipeLayout
の元のitem viewをスライドさせることを期待しているので、swipeLayout.getItemView();
が取得したのはクライアントが最も元のitem viewを作成することである.@Override
public boolean tryCaptureView(View child, int pointerId) {
final View itemView = swipeLayout.getItemView();
if (null != itemView && itemView == child) {
return true;
}
return false;
}
スライド境界
コントロールの実際の動作をスライド操作で処理し、予想される範囲内であることを保証する必要があります.例えば、右メニューの例を挙げると、スライドできる最大距離はメニューの幅で、縦はスライドできません.
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
if (swipeLayout.getEdgeTracking() == MenuItem.EdgeTrack.RIGHT) {
int menuWidth = swipeLayout.getRightMenuWidth();
if (left > 0 && dx > 0) {
return 0;
}
if (left < -menuWidth && dx < 0) {
return -menuWidth;
}
}
return left;
}
スライド動作
スライド可能な境界が設定された後、demoを実行すると、スライド可能な境界内でどこまでスライドするかがわかります.これは明らかに私たちが望んでいるものではありません.ここでの効果は次のように定義されています.
// , ,
private float openMenuBoundaryPercent = 0.2f;
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
final View itemView = swipeLayout.getItemView();
if (releasedChild != itemView) {
return;
}
final int et = swipeLayout.getEdgeTracking();
final int l = Math.abs(itemView.getLeft());
final int menuWidth;
//
if (et == MenuItem.EdgeTrack.LEFT) {
menuWidth = swipeLayout.getLeftMenuWidth();
}else if (et == MenuItem.EdgeTrack.RIGHT){
menuWidth = swipeLayout.getRightMenuWidth();
}else {
menuWidth = 0;
}
final float min = Math.abs(menuWidth * openMenuBoundaryPercent);
final int left;
//
if (l < min || (MenuStatus.OPEN == this.menuBoundaryStatusOfBeenTo && l < menuWidth)) {
left = 0;
} else {
if (et == MenuItem.EdgeTrack.LEFT) {
left = +1 * menuWidth;
}else if (et == MenuItem.EdgeTrack.RIGHT) {
left = -1 * menuWidth;
}else {
left = 0;
}
}
this.helper.settleCapturedViewAt(left, 0);
this.swipeLayout.invalidate();
}
ここで補足すると、上記のコードでは
this.helper.settleCapturedViewAt(left, 0); this.swipeLayout.invalidate();
を用いて位置の設定を行い、内部ではScroller
が実用的であるため、SwipeLayout
では以下の方法を複写して使用する必要がある.@Override
public void computeScroll() {
super.computeScroll();
if (helper.continueSettling(true)) {
invalidate();
}
}
上記の説明およびコードによれば、スライド時に境界条件(ここでは左右の境界を指す)が制限されていることがわかります.つまり、スライド時に最近どの境界に達したかを知る必要があります.
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
this.updateMenuStatus(left);
}
private void updateMenuStatus(int left) {
final int et = swipeLayout.getEdgeTracking();
int menuWidth = 0;
if(MenuItem.EdgeTrack.LEFT == et) {
menuWidth = swipeLayout.getLeftMenuWidth();
}else if (MenuItem.EdgeTrack.RIGHT == et) {
menuWidth = swipeLayout.getRightMenuWidth();
}
//
if (left == 0) {
this.menuBoundaryStatusOfBeenTo = MenuStatus.CLOSED; }else if (Math.abs(left) >= menuWidth) {
this.menuBoundaryStatusOfBeenTo = MenuStatus.OPEN;
}
// item
if (left == 0) {
this.openView.remove(this.swipeLayout);
}else if (0 != menuWidth && left == menuWidth) {
if (!openView.contains(swipeLayout)) {
openView.add(swipeLayout);
}
}
}
// ( , )
@MenuBoundaryStatusOfBeenToWhereprivate int menuBoundaryStatusOfBeenTo = MenuStatus.CLOSED;
@Retention(RetentionPolicy.SOURCE)
@IntDef({MenuStatus.OPEN, MenuStatus.CLOSED})
private @interface MenuBoundaryStatusOfBeenToWhere {}
/**
*
*/
public interface MenuStatus{
int CLOSED = -1;
int DRAGING = 0;
int OPEN = 1;
}
スライド領域
消費可能なイベントのviewが存在する場合、スライド効果は無効になります.この場合、メニューが横スライドのみを定義しているため、ドラッグ可能な領域を限定するには、次の方法を繰り返します(なぜ自分でgoogleしてください):
@Override
public int getViewHorizontalDragRange(View child) {
return swipeLayout.getItemView() == child ? child.getWidth() : 0;
}
スライドメニューのサポートはここまでで紹介しましたが、書いてあるのは雲の中の霧の中のもので、一部の場所はあいまいであるべきで、見るだけでは全面的に理解するのは難しいと思います.結局、この機能は処理する細部が多いので、まず実現する効果が何なのかを知ってから読んでください.最後はやはりソースコードに移動して、見ればわかるはずです.