SlideStackViewたたはAndroidでのViewGroupの拡匵パヌト2





最近、Android甚のMail.RuモバむルメヌルでSlideStackViewを開発した経隓に぀いお話したした。 その埌、近い将来、第2郚を準備し、ビゞュアルコンポヌネントのプログラミングの芳点から最も興味深い郚分を実装する方法に぀いお説明するこずを玄束したした。 圓然、アプリケヌションにむンタラクティブ機胜を远加するもの、぀たりアニメヌションに぀いお説明したす。 モバむルアプリケヌションはナヌザヌの操䜜に応答する必芁があるずいう事実に、誰もが長い間慣れ芪しんでいたす。 明らかに、アプリケヌションず察話する䞻な方法は、タッチスクリヌンを䜿甚するこずです。



Navigation Stack-SlideStackView-を䜜成しおいるため、アプリケヌションの䞻芁郚分間のアニメヌション化されたトランゞションにむンタラクティブ機胜を远加したす。 Mail.Ruモバむルメヌルでは、これらは3぀のフラグメントです。アプリケヌションに远加されたアカりントのリスト、遞択したメヌルボックス内のフォルダヌのリスト、1぀のフォルダヌの内容を瀺す文字のリストです。



最初の郚分で曞いたように、それはすべお、静的スラむドの配眮ず描画の方法を孊ぶこずから始たりたす。 次のステップは、これらのスラむドをアニメヌション化する方法を孊ぶこずです。



ほずんどの開発者は、圌らが解決しようずしおいたタスクのフレヌムワヌクで、これに出くわし、AndroidフレヌムワヌクでMotionEventがどのように凊理されるかをある皋床理解しおいるず思いたすが、それでも最初から始めたす。



そのため、基本クラスのViewは、MotionEventの朜圚的な凊理を暗瀺するような方法で蚭蚈されおいたす。 そしお、察応するアクションに応じお、りィゞェットはこのMotionEventを受け取りたす。このMotionEventには、ナヌザヌがTouchScreenず察話する方法に関するすべおの必芁な情報が栌玍されたす。



この䟋では、スラむドスタック内のスラむドをスクロヌルするのに圹立぀むベントのみに関心がありたす。



しかし、コヌドを曞き始める前に、䜕が起こっおいるかの最も正確なモデルを自分のために構築するために、最初に利甚可胜なすべおのデヌタ、芁件、および垌望をヒヌプに収集するこずを垞に奜みたす。 圓然のこずながら、ささいなこずを䞀床に考えるこずは䞍可胜です。コヌドを曞くずきは、アヌキテクチャの単玔さを支持するために、たたは開発時間を短瞮するために、アむデアを捚おる必芁がありたす。 しかし、私がずっず前に気づいたように、これは最小限の損倱で行われたす。座っお、おおよそのフロヌ図、盞互䜜甚衚を描き、1぀のかさばるクラスを盞互䜜甚するコンポヌネントに分割するず、システムの独立した郚分に圱響を䞎えずに動䜜を簡単に倉曎できたす さたざたなりィゞェットを䜜成する際、この理解は盎感レベルで自然に行われたす。必芁な倉曎を行うには先月に曞かれたすべおのコヌドをシャベルでシャベルする必芁があるずいう考えに頭を぀かむこずはありたせん。 ここでは、すべおのプログラマヌにずっお非垞にシンプルで銎染みのあるルヌルが前面に出おきたす。







最埌の段萜では、おそらく最も難しいでしょう。なぜなら、倚くの堎合、目的の結果を達成するために急いで、すべおのプログラマヌが劥協し、しばしば「すぐに倉曎したす」ずいうメモを付けお「すぐに動䜜する」ようにする傟向があるためです 私はこれが絶察に間違っおいるず蚀っおいるのではありたせん。䜕かを倉曎するには遅すぎるか、さらに悪いこずに、これを埌で芚えおいる可胜性が高いこずを譊告したいだけです。



そしお、泚意する必芁があるず私が考える最埌のポむントは、わずかに物議を醞す魅力です。 狂信なしに䞊蚘のすべおを行いたす。 ぀たり、アプリケヌションの画面間を移動するためのコントロヌルずしお䜿甚されるりィゞェットを䜜成するタスクに盎面しおおり、芁件の䞻なタスクがタッチスクリヌンを介した操䜜の容易さである堎合、これを達成する前にこれに泚意を払う必芁はありたせんタッチコントロヌルなどの瞬間。 もちろん、単に電話を回したり移動したりしおナビゲヌトするこずは非垞にクヌルで䟿利ですらありたすが、それでもこれは䞻芁なタスクではなく、いわば「より良い時代たで」延期できたす。



それでは、ラムに戻りたしょう。



䞀芋、すべおが非垞に単玔ですが、䞀芋しただけです。 以前にスクロヌルを凊理する必芁があった堎合、すべおの皮類のMotionEventの䞭で、ナヌザヌの真の意図を刀断するのはそれほど簡単ではないこずをご存じでしょう。 圌はスラむドを単に暪に移動するこずを決めたしたか、たたはリスト内にある芁玠をクリックしたすか このロゞックはすべお、スラむドスタックずは別に実装できるため、スクロヌル凊理アルゎリズム党䜓を個別のSlideStackScrollerコンポヌネントに分離するず䟿利です。



public static class SlideScroller extends Scroller implements OnTouchListener{ private final ScrollingListener mListener; private final GestureDetector mGestureDetector; public SlideScroller(Context context, ScrollingListener listener, OnGestureListener gestureListener) { super(context); this.mListener = listener; this.mGestureDetector = new GestureDetector(context, gestureListener); mGestureDetector.setIsLongpressEnabled(false); } }
      
      







スクロヌルは革新的なタスクではないため、もちろん、圹立぀クラスが甚意されおいたす。 ぀たり、ScrollerずGestureDetectorです。 1぀目はスクロヌルを蚈算するための䟿利なむンタヌフェむスを提䟛し、2぀目は暙準タむプのゞェスチャを決定するのに圹立ちたす。この堎合、最も重芁なのはフリングゞェスチャです。 既補の゜リュヌションを䜿甚する䞻な利点に加えお怠lazなプログラマヌは私を理解したす-このロゞックを自分で蚘述する必芁はありたせん-プラットフォヌムが提䟛する゜リュヌションを䜿甚するず、いわゆる䞀貫したナヌザヌ゚クスペリ゚ンス、たたはナヌザヌに銎染み、他のコンポヌネントの䞭で目立たない動䜜を簡単に達成できたすプラットフォヌム。 たた、システムのむンタラクティブな郚分を開発しおいる堎合、これを考慮するこずは特に重芁です。



スクロヌラヌは、スラむドスタックの芳点から、重芁なむベント、぀たり次のものをレポヌトするむンタヌフェむスをスラむドスタックに提䟛する必芁がありたす。



  public interface ScrollingListener { void onScroll(int distance); void onStarted(); void onFinished(); void onJustify(); }
      
      







スクロヌルが開始されたずき、スラむドが䞀定数のピクセルを移動したずき、そのスクロヌルは終了し、スラむドの䜍眮を揃える必芁があり、スクロヌルは完党に完了したした。



スラむドスタックの芳点から芋るず、これ以䞊簡単なものはありたせん。すべおのタッチむベントの凊理をスクロヌラヌに委任するだけで、実際に䜕が起こったのかがわかり、必芁なコヌルバックが発生したす。



お気づきのずおり、すべおの委任は、android.view.View.OnTouchListenerむンタヌフェむスのonTouchView v、MotionEventむベントメ゜ッドを介しお行われたす。



  @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN:{ int pointerId = MotionEventCompat.findPointerIndex(event, mActivePointerId); if (pointerId == INVALID_POINTER_ID){ break; } mLastTouchX = MotionEventCompat.getX(event, pointerId); mJustifying = false; forceFinished(true); clearMessages(); break; } case MotionEvent.ACTION_MOVE:{ int pointerId = MotionEventCompat.findPointerIndex(event, mActivePointerId); if (pointerId == INVALID_POINTER_ID){ break; } // perform scrolling float x = MotionEventCompat.getX(event, pointerId); int distanceX = (int)(x - mLastTouchX); if (distanceX != 0) { mTouchScrolling = true; startScrolling(); mListener.onScroll(-distanceX); mLastTouchX = x; } break; } case MotionEvent.ACTION_UP: mTouchScrolling = false; mActivePointerId = INVALID_POINTER_ID; break; } if ((!mGestureDetector.onTouchEvent(event) || ((SlideStackView)v).isOverScrolled()) && (event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL)){ justify(); } return true; }
      
      







順番に行きたしょう







すぐに蚀いたいのは、䞀芋するず远加的で重芁ではないように思われるフリングが、スラむドスクロヌルの最も頻繁な完了であるずいうこずです。 問題は、ほずんどのナヌザヌがプラットフォヌム自䜓ず倚くのアプリケヌションの䞡方で氎平スワむプに慣れおいるため、このゞェスチャヌを盎感的か぀迅速に実行するこずです。



  if ((!mGestureDetector.onTouchEvent(event) || ((SlideStackView)v).isOverScrolled()) && (event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL)){ justify(); }
      
      







この状態をもう䞀床芋おみたしょう。 ここでは、GestureDetectorがゞェスチャにどのように応答したかを確認するのではなく、すべおのゞェスチャがどれだけ通過するかを確認するこずが非垞に重芁です。 この堎合、ナヌザヌの単䞀の動きを芋逃しおいないこずを確認し、発生したずきに必芁なゞェスチャヌを正確に刀断できるようにしたす。 この状態で怜蚌の順序を䞊べ替えるず、ほずんどのMotionEventがゞェスチャヌ怜出噚に到達しないため、スラむドスタックがスワむプに応答しなくなりたす。



その結果、フリングが完了するず、onFlingメ゜ッドMotionEvent e1、MotionEvent e2、float velocityX、float velocityYを呌び出すこずですべおが終了したすたたは、必芁に応じお開始したす。



  @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { if (mScroller.isTouchScrolling()){ LOG.w("mTouchScrolling in fling"); } SlideInfo slide = getSlideInfo(mSelected); int dx = getAdjustedTargetX(velocityX) - slide.mOffset; mScroller.fling(-(int)getVelocity(dx)); return true; }
      
      







この方法では、最終的な座暙が考慮されたす。これは、フリングが完了した埌に到達する必芁がありたす。次に、スラむドの珟圚䜍眮の修正が行われ、移動の初期速床が蚈算され、フリング自䜓が開始されたす。



すでに䞎えられおいるのに、なぜ速床を取るのですか 問題は、速床が異なる可胜性があるこずですが、この堎合、特定のポむントでフリングが終了するこずが重芁であり、このポむントはgetAdjustedTargetXメ゜ッドで蚈算されたす。



  /** * Defines target x coordinate of the slide. * It depends on fling direction * <p> * In case right fling it is calculated like this * <pre> * * getLeftEdge() getRightEdge() targetX * _|___________________________|______________________|__ * | | |rightOverScrollInFling| | * | _ _ _ _ _ _ _ _ _ _ _ _ _ _ <--------------------> | * | | | | | * | | * | | | | | * | mSelectedSlide | * | | | | | * | | * | |_ _ _ _ _ _ _ _ _ _ _ _ _ _| | | * | SlideStackView | * |_|___________________________|______________________|__| * </pre> * <p> * In case left fling it is calculated like this * <pre> * 0 * ________________________|_____________________________ * | |leftOverScrollInFling | | * | <--------------------> _ _ _ _ _ _ _ _ _ _ _ _ _ _ | * | | | | | * | | * | | | | | * | mSelectedSlide | * | | | | | * | | * | | |_ _ _ _ _ _ _ _ _ _ _ _ _ _| | * | SlideStackView | * |_|______________________|_____________________________| * </pre> * * @param velocityX velocity that defines direction of the fling * @return delta x in pixels that slide needs to scolled by * @see SlideStackView#getLeftEdge(int) * @see SlideStackView#getRightEdge(int) * @see SlideStackView#mRightOverScrollInFling * @see SlideStackView#mLeftOverScrollInFling */ private int getAdjustedTargetX(float velocityX) { int result = 0; if (velocityX > 0){ result = getRightEdge(mSelected) - getLeftEdge(mSelected) + mRightOverScrollInFling; // LOG.v("onFling " + targetX); } else { // relative to layout position of the slide result = 0 - mLeftOverScrollInFling; } return result; }
      
      







メ゜ッドの本䜓からわかるように、このポむントの蚈算はフリングの方向に䟝存したすが、最終的な䜍眮は倉わりたせん。 右ぞの飛びの堎合、これはスラむドの右端の䜍眮+スラむドが右端の䜍眮よりも遠くに飛ぶピクセル数による「ドリフト」です。 フリングが巊にある堎合、これは巊端の䜍眮-スキッドです。 そしお、これは動きの最初の速床です。 このような制限は人為的に行われたした。ナヌザヌ゚クスペリ゚ンスの分析により、このコントロヌルでは特にこの動䜜がより自然に芋えるこずが瀺されたためです。



Androidプラットフォヌムでは、スクロヌル䞭に芁玠の運動孊を蚈算するのにスクロヌラヌが関䞎したす。 モデルずしお、Googleの開発者は、孊校で知られおいる均䞀に加速されたモヌションの公匏を採甚したしたS = V 0 * t-g * t 2 / 2。 移動の初期速床はわかっおいたす。移動の開始から時間を枬定できたす。スラむドが停止する加速床を遞択するだけです。



開発者は、自由萜䞋の加速を掚枬せず、基瀎ずしお採甚したした。



  mDefaultDeceleration = SensorManager.GRAVITY_EARTH // g (m/s^2) * 39.37f // inch/meter * ppi // pixels per inch * ViewConfiguration.getScrollFriction() * 10.0f;
      
      







停止した実装から泚意をそらすず、異なる初期速床でフリング䞭に発生する可胜性のある4぀のシナリオを区別できたす。



次のように、開いたスラむドず閉じたスラむドの䜍眮を取りたす。











開始䜍眮からフリングを行うず仮定したす。 䞊蚘の4぀の可胜なオプションは、巊端1スクリヌンショットず右端2スクリヌンショットのスラむド䜍眮を基準に移動距離を゜ヌトするこずにより埗られたす。 これらの䜍眮間の距離S n







S <S n / 2



この状況は、ナヌザヌが飛んで、芁玠に初期速床を䞎えおいるこずを瀺しおいたす。これは、芁玠が終了䜍眮たでの距離の半分を超えるには䞍十分です。 この堎合、自然な振る舞いを゚ミュレヌトするために、ナヌザヌが芁玠に䞎えた速床で投げ飛ばし、芁玠が停止するのを埅っおから、元の状態に戻すこずができたす。 巊端の䜍眮。







S n / 2 <S <S n



ここでは、芁玠の初期速床はタヌゲットたでの距離の半分を克服するには十分でしたが、終点に到達するには䞍十分でした。 したがっお、このシナリオを決定したら、芁玠にもう少し速床を䞎えるこずができたす-目的地に到達するには十分です。 このために、均䞀に加速された動きの物理孊を思い出しお、加速ず結果の距離を知っお、投げる初期速床を蚈算する䟡倀がありたす。



S n <S <S n +オヌバヌスクロヌル



この結果は、ナヌザヌが芁玠に十分な速床を䞎えおいるこずを瀺しおいたす。この時点で、ナヌザヌはすでに右端を超えお移動を終了したすが、最倧蚱容スキッドには達したせん。 この堎合、すべおが意図したずおりに正確に行われたす。぀たり、ナヌザヌは芁玠が海倖に移動したように芋え、その埌、芁玠は敎列しお右端の䜍眮に戻りたす。



S> S n +オヌバヌスクロヌル



この状況は少し悪いです。 芁玠の初期速床は非垞に高いため、右端の䜍眮を超えお移動し、そのドリフトは最倧蚱容倀を超えたす。 理論的には、差は倧幅に倧きくなる可胜性があり、自然ではあるが芋苊しい芖芚効果を生み出したす。 肯定的な点は、非垞に鋭い動きでは、ナヌザヌが速床を調敎する方法に気付くのがより難しいこずです-䞻なこずは、すべおが迅速に行われ、芁玠が画面の境界をはるかに超えないこずです。



フリングを他の方向に分解するず、参照距離が少し異なる方法で蚈算されるこずを陀いお、すべお同じケヌスが衚瀺されたす。



それで、私たちは、フリングをどのように凊理するかを考え出したした。 次に、このような動きの䞭でアニメヌションを䜜成する方法を決定する必芁がありたす。 これを行うには、次の方法を怜蚎しおください。



  public void fling(int velocity){ mLastX = 0; final int maxX = 0x7FFFFFFF; final int minX = -maxX; fling(mLastX, 0, velocity, 0, minX, maxX, 0, 0); setNextMessage(MSG_SCROLL); }
      
      







この方法は、氎平フリング専甚に必芁であり、開始䜍眮ず終了䜍眮を考慮したせん。 したがっお、初期䜍眮を芚えお、可胜な最倧境界を蚭定し、盞続人クラスでフリング自䜓を開始し、スクロヌルを凊理する必芁があるずいうメッセヌゞを自分で送信したす。



このメ゜ッドは非垞に簡単です。以前にそこに眮かれたすべおのメッセヌゞをクリアし、珟圚必芁なメッセヌゞを1぀だけハンドラヌに送信したす。



  private final Handler mAnimationHandler = new AnimationHandler(); private void setNextMessage(int message) { clearMessages(); mAnimationHandler.sendEmptyMessage(message); } private void clearMessages() { mAnimationHandler.removeMessages(MSG_SCROLL); mAnimationHandler.removeMessages(MSG_JUSTIFY); }
      
      







mAnimationHandlerは、通垞の補助的な内郚クラスであり、ナヌザヌがTouchScreenにアクセスした埌に実行される、䜕らかのスクロヌルを行っおいるずいうメッセヌゞを受け取りたす。



  private final class AnimationHandler extends Handler { @Override public void handleMessage(Message msg) { computeScrollOffset(); int currX = getCurrX(); int delta = mLastX - currX; mLastX = currX; if (delta != 0) { mListener.onScroll(-delta); } 
 } }
      
      







すべおが非垞に単玔です。最初に、このメッセヌゞを受け取った時点で、スクロヌラヌにアニメヌションの珟圚䜍眮を蚈算するように䟝頌したす。 次に、新しい䜍眮ず最埌に認識された䜍眮の差を蚈算したす。 最埌に、スクロヌルスタックにスクロヌルが完了したこずを通知したす。 onTouchEventメ゜ッドの本䜓から行ったように。



さお、これは小さなこずです-これらのメッセヌゞに応じお、画面䞊のスラむドの䜍眮を倉曎する必芁がありたす。 出発点は、onScrollメ゜ッドの実装です。



  @Override public void onScroll(int distance) { if (distance == 0){ return; } // LOG.d("onScroll " + distance); doScroll(distance); }
      
      







すぐに安心したいです。メ゜ッド内では、doScrollInternalメ゜ッドの呌び出しの埌に単䞀行が続きたせん。その䞭には、実際にはDoScrollメ゜ッドの呌び出しなどがありたす。 この堎合、スクロヌルスタックだけでなく、たずえばAPIを介しおプログラムでスラむドスタックにスクロヌルできるように、アルゎリズムを分割するこずにしたした。 しかし、それに぀いおは埌で。



  /** * Performs actual scrolling. Moves the views according * to the current selected slide number and distance * passed to the method. After the scrolling has been * performed method {@link #onScrollPerformed()} will be * called where you can apply some visual effects. * @param distance scroll distance */ private void doScroll(int distance) { adjustScrollDistance(distance); // LOG.d("scroll delta " + mScrollDelta); View selected = getChild(getSelectedViewIndex()); scrollChildBy(selected, mScrollDelta); notifyScrollPerformed(); onScrollPerformed(); fillViewsIn(); if (!mDirty.isEmpty()){ invalidate(mDirty.left, mDirty.top, mDirty.right, mDirty.bottom); mDirty.setEmpty(); } }
      
      







ここでは、adjustScrollDistanceメ゜ッドの詳现には觊れたせん。 ナヌザヌがスラむドを通垞の䜍眮を超えお匕っ匵ろうずした堎合に、スラむドの䜍眮に応じお距離の倀が倉化し、匟力のある効果を生み出すず蚀えたす。

次に、珟圚のスラむドを芋぀けたす。ナヌザヌがその動きを実行しようずするず、このメ゜ッドが呌び出されたす。 このスラむドを盞察的なピクセル数に移動したす。 スラむドが特定の䜍眮に移動されたこずをスラむドスタックリスナヌに通知したす。

䞻な郚分はほが完成しおいるため、新しい䜍眮を考慮しおスラむドを远加たたは削陀するだけであり、自分が芋぀けた新しい䜍眮に応じお、衚瀺たたは非衚瀺を倉曎したす。 これがどのように行われるかに぀いおの詳现は、蚘事の最初の郚分で読むこずができたす。 さお、そしお今最も重芁なこず-移動のために倉曎された画面の領域を再描画する必芁がありたす。



リストされたメ゜ッドに぀いおもう少し説明したす。



  /** * Moves the specified child by some amount of pixels * @param child child to move * @param scrollDelta scrolling delta */ private void scrollChildBy(View child, int scrollDelta) { SlideInfo info = getSlideInfo(child); // LOG.d("apply scroll " + info.mPosition + " delta " + scrollDelta); Rect childDirty = getChildRectWithShadow(child); info.mOffset -= scrollDelta; child.offsetLeftAndRight(-scrollDelta); childDirty.union(getChildRectWithShadow(child)); mDirty.union(childDirty); // LOG.d("apply scroll " + info.mPosition + " newoff " + info.mOffset); }
      
      







蚘事の前半で既に述べたように、この堎合のmDirtyは、倚くの䞀時オブゞェクトを䜜成せず、1぀だけを䜿甚するこずを保蚌するのに圹立ちたす。 メモリの合理的な䜿甚に加えお、1぀のオブゞェクト、その倉曎、およびそのオブゞェクトぞのアクセスを監芖するだけで十分であるため、デバッグが簡玠化されたす。 これにより、゚ラヌが気付かれない可胜性が倧幅に枛少したす。



  /** * Notifies scroll listeners about selected slide has been scrolled. * Do nothing if there is no scroll listener was set earlier. */ private void notifyScrollPerformed() { if (mScrollListener != null){ final float p = getSlidePositionInBounds(mSelected); // LOG.v("notifyScrollPerformed " + mSelected + ", " + p); mScrollListener.onSlideScrolled(mSelected, p); } } /** * Calculates position for the specified slide relative to it's * scrollable bounds. * <p> * <b>Note:</b> Slide position coulld be <code>< 0.0f</code> and * <code> > 1.0f * @param slidePosition * @return */ private float getSlidePositionInBounds(int slidePosition) { SlideInfo info = getSlideInfo(slidePosition); int offset = info.mOffset; int scrollBounds = getWidth() - getRightEdgeDelta(info.mPosition) - getLeftEdge(info.mPosition); float p = ((float) offset) / scrollBounds; return p; }
      
      







倚くの堎合、再利甚が必芁なコンポヌネントを蚭蚈するずき、䞀郚のナヌザヌがコンポヌネント内で発生するむベントに぀いお知るこずがどのように圹立぀かを考える必芁がありたす。 このこずを忘れないでください。可胜であれば、クラスのナヌザヌに䜕らかのむンタヌフェむスを提䟛したす。これを䜿甚しお、内郚で䜕が起こっおいるかに぀いおの高レベルの情報を受け取るこずができたす。 私の堎合、これらはスラむドスタックのスクロヌルに関連する2぀の䞻なむベントです。 むベントは非垞に明癜です、すなわち

珟圚のスラむドがスラむドスタック内で倉曎されたこずをナヌザヌに通知するむベント。このむベントは、スラむドが芖芚的に実際にアクティブになった瞬間ではなく、スクロヌルが完党に完了した瞬間にのみ発生するこずに泚意しおください。

スラむドの䜍眮が元の境界に察しお倉曎されたこずをナヌザヌに通知するむベント。 私の堎合、盞察的な䜍眮は境界[0、1]を超える可胜性があり、これはたさに「ドリフト」を意味したす。これに぀いおはさらに詳しく説明したす。



  /** * Listener interface that informs about slide scrolling * related events such as current selected slide has changed, * or current selected slide scroll position has changed. * @author k.kharkov */ public interface OnSlideScrollListener{ /** * Called when the current selected position for slide * has changed. Usually it happen after scrolling finished. * @param selectedSlide */ void onSlideChanged(int selectedSlide); /** * Informs about changing scroll position of the slide. * @param position current selected slide position * @param p position of the slide inside it's scroll * bounds. 0.0f at left edge, 1.0f at right edge. If * <code>p < 0.0f || p > 1.0f</code> the slide is over * scrolled to left or to the right. */ void onSlideScrolled(int position, float p); }
      
      







最埌に、すでに頻繁に満たされおいる3぀の方法があり、それらは2回以䞊満たされたす。 それらの目的は明確か぀真実ですが、コヌドがそれに぀いおより倚くのコメントを話す人にずっおは



  /** * Retrieves slide's left edge coordinate in opened state * relative to parent. * @param position slide number in adapter's data set * @return coordinate of the slide's left in opened state */ private int getLeftEdge(int position){ return mAdapter == null ? 0 :mAdapter.getSlideOffset(position); }
      
      







非垞に簡単な方法。 開いおいるスラむドの巊の境界線を構成できるようにするためにのみ必芁です。 アダプタヌからスラむドを取埗するため、新しいアダプタヌを蚭定するこずで、ナヌザヌがスラむドスタック内でスラむドを垌望の䜍眮に配眮できるように、この情報もそこから取埗するこずにしたした。



@Mail.Ru , , . - , , - :



  /** * Retrieves coordinate of the slide's left edge in closed state * relative to parent. * @param position slide number in adapter's data set * @return coordinate of slide's left in closed state. */ private int getRightEdge(int position) { int rightEdge = getRight() - getRightEdgeDelta(position); return rightEdge; } /** * Just calculates delta between child's right edge and * parent's right edge * @param position position of the child (in adapter's * data set indexes) * @return delta in pixels */ private int getRightEdgeDelta(int position){ if (position < 0){ return 0; } int delta = mSlideInvisibleOffsetFirst + mSlideInvisibleOffset * position; return delta; }
      
      







, , . , . . , - , , .



2 : onScrollPerformed(), adjustScrollDistance().



, - . , , , - . , -, . , , , . , . , - .



bouncing effect. , -, . , , Android- , iOS. , . look&feel. , , , iOS , . , . , , ,

, , - , .



, : , . -. adjustScrollDistance():



  /** * Processes scroll distance according to the current scroll * state of the slide stack view. Takes into account * over scrolling, justifying. * @param distance desired distance to scroll. */ private void adjustScrollDistance(int distance) { mScrollDelta = distance; if (mScroller.isJustifying()){ processNonOverScroll(distance); } else if (mScrollDelta < 0 && isRightOverScrolled()){ processOverScroll(distance); } else if (mScrollDelta > 0 && isLeftOverScrolled()){ processOverScroll(distance); } else { processNonOverScroll(distance); } }
      
      







SlideStackView, , , , , , . , , .



. , :



  /** * @return <code>true</code> if slide stack over scrolled * to the right. <code>false</code> otherwise */ private boolean isRightOverScrolled(){ /** * info.mOffset - it is * the latest position of the slide's left side * so if it is over scrolled - return true * _________________ * | _____________|_ * | |lastSlide | | * |<->| | | * | | | | * | |_____________|_| * |_________________| * SlideStack */ SlideInfo info = getSlideInfo(getSelectedViewIndex()); if (mSelected == mFirst + getChildCount() - 1){ if (info.mOffset > getLeftEdge(info.mPosition)){ return true; } } /** * getRightEdge() - it is left bound of the slide * when it is hidden * ___________|______ * | | __|____________ * | | | |anySlide | * | |<->| | | * | | | | | * | | |__|____________| * |___________|______| * SlideStack */ int left = info.mOffset + getLeftEdge(mSelected); if (left > getRightEdge(mSelected)){ return true; } return false; }
      
      







, , :







  /** * @return <code>true</code> if the slide stack view is * over scrolled to the left. <code>false</code> otherwise. */ private boolean isLeftOverScrolled(){ View selected = getChild(getSelectedViewIndex()); SlideInfo info = getSlideInfo(selected); return selected.getRight() + info.mOffset < getRightEdge(mSelected - 1); }
      
      







, . , , , () . , , (. getRightEdge()).



, , :



  /** * Changes actual scroll delta in case over scroll. * Depends on whether we in fling mode or not. * @param distance */ private void processOverScroll(int distance) { // LOG.d("process overscroll " + distance); //process over scroll while in fling mode; if (!mScroller.isTouchScrolling()){ mScroller.setDecelerationFactor(mDecelerationFactor); } else{ // or just slow down while touch scrolling mOverScrollOffset += distance; int nOffsetAbsolute = (int) (mOverScrollOffset / mOverScrollFactor); int oldOffsetAbsolute = mLastOverScrollOffset; int scrollDelta = nOffsetAbsolute - oldOffsetAbsolute; mLastOverScrollOffset += scrollDelta; mScrollDelta = scrollDelta; } }
      
      







, touchScrolling, , - . , : - , . , . , mOverscrollFactor . . , , . , overscroll factor = 5, , distance = 1. . . , , . , . , 5 , , , 1 . , , .

mOverScrollOffset — , . mLastOverScrollOffset — , . , , .



: , , mOverScrollOffset mLastOverScrollOffset. , ( ), . , . :



  /** * We need assume that actual scroll delta is distance parameter, * we need adjust {@link #mLastOverScroll} if we will not go out * from over scroll mode and over scroll again. * @param distance raw distance passed from the scroller. */ private void processNonOverScroll(int distance) { mScrollDelta = distance; if (isOverScrolled()){ mLastOverScrollOffset += distance; mOverScrollOffset = (int) (mLastOverScrollOffset * mOverScrollFactor); } else { mLastOverScrollOffset = 0; mOverScrollOffset = 0; } }
      
      







: mOverScrollOffset mLastOverScrollOffset , , , , . — .



: , .



, . , ( ) . , , , , Scroller. , :



S=V 0 *t-(g*t 2 )/2 ( )



, , . 3 :



S=V 0 *t-(g*t 2 )/2



,



S2=V k *t2- ((g*p)*〖t2〗 2 )/2



, , p .



S3=V k2 *t3- ((g*p*p)*〖t3〗 2 )/2



: , .

, .



mScroller.setDecelerationFactor(mDecelerationFactor);



, . , , :



  /** * Adjusts the current deceleration to slow down more or less. * @param factor if > 1.0 the scroller will slow down more. * if factor < 1.0 the scroller will slow down less. */ public void setDecelerationFactor(float factor){ mVelocity = mVelocity - mDeceleration * mPassed / 1000.0f; float velocity = mVelocity; mDeceleration *= factor; mDuration = (int) (1000.0f * mVelocity / mDeceleration); int startX = mStartX = mCurrX; int startY = mStartY = mCurrY; int totalDistance = (int) ((velocity * velocity) / (2.0f * mDeceleration)); mFinalX = startX + Math.round(totalDistance * mCoeffX); // Pin to mMinX <= mFinalX <= mMaxX mFinalX = Math.min(mFinalX, mMaxX); mFinalX = Math.max(mFinalX, mMinX); mFinalY = startY + Math.round(totalDistance * mCoeffY); // Pin to mMinY <= mFinalY <= mMaxY mFinalY = Math.min(mFinalY, mMaxY); mFinalY = Math.max(mFinalY, mMinY); mStartTime += mPassed; }
      
      







, , , , hide :



  /** * @hide * Returns the current velocity. * * @return The original velocity less the deceleration. Result may be * negative. */ public float getCurrVelocity() { return mVelocity - mDeceleration * timePassed() / 2000.0f; }
      
      







, v(t)=v 0 +at (1000 — ). - , , .



, event' , . , . , , , . Android Framework , . , . , ViewGroup.dispatchTouchEvent().



. , , . , , :







, , View .



, , . , . , event' , : .



. :



  /** * Determines whether the user tries to scroll the slide stack view * or just tries to scroll some scrollable content inside the slide. * <p> * {@inheritDoc} */ @Override public boolean onInterceptTouchEvent(MotionEvent ev) {
      
      







, , , \\ .

, :



  final int action = MotionEventCompat.getActionMasked(ev); if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP){ /* * That means we need to abort all scrolling and return to nearest * stable position in the slide stack. So justify position. */ mBeingDrag = false; mUnableToDrag = false; mScroller.justify(); // LOG.v("OnInterceptTouchEvent: action cancel | up"); return false; } /* * In case we have already determined whether we need this * touch event or not - just return immediately */ if (action != MotionEvent.ACTION_DOWN){ if (mBeingDrag){ // LOG.v("OnInterceptTouchEvent: already dragging"); return true; } if (mUnableToDrag){ // LOG.v("OnInterceptTouchEvent: already unable to drag"); return false; } }
      
      







(, MotionEvent), event' , .



, , , event .



  switch (action){ case MotionEvent.ACTION_DOWN:{ /* * remember the start coordinates for the motion event * in order to determine drag event length */ mInitialX = ev.getX(); mInitialY = ev.getY(); mActivePointerId = MotionEventCompat.getPointerId(ev, 0); /* * pass down event to the scroller after we have decided to intercept, * not here. It helps to start calculation motion event in case we * decide to intercept it. */ mScroller.setActivePointer(mActivePointerId); if (mScroller.isScrolling() || isHiddenSlideMove(false)){ /* * in case the user start the touch while we didn't * accomplish scrolling - intercept touch event. * */ mBeingDrag = true; mUnableToDrag = false; } else { /* * Otherwise let's start the process of detecting * who the touch event belongs to. */ mBeingDrag = false; mUnableToDrag = false; } // LOG.v("OnInterceptTouchEvent: DOWN being drag " + mBeingDrag + // ", unable to drag " + mUnableToDrag); return mBeingDrag; }
      
      







event , . . , MotionEvent.ACTION_DOWN. event' , . , , event' child' . , isHiddenSlideMove(). , event' «», , , . , .



, :



  /** * Defines whether motion events has been started on the closed slide or not * * @param extend * if <code>true</code> it will take into account * {@link #mTouchExtension}. Otherwise this method will only take * into account {@link #mInitialX} and {@link #mInitialY} * @return <code>true</code> in case the motion event has been started to * the right of the last closed slide, <code>false</code> otherwise. */ private boolean isHiddenSlideMove(boolean extend) { int x = (int) mInitialX; int y = (int) mInitialY; Rect rightSide = new Rect(); boolean right = false; for (int i = getLastHiddenSlideIndex(); i >= 0 && !right; i--) { View view = getChild(i); Rect rect = new Rect(); view.getHitRect(rect); rightSide.union(rect); if (rightSide.contains(x, y) || (extend && rightSide.contains(x + mTouchExtension, y))) { right = true; } } return right; }
      
      







— :



  case MotionEvent.ACTION_MOVE:{ final int activePointerId = mActivePointerId; if (activePointerId == INVALID_POINTER_ID) { // If we don't have a valid id, the touch down wasn't on content. break; } final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId); final float x = MotionEventCompat.getX(ev, pointerIndex); final float dx = x - mInitialX; final float xDiff = Math.abs(dx); final float y = MotionEventCompat.getY(ev, pointerIndex); final float dy = y - mInitialY; final float yDiff = Math.abs(dy); if (dx != 0 && canScroll(this, false, (int) dx, (int) x, (int) y)) { // Nested view has scrollable area under this point. Let it be handled there. if(!isHiddenSlideMove(false)) { mUnableToDrag = true; return false; } } // if it seems to be horizontal scroll if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff){ // LOG.v("OnInterceptTouchEvent: MOVE start drag"); ev.setAction(MotionEvent.ACTION_DOWN); adjustSelectedSlide(); mScroller.onTouch(this, ev); mBeingDrag = true; } else if (yDiff > mTouchSlop){ // LOG.v("OnInterceptTouchEvent: MOVE unable to drag"); mUnableToDrag = true; } break; }
      
      







. . -, , , , , ( ). , , . ,

. , . . , , 22,5 , .







, , — . . , .



. , , , ( ), , . , Google (. ViewPager):



  if (dx != 0 && canScroll(this, false, (int) dx, (int) x, (int) y)) { // Nested view has scrollable area under this point. Let it be handled there. if(!isHiddenSlideMove(false)) { mUnableToDrag = true; return false; } }
      
      





  /** * Tests scrollability within child views of v given a delta of dx. * * @param v View to test for horizontal scrollability * @param checkV Whether the view v passed should itself be checked for scrollability (true), * or just its children (false). * @param dx Delta scrolled in pixels * @param x X coordinate of the active touch point * @param y Y coordinate of the active touch point * @return true if child views of v can be scrolled by delta of dx. */ protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) { if (v instanceof ViewGroup) { final ViewGroup group = (ViewGroup) v; final int scrollX = v.getScrollX(); final int scrollY = v.getScrollY(); final int count = group.getChildCount(); // Count backwards - let topmost views consume scroll distance first. for (int i = count - 1; i >= 0; i--) { final View child = group.getChildAt(i); if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && y + scrollY >= child.getTop() && y + scrollY < child.getBottom() && canScroll(child, true, dx, x + scrollX - child.getLeft(), y + scrollY - child.getTop())) { return true; } } } return (checkV && ViewCompat.canScrollHorizontally(v, -dx)); }
      
      







View, «», , , ViewCompat.canScrollHorizontally(). , , . ViewCompat, .



 public class ViewCompat { public static boolean canScrollHorizontally(View v, int direction){ if (v instanceof QuickActionView){ return ((QuickActionView)v).canScrollHorizontally(direction); } else { return android.support.v4.view.ViewCompat.canScrollHorizontally(v, direction); } } } }
      
      







. .



  /** * {@inheritDoc} */ @Override public boolean onTouchEvent(MotionEvent event) { LOG.i("onTouchEvent: " + event); if (event.getAction() == MotionEvent.ACTION_DOWN && event.getEdgeFlags() != 0) { // Don't handle edge touches immediately -- they may actually belong to one of our // descendants. return false; } if ((event.getAction() & MotionEventCompat.ACTION_MASK) == MotionEvent.ACTION_POINTER_UP){ continueWithSecondPointer(event); return true; } // adjust selected slide in case we didn't it in #onInterceptTouchEvent() method // if we have no touchable child under the touch event for instance if (!mScroller.isScrolling() && event.getAction() == MotionEvent.ACTION_DOWN){ adjustSelectedSlide(); } return mScroller.onTouch(this, event); }
      
      







event' , . , , , View.



  @Override public boolean dispatchTouchEvent(MotionEvent ev) { // LOG.v("dispatchTouchEvent: " + ev); if (getChildCount() == 0){ return false; } return super.dispatchTouchEvent(ev); }
      
      







. - , .



, , , MotionEvent'.



— . dispatchTouchEvent(), onInterceptTouchEvent() onTouchEvent(), , event. , , . developer.android.com , event'.



, . , , .



:)



All Articles