Androidでラテラルナビゲーションを実装します

最近、モバイルアプリケーションの設計パターンの中で、最終アプリケーションとのユーザーインタラクションを簡素化する着実な傾向があります。 特に、ジェスチャー認識に特別な重点が置かれ始めました。 ジェスチャーは直感的で自然であり、便利であり、不要なインターフェイス要素を取り除くことができるため、アプリケーションが簡素化されます。



ジェスチャーの適切な使用の良い例は、サイドナビゲーションの人気の高まりです。 パターンとしてのラテラルナビゲーションに関する記事は以前Habréで公開されていましたが、実装については何も言われていませんでした。



残念ながら、横方向のナビゲーションを実装するプロジェクトは非常に少なく、それらのほとんどは時間がかかり、不便です。 幸運なことに、検索を開始してしばらくしてから、 ActionsContentViewプロジェクトに出会いました。 このプロジェクトは、私がかつて遭遇したすべての問題を解決しました。 プロジェクトを慎重に研究した後、自分のニーズに合わせて少し書き直しました。



当初、この記事では、クリックしてサイドメニューを開く方法と、ジェスチャーでメニューを開く方法の両方をペイントしたかったのです。 しかし、記事の終わりに向かって、ジェスチャの処理とそれらのナビゲーションを開くことはかなり多くの問題であり、多くの機能も考慮する必要があることが明らかになりました。 そのような場合の記事は非常に巨大であるため、それを読むのは単に不便です。

そのため、これまではクリックによるサイドメニューの実装のみを説明することにしました。







アプリケーションアーキテクチャ



コンテンツレイヤーとしてフラグメントを使用し、メニューはバックグラウンドのアクティビティに配置されます。

フラグメントの利点は明らかです。実際、アクティビティ内のすべての利点を使用できます。さらに、アクティビティからフラグメントのあるレイヤーはビューとして表示されます。



Activityを静的にします;フラグメント内を渡すときは、フラグメント自体のみが変更されます。 また、同じウィンドウで新しいフラグメントを開始する方法と、メニューを開く/閉じる方法をフラグメントで提供する必要があります。



これを実装するために、フラグメントとアクティビティ間の相互作用のメソッドを記述するインターフェイスを作成します。



import android.support.v4.app.Fragment; public interface SideMenuListener { public void startFragment(Fragment fragment); public boolean toggleMenu(); }
      
      





アクティビティで実装します:

 import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentActivity; public class MainActivity extends FragmentActivity implements SideMenuListener { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } public void startFragment(Fragment fragment) { // TODO Auto-generated method stub } public boolean toggleMenu() { // TODO Auto-generated method stub return false; } }
      
      





コンテンツを含むフラグメントから上記のメソッドにアクセスする必要があるため、Fragmentクラスを拡張して追加します。

 import android.support.v4.app.Fragment; public class ContentFragment extends Fragment { protected void startFragment(Fragment fragment) { ((SideMenuListener) getActivity()).startFragment(fragment); } protected boolean toggleMenu() { return ((SideMenuListener) getActivity()).toggleMenu(); } }
      
      





将来的には、すべてのフラグメントを継承します。



マークアップを作成し、フラグメントの変更を実装します



次に、メニュー自体を模したリストを作成して、入力する必要があります。 また、コンテンツフラグメント自体も必要です。



アクティビティマークアップファイルは非常に簡単です。

 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#888" > <ListView android:id="@+id/menu" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_alignParentTop="true" /> </RelativeLayout>
      
      





記入するだけでなく:

  private String[] names = { "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "" }; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ListView menu = (ListView) findViewById(R.id.menu); ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, names); menu.setAdapter(adapter); }
      
      





今すぐフラグメントを作成して追加します。

 import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.Button; public class TestFragment extends ContentFragment { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.test_fragment, container, true); Button toogle = (Button) v.findViewById(R.id.toggle); toogle.setOnClickListener( new OnClickListener() { public void onClick(View arg0) { toggleMenu(); } }); return v; } }
      
      





  <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="#CCC" > <Button android:id="@+id/toggle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_alignParentTop="true" android:text="Toggle" /> </RelativeLayout>
      
      







このボタンで、あなたが推測したように、メニューを開いたり閉じたりします。



フラグメントを変更するメカニズムを実装します。

 public class MainActivity extends FragmentActivity implements SideMenuListener { private String[] names = { "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "" }; private FragmentTransaction fragmentTransaction; private View content; private int contentID = R.id.content; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); content = findViewById(contentID); // ... } public void startFragment(Fragment fragment) { fragmentTransaction = getSupportFragmentManager().beginTransaction(); fragmentTransaction.replace(contentID, fragment); fragmentTransaction.addToBackStack(null); fragmentTransaction.commit(); } // ... }
      
      







結果のフラグメントをアクティビティの上に追加します。

 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#888" > <ListView android:id="@+id/menu" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_alignParentTop="true" /> <fragment android:id="@+id/content" android:name="com.habr.sidemenu.TestFragment" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_alignParentLeft="true" android:layout_alignParentTop="true" /> </RelativeLayout>
      
      





フレームの準備ができました。これで、横方向のナビゲーション自体を実装できます。



サイドクリックナビゲーション



toggleMenu()メソッドは、メニューの状態に応じて自動的に開いたり閉じたりします。 したがって、状態を保存する必要があります。

また、メニューが開いたときに「到達」する座標の値も必要です。 携帯電話のディスプレイは幅が異なるため、係数を保存し、電話の解像度に基づいて値自体を計算する必要があります。

また、アニメーションの開始と終了の時間をミリ秒単位で示すことをお勧めします。



だから:

 public class MainActivity extends FragmentActivity implements SideMenuListener { private final double RIGTH_BOUND_COFF = 0.75; private static int DURATION = 250; private boolean isContentShow = true; private int rightBound; // .. @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); DisplayMetrics displaymetrics = new DisplayMetrics(); getWindowManager().getDefaultDisplay().getMetrics(displaymetrics); rightBound = (int) (displaymetrics.widthPixels * RIGTH_BOUND_COFF); // .. } }
      
      







次に、メニューをスクロールするクラスの実装について少し説明します。 この目的のために、スクロールをカプセル化するScrollerクラスを使用します。 実際、このクラスは開始点とオフセット値を受け取り、しばらくして特定の数値を生成します。



ほとんどの場合、Scrollerは、自身を再帰的に呼び出すスレッド内で使用されます。 私が出会ったすべての例で、Scrollerはそのように使用されています。

おそらく、別のスレッドで無限ループと組み合わせて使用​​できますが、そのような実装を使用することにしました。



openMenu()およびcloseMenu()メソッドは、メニューの開閉を担当します。 このメソッドは、スクロール開始変数を再初期化し、fling()メソッドを開始します。これは実際にシフトに関与しています。



fling()メソッドでは、一連のチェックの後、Scroller'aのカウントダウンが開始され、その後でストリームが開始されます。

スレッドのrun()メソッドは、2つのアクションを実行します。





実際、クラス自体は内部的に作成されます。

  private class ContentScrollController implements Runnable { private final Scroller scroller; private int lastX = 0; public ContentScrollController(Scroller scroller) { this.scroller = scroller; } public void run() { if (scroller.isFinished()) return; final boolean more = scroller.computeScrollOffset(); final int x = scroller.getCurrX(); final int diff = lastX - x; if (diff != 0) { content.scrollBy(diff, 0); lastX = x; } if (more) content.post(this); } public void openMenu(int duration) { isContentShow = false; final int startX = content.getScrollX(); final int dx = rightBound + startX; fling(startX, dx, duration); } public void closeMenu(int duration) { isContentShow = true; final int startX = content.getScrollX(); final int dx = startX; fling(startX, dx, duration); } private void fling(int startX, int dx, int duration) { if (!scroller.isFinished()) scroller.forceFinished(true); if (dx == 0) return; if (duration <= 0) { content.scrollBy(-dx, 0); return; } scroller.startScroll(startX, 0, dx, 0, duration); lastX = startX; content.post(this); } }
      
      







次に、クラス内のそのようなフィールドを初期化し、toggleMenu()に入力するだけです。

 public class MainActivity extends FragmentActivity implements SideMenuListener { private ContentScrollController menuController; // ... @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); menuController = new ContentScrollController(new Scroller(getApplicationContext(), new DecelerateInterpolator(3))); // ... } public boolean toggleMenu() { if(isContentShow) menuController.openMenu(DURATION); else menuController.closeMenu(DURATION); return isContentShow; } }
      
      







できた ボタンで開くクイックサイドメニューがあります。 唯一のバグ-フラグメントをスクロールしながらメニューがスクロールします。 このバグを解消するには、指のクリックの座標がフラグメント領域に入るかどうかを確認し、それに応じて、イベントが使用されているかどうかを判断する必要があります。



 public class MainActivity extends FragmentActivity implements SideMenuListener { private Rect contentHitRect = new Rect(); // ... @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); content.setOnTouchListener(new OnTouchListener() { public boolean onTouch(View v, MotionEvent event) { v.getHitRect(contentHitRect); contentHitRect.offset(-v.getScrollX(), v.getScrollY()); if (contentHitRect.contains((int)event.getX(), (int)event.getY())) return true; return v.onTouchEvent(event); } }); // ... } }
      
      







これですべてが機能します。

結果として生じるサイドメニューは、さまざまな携帯電話で非常に高速に機能しますが、画面の変更を整理するための既成のアーキテクチャソリューションがあります。



どんなコメントでも喜んでいます。



すぐに使えるソースコード



SideMenuListener.java
 package com.habr.sidemenu; import android.support.v4.app.Fragment; public interface SideMenuListener { public void startFragment(Fragment fragment); public boolean toggleMenu(); }
      
      







ContentFragment.java
 package com.habr.sidemenu; import android.support.v4.app.Fragment; public class ContentFragment extends Fragment { protected void startFragment(Fragment fragment) { ((SideMenuListener) getActivity()).startFragment(fragment); } protected boolean toggleMenu() { return ((SideMenuListener) getActivity()).toggleMenu(); } }
      
      







TestFragment.java
 package com.habr.sidemenu; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.Button; public class TestFragment extends ContentFragment { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.test_fragment, container, true); Button toogle = (Button) v.findViewById(R.id.toggle); toogle.setOnClickListener( new OnClickListener() { public void onClick(View arg0) { toggleMenu(); } }); return v; } }
      
      







MainActivity.java
 package com.habr.sidemenu; import android.graphics.Rect; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentActivity; import android.support.v4.app.FragmentTransaction; import android.util.DisplayMetrics; import android.view.MotionEvent; import android.view.View; import android.view.View.OnTouchListener; import android.view.animation.DecelerateInterpolator; import android.widget.ArrayAdapter; import android.widget.ListView; import android.widget.Scroller; public class MainActivity extends FragmentActivity implements SideMenuListener { private String[] names = { "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "" }; private FragmentTransaction fragmentTransaction; private View content; private int contentID = R.id.content; private final double RIGTH_BOUND_COFF = 0.75; private static int DURATION = 250; private boolean isContentShow = true; private int rightBound; private ContentScrollController menuController; private Rect contentHitRect = new Rect(); @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); content = findViewById(contentID); menuController = new ContentScrollController(new Scroller(getApplicationContext(), new DecelerateInterpolator(3))); DisplayMetrics displaymetrics = new DisplayMetrics(); getWindowManager().getDefaultDisplay().getMetrics(displaymetrics); rightBound = (int) (displaymetrics.widthPixels * RIGTH_BOUND_COFF); content.setOnTouchListener(new OnTouchListener() { public boolean onTouch(View v, MotionEvent event) { v.getHitRect(contentHitRect); contentHitRect.offset(-v.getScrollX(), v.getScrollY()); if (contentHitRect.contains((int)event.getX(), (int)event.getY())) return true; return v.onTouchEvent(event); } }); ListView menu = (ListView) findViewById(R.id.menu); ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, names); menu.setAdapter(adapter); } public void startFragment(Fragment fragment) { fragmentTransaction = getSupportFragmentManager().beginTransaction(); fragmentTransaction.replace(contentID, fragment); fragmentTransaction.addToBackStack(null); fragmentTransaction.commit(); } public boolean toggleMenu() { if(isContentShow) menuController.openMenu(DURATION); else menuController.closeMenu(DURATION); return isContentShow; } private class ContentScrollController implements Runnable { private final Scroller scroller; private int lastX = 0; public ContentScrollController(Scroller scroller) { this.scroller = scroller; } public void run() { if (scroller.isFinished()) return; final boolean more = scroller.computeScrollOffset(); final int x = scroller.getCurrX(); final int diff = lastX - x; if (diff != 0) { content.scrollBy(diff, 0); lastX = x; } if (more) content.post(this); } public void openMenu(int duration) { isContentShow = false; final int startX = content.getScrollX(); final int dx = rightBound + startX; fling(startX, dx, duration); } public void closeMenu(int duration) { isContentShow = true; final int startX = content.getScrollX(); final int dx = startX; fling(startX, dx, duration); } private void fling(int startX, int dx, int duration) { if (!scroller.isFinished()) scroller.forceFinished(true); if (dx == 0) return; if (duration <= 0) { content.scrollBy(-dx, 0); return; } scroller.startScroll(startX, 0, dx, 0, duration); lastX = startX; content.post(this); } } }
      
      







activity_main.xml
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#888" > <ListView android:id="@+id/menu" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_alignParentTop="true" /> <fragment android:id="@+id/content" android:name="com.habr.sidemenu.TestFragment" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_alignParentLeft="true" android:layout_alignParentTop="true" /> </RelativeLayout>
      
      







test_fragment.xml
  <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="#CCC" > <Button android:id="@+id/toggle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_alignParentTop="true" android:text="Toggle" /> </RelativeLayout>
      
      









使用したソース






All Articles