Android用の独自のビューの作成-何か問題が発生する可能性はありますか?

「夕方、何もありませんでした」-ズーム機能を備えたビューを作成し、ポイント数に応じてランクごとにユーザーを分散させるというアイデアが生まれました。 それ以前は、このレベルの独自のビューを作成した経験がなかったため、タスクは初心者にとっては面白くて簡単に思えましたが...



この記事では、Android SDKとタスク側(クラスタリングアルゴリズム)の両方で直面する問題について説明します。 この記事の主な目的は、いわゆる「カスタムビュー」の作成方法を教えることではなく、作成時に発生する可能性のある問題を示すことです。



このトピックは、このようなものを作成した経験がほとんどない(またはまったくない)人、およびAndroid SDKの「柔軟性」を初めて信じる著者の興味を引く人にとって興味深いものになります。



1.仕組みは?



作品のデモを見る(GIF)




最初に、ビューの作成方法を簡単に説明します。

階層(独自のビューは緑色でマークされています)







RankingsListView







テーブルの先頭には、 RankingsListView



ScrollView



後継)があります。 スクロールを管理し(予期せず、ハァッ?)、ズームし、また、 RankingView



からリストを作成します。



RankingView







RankingView



はランク(左)とUsersView



(右)が表示されます。



UsersView







UsersView



、ごUsersView



、ユーザーの表示と、ユーザーをグループに結合および分離するアニメーションの表示に取り組んでいます。



GroupView









ユーザーとユーザーグループの両方がGroupView



と呼ばれる単一のビューに表示されGroupView



。 グループではなく1人のユーザーが表示されている場合にのみ、緑色の円はありません(その中にグループ内のユーザー数が表示されます)。 さて、右側には「%」記号の付いたユーザー/グループスコアインジケーターがあります。



おそらく退屈な部分で、私たちは問題に目を向けます。



ps 「結論」のソースへのリンク。



2. Android SDK「あなたとゲームをプレイしたい」©Goog ...見た



無害な状態から始めましょう。



2.1。 DataBinding



を使用してカスタムView



内でマークアップを膨張させる



DataBinding



とそのコード生成は驚くべき働きをします:



 WidgetGroupViewBinding binding; … binding = DataBindingUtil.setContentView(this, R.layout.activity_main); // binding.title   
      
      





このマークアップがどんなに複雑であっても、マークアップに示されているすべてのビューはid



によって2行およびbinding



変数を介してアクセスできます。 これ以上:



 @ BindView (R.id.tb_progress) View loadingView; @BindView (R.id.tb_user) View userView; @BindView (R.id.iv_avatar) ImageView avatarView; @BindView (R.id.tv_name) TextView nameView; @BindView (R.id.l_error) View errorView; @BindView (R.id.l_container) ViewGroup containerLayout;
      
      





...そして、 ButterKnife



ような12行以上。 しかし、ちょっと待ってください! setContentView()



Activity



メソッドです。 そして、ビューは何をしますか



現在のビュー内にマークアップを追加するには、コンストラクター内などでinflate(getContext(), R.layout.my_view_layout, this)



メソッドinflate(getContext(), R.layout.my_view_layout, this)



を呼び出す必要があります。 最後のフラグは興味深いです。 現在のビューにマークアップビューを追加します。 これにより、マークアップで例えばLinearLayout



などのルートタグがあり、 LinearLayout



から継承されたビュー内でinflate(…)



を使用しようとすると、階層内に2つのLinearLayoutが取得されます...



...これは、 LinearLayout



の1つが冗長であるため、快適ではありませんが、非常に論理的です。 どうする? これを回避するのは簡単です。 ここで説明するように 、マークアップ内で<merge>



を使用して、ビュー内にあるべきすべてのものでラップする必要があります。



しかし! DataBinding



<merge>



サポートしません。 そのルートタグは<layout>



である必要があり、その内部には<merge>



ではなく単一の子要素が存在する必要があります(実際、 <data>



も存在する可能性がありますが、これはまったく別の話です)。



その結果、現時点では、追加のレイアウトを作成せずに独自のビューでDataBinding



を使用する方法はありません。これは、パフォーマンスに最良の影響を与えません。 ButterKnifeはまだ私たちのすべてです。



UPDcode.google.comでの機能リクエスト



2.2。 測定とレイアウトのパス



ビューの作成経験がなかったにもかかわらず、このトピックに関する人目を引く記事を時々読んでおり、 「ビューがどのように描かれるか」というトピックに関するドキュメントのセクションも見ました。 後者を信じるなら、すべてが可能な限り単純です:





まあ非常に簡単です。 このため、私自身の見解を実現することは難しくないと信じていました。 しかし、それはありました(ネタバレ:著者は、本文の後半のonLayoutメソッドから大きな問題をつかむでしょう)。 ビューを作成した後に推測したヒントとルールのリストを次に示します。





ビューのトピックについて読んだ記事では、これらのメソッドのそのような説明を見たことはありません。 これに遭遇しないか、これがカスタムビュークラブの最初のルールですonLayout()



(除外;使用済み)については話さないでください。



私の場合、 UsersView.onLayout()



内で、ビューのY座標が変化します。これにより、一部のビューが表示され、他のビューが非表示になり、...につながります(右下に注意)。









...底面図をクリップします。 これは遠く離れたところでのみ起こりました。 私は愚かにいじくり回さなければなりませんでしたが、娘のビューは、現在の位置Yでは親に半分しか表示されないと判断したようです。したがって、 Bitmap drawingCache;



トリミングできますBitmap drawingCache;



。 ここで、 measureChildren()



内のonLayout()



の形で、非常に追加の「測定パス」が助けになりました。これにより、ビューはY座標を変更した後にキャッシュを再検討しました。



2.3。 ScrollView



は子のScrollView



許可しません



おそらく、あなたの多くは、 layout_height=match_parent



ScrollView



要素の高さを設定する必要に遭遇し、即座に失敗しました。結果はwrap_content



場合と同じであったため、 fillViewport



フラグの説明があるこのような記事を見つけましか? そして今、質問: fillViewport



フラグと同じ結果を達成するが、子の高さを動的に変更する方法は?



順番に行きましょう。 要素の高さをどのように変更できますか? もちろん、 LayoutParams.height



を通じて、これ以上何もしません。 問題は解決しましたか? いや 高さは変更されていません。 どうしたの? 子ビューでonMeasure()



を検討した結果、 ScrollView



単にパラメーターの設定height



れたheight



を無視し、まず「 UNSPECIFIED



」に等しいmode



onMeasure()



を送信し、次に「 EXACTLY



」およびheight



値でonMeasure()



を送信するという結論に達しました。 ScrollView



のサイズに等しい( fillViewport



設定されている場合)。 またheight



ビューのheight



を変更する唯一の方法はそのLayoutParams



を変更することなLayoutParams



、子は変更されません。



私は2つの解決策を見つけました:



  1. ScrollView



    非常にLayoutParams



    LayoutParams



    を無視するため、 onMeasure()



    メソッドを書き換えて、 onMeasure()



    を追加することができます。



     if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.UNSPECIFIED && getLayoutParams().height > 0) { heightMeasureSpec = MeasureSpec.makeMeasureSpec( getLayoutParams().height, MeasureSpec.EXACTLY); }
          
          





    したがって、 ScrollView



    を彼のために機能させます。 しかし、これのために毎回サブクラスを作成するのは自然です-良い考えではありません。 子FrameLayout



    がどのクラスである必要があるかはわかりません: FrameLayout



    LinearLayout



    RelativeLayout



    など。 可能な各Layout



    とサブクラスに対して行うには-ごみ。 したがって、ここにソリューション番号2があります。



  2. 子をFrameLayout



    ラップし、このFrameLayout



    子のサイズを変更します。


私が使用したそのアプローチ番号2、そしてそれはバタンと働いた。 ただし、これが本当に有効な方法(ハック)かどうかはわかりません。 追加のFrameLayout



FrameLayout



尊重するため、おそらくこのハックは機能しませんが、 ScrollView.onLayout()



内ではあらゆる ScrollView.onLayout()



ゲームが発生し、子のサイズを部分的にしか計算しません。 これは単なる推測に過ぎませんが、スクロールの問題を説明することはできません(ネタバレ:問題については後で説明します)。



そうそう、 ScrollView



にはバグトラッカー上の子供の不変のサイズ問題がありますが、Androidで通常発生するように、2014年以降は新しいステータスのままです。



2.4。 background



近くの丸い角



被験者(左下と左上):









それはもっと簡単に見えるかもしれません- <shape>



drawable



し、 background



を設定すればdrawable



です... タスクは、色がプログラムで変更され、丸みを帯びたエッジが最初と最後の要素にのみ追加されることです。



奇妙なことに、 ShapeDrawable



クラスにShapeDrawable



、予想されるように丸い角を操作するためのメソッドがありません。 しかし、幸いなことに、 RoundRectShape



PaintDrawable



相続人(このクラスが必要な理由-尋ねないでください、あなたはショックを受けています)には、欠落しているメソッドがあります。 これにより、ほとんどすべてのアプリケーションの問題は解決されたと見なされますが、このタスクの問題ではありません。



タスクの詳細は、最大のzoom in



が何でも可能であるということです。つまり、 background



ビューが引き伸ばされ、...

Logcat: W/OpenGLRenderer: Path too large to be rendered into a texture





一定のサイズを超えると、 background



が表示されなくなるだけです。 警告からわかるように、一部のPath



大きすぎてテクスチャに描画できません。 情報源を少し調べてみると、この同志がすべてを責めることになっているという結論に達しました。



 canvas.drawPath(mPath, paint);
      
      





... mPath



角丸長方形が配置されます。



 mPath.addRoundRect(mInnerRect, mInnerRadii, Path.Direction.CCW);
      
      





この問題を解決するには、たとえばColorDrawable



を継承し、 drawPath()



呼び出さずにdraw()



メソッドを呼び出します。



 canvas.clipPath(roundedPath);
      
      





しかし、残念ながら、このアプローチには比較的重大な欠点がありcanvas.clipPath()



canvas.clipPath()



アンチエイリアスの影響を受けません
。 ただし、これを行う別の方法がない場合、これに満足する必要があります。



2.5。 FrameLayout



内にView



FrameLayout





UsersView



を実装しようとすると、このタスクに直面しました。その中で、GroupViewはOY軸に沿った任意の場所に配置できます。



最初に思い浮かんだのは(そしてFrameLayout



内でビューを移動する唯一の可能な方法だと思ったこと)、 topMargin



を使用してtopMargin



パラメーターを変更することtopMargin



。 これは、stackoverflowに対するほとんどの回答(1、2、3)によって証明されています。



このアプローチの欠点は、 LayoutParams



を変更するとrequestLayout()



が呼び出されることです。これは、レイアウトパス自体がより良い時間(次の16msフレーム)まで遅延しても、特にGroupView



内のすべてのGroupView



に対して呼び出される場合、非常に高価な操作です。



しかし、幸いなことに、別の方法がありますView.setY()



stackoverflowへの回答 )。 アニメーションや、不変のLayout



内の子供に最適です。 requestLayout()



呼び出しませんが、ビュー自体のフィールドのみを使用し、(パス)レイアウトのフェーズのみに影響します。 また、新しいビュー位置の計算onLayout()



は、 onLayout()



を呼び出す前にonLayout()



直接行われるため、 requestLayout()



はまったく必要ありません。



2.6。 フォントのインデントを確認しますか? いや? そして彼らはあなたを見る! © includeFontPadding





ユーザーがグループに結合されると、グループ内のユーザー数を表示するアイコンが追加されます。 彼女は次のようになります。







そして、これが彼女の以前の姿です。







違いに気づきましたか? 2枚目の写真を見ると不快感はありますか? もしそうなら、あなたは私を理解しています。 長い間、このアイコンを見ると、思ったほど魅力的に見えない理由を理解できませんでした。 ペイントを拾って、テキストの左/右/上/下のピクセル数を計算すると推測して、私は理由を理解しました-テキストは中央に配置されていません。 テキストの重要性に関して明らかに何かが間違っています。 はい?..いいえ 重力が正しく設定され、他のパラメーターも設定されました。 すべてが完璧に見えました。



一般に、私は苦しむことはありません、解決策は非常に簡単であることが判明しましたが、それをまったく理解していなかったという理由だけですぐに見つけることができませんでした。 ところで、ここに、ソリューションへのリンクがあります 。 要するに、フォント自体にはパディングがあり、それが非中央揃えの理由でした。 includeFontPadding=false



パラメーターをTextView



に追加すると、問題は完全になくなりました。



2.7。 ViewPropertyAnimator



reverse()



メソッドがありません



ランクアイコンを特定のサイズで非表示にするアニメーションを作成したかったのです。 次のようになります。









アニメーションの開始時間を決定するには、現在のサイズをすべてのビューに合わせて必要なサイズと比較し、必要に応じてview.animate().setDuration(fadeDuration).alpha(0 or 1)



ます。



ただし、これは高速fade



アニメーションでのみ有効です。 ただし、 fade



が遅い場合、 zoom out



後にシャープにzoom in



と、ビューのアルファチャネルは1または0ではなく、たとえば0.5になります。 そのため、アニメーションは同じfadeDuration



に対して0.5から0まで再生されます。 アニメーションが2倍遅くなったように見えます。 view.animate()



呼び出す前にview.setAlpha(0 or 1)



ようなものを追加するのは良い解決策ではありません。 ビューは高速ズームでちらつき始めます。



理想的には、 setReverseDuration()



(パラメータなしsetReverseDuration()



の形式のメソッドがあるはずです。これは、「ええ、 fade



アニメーションを500 setReverseDuration()



再生したので、リバースアニメーションも同じ量を再生します」。 しかし、これは残念なことではありません。 私が見つけた唯一の方法は、ペンでそのようなことをすることでした。 私の場合、アニメーションは非常にシンプルだったので、これで私は非表示にできました:



 final float realDuration = iconView.getAlpha() * animationDuration;
      
      



...そしてこれはショーのために:



 final float realDuration = (1 - iconView.getAlpha()) * animationDuration;
      
      





それでは、いつものように: view.animate().setDuration((long) realDuration)



-それだけです。



UPDcode.google.comでの機能リクエスト



2.8。 ScaleGestureDetector



(別名「ピンチ」、別名「ズーム」)



ScaleGestureDetector



API ScaleGestureDetector



非常に優れていlistener



をハングさせてイベントを待機し、すべてのonTouchEvent()



ディテクター自体onTouchEvent()



渡すonTouchEvent()



ください。 しかし、すべてがそれほどバラ色ではありません。



2.8.1。 小さなメモ



まず、 ScaleGestureDetector



自体とScaleGestureDetector



間でonTouchEvent()



イベントを区別する方法はどこにも記載されていません(結局、 ScrollView



の子孫であるRankingsListView



内で発生します)。 その結果、メソッドは次のようになります。



 @Override public boolean onTouchEvent(MotionEvent ev) { scaleDetector.onTouchEvent(ev); super.onTouchEvent(ev); return true; }
      
      





したがって、すべてのstackoverflow-answers( )を実行することをお勧めします。 ただし、このアプローチには欠点があります。 ピンチしてもスクロールは発生します。 些細なことのように思えるかもしれませんが、実際、見栄えをよくしようとしたときに誤ってビューをめくってしまうのは非常に不快です。



super.onTouchEvent()



scaleDetector.onTouchEvent()



間の責任を区別する複雑なソリューションを探すための長くて退屈な検索を探す準備ができていました...そして実際に検索しました...しかし、結果は非常にシンプルでした:



 @Override public boolean onTouchEvent(MotionEvent ev) { scaleDetector.onTouchEvent(ev); if (ev.getPointerCount() == 1) { super.onTouchEvent(ev); } return true; }
      
      





素晴らしいですね super.onTouchEvent()



は、最初にsuper.onTouchEvent()



id



指のid



追跡しないため、指1でスクロールを開始し、指2でスクロールを終了しても、大丈夫です。 残念ながら、Android SDKが再びホイールにスティックを入れることを確信していたので、ソースをグーグルで調べてみて、これを試してみました。 私が言えることは、Android SDKは時には驚きとして機能することです。



第二に 、もしあなたがミクロ最適化疾患に苦しんでいるなら、あなたは子供の意見の​​大きさを注意深く監視する必要があります。 既にご存知のように、ピンチでScrollView



子の内側の子の高さをScrollView



ます。 この子はLinearLayout



、その子はlayout_weight=1



設定されます。 言い換えると、それらはすべて同じ高さです...ではありません。



これはまったく目立つことはありませんが、ピクセルは原子単位であるため、その子ビューは常に同じ高さになるとは限りません。 つまり、 LinearLayout



の高さが1001で、子が2つある場合、そのうちの1つは501で、もう1つは500です。これを目で確認することはほとんど不可能ですが、間接的な結果が生じる可能性があります。



ViewPropertyAnimator



reverse()



について話したとき、ランクアイコンを非表示にするアニメーションを示しました。 チェック自体は簡単TextView



。2つのTextView



ImageView



高さをonLayout()



にまとめ、それらが一度に現在のビューに収まらない場合は、 fade



ImageView



非表示にしImageView



。 また、この合計の高さ(いわゆる「高さのしきい値」)が変わらないことにも注意してください。 その結果、しきい値が500ピクセルの場合、説明されているケースでは、500ビューサムネイルの1つがアイコンで非表示になり、2番目の501サイズは非表示になります。



状況はまれであり、あまり重要ではありません(非表示でないアイコンと非表示のアイコンを同時に検出するためにマウスを非常にゆっくり動かすのはそれほど簡単ではありませんでした(難しくはありませんでした))。 ただし、この動作が気に入らない場合は、修正する方法は1つしかありませんgetHeight()



を使用してしきい値と照合しないでください。 onSizeChanged()



内のonSizeChanged()



、すべての子の最小サイズを見つけ、全員にこの数値としきい値を比較するよう通知します。 私はそれをshared height



と呼び、私にとってはこのように見えます:



 private void updateChildsSharedHeight() { int minChildHeight = Integer.MAX_VALUE; for (int i = 0; i < binding.lRankings.getChildCount(); ++i) { minChildHeight = Math.min(minChildHeight, binding.lRankings.getChildAt(i).getMeasuredHeight()); } for (int i = 0; i < binding.lRankings.getChildCount(); ++i) { RankingView child = (RankingView) binding.lRankings.getChildAt(i); child.setSharedHeight(minChildHeight); } }
      
      





また、しきい値との調整自体は次のとおりです。



 int requiredHeight = binding.tvScore.getHeight() + binding.tvTitle.getHeight() + binding.ivIcon.getHeight(); boolean shouldHideIcon = requiredHeight > sharedHeight;
      
      







UPDcode.google.comでの機能リクエスト



2.8.2。 問題



そして、 ScaleGestureDetector



についてのScaleGestureDetector



ではなく、その問題について話しましょう。



2.8.2.1。 最小ピンチ









I and and and and and and and and-fro ...彼(最小ピンチ)は無効になりません。 コーディング中に、「ピンチをトリガーするための最小距離」についてはまだ知りませんでしたので、ログを少し調べて、これがコードの妨害か何かを確認する必要がありました。 ログによれば、指の間の距離が510ピクセル未満の場合、 ScaleGestureDetector



単にタッチへの応答を停止し、 onScaleEnd()



イベントを送信します。 何らかの種類の「最小限のピンチ」があるという情報は、ドックにもスタックオーバーフローにも存在しませんでした。 エミュレータでデバッグが行われなかった場合、おそらく私はこれに気付かないでしょう。 その上で、ピンチ距離は少なくともミリメートルである可能性があり、これが問題に関する情報を見つける理由となりました。 しかし、それは私が思っていたよりずっと近いことがわかりました。



 mMinSpan = res.getDimensionPixelSize(com.android.internal.R.dimen.config_minScalingSpan);
      
      





そして、そして、そして、そして、そして、そして、そして、そして、そして、そして、そして、スルーを通してcom.android.internal.R.dimen.config_minScalingSpan



もちろん、このクラスは、このフィールドを変更する方法を持たず、もちろん、 com.android.internal.R.dimen.config_minScalingSpan



は、魔法の27mmに等しい 私にとって、最小限のピンチの存在は非常に奇妙な現象のようです。 このように意味があったとしても、それを変更する機会を与えてみませんか?

通常の問題の解決策は、リフレクションです。



2.8.2.2。 坂道



「私」のような「ずさん」が何であるかを知らない人のために、私は翻訳します:

スロップ-ナンセンス、ナンセンス©ABBYY Lingvo



さて、冗談はさておき。「スロップ」とは、ユーザーが誤って画面に触れて、実際に移動/スクロール/ズーム/その他のものを望んでいないと思われる状態です。一種の「偶発的な動きに対する保護」。GIFの説明:









... gifでは、ピンチの開始前に、ピンチとは見なされない最小限の動きが許可されていることがわかります。原則として、良いことに加えて。



しかし...スロップも変更できません!ScaleGestureDetector



次のように説明します。



 mSpanSlop = ViewConfiguration.get(context).getScaledTouchSlop() * 2;
      
      



...などViewConfigeuration







 mTouchSlop = res.getDimensionPixelSize( com.android.internal.R.dimen.config_viewConfigurationTouchSlop);
      
      





この意味はどこから来たのですか? なんで? なんで?それは不明です...一般的に、Android SDKは最高のリフレクションチュートリアルです。



UPDcode.google.comのSlop / Spanの機能リクエスト



2.8.2.3。detector.getScaleFactor()



最初のピンチレース



イベント内onScale(ScaleGestureDetector detector)



で送信され、detector



メソッドを使用detector.getScaleFactor()



してピンチ係数を見つけることができます。したがって、最初のピンチでは、このメソッドは奇妙なジャンプのような値を返します。厳密な移動中の値のログはzoom out



次のとおりです。見た目は、それほどではありませんが、ビューのサイズは常にぴくぴくしていました。アニメーションでこれらのジャンプをどのように見るかは、完全に省略する方が良いでしょう。長い間、私は問題が何であるかを理解しようとしました。実際のデバイスで確認し(突然、エミュレーターの問題が発生することはわかりません)、猫が1ミリメートル余分に死んでいるかのようにマウスを動かしました(まったくわからないので、突然、けいれんして、ログを記録します)-しかし、そうではありません。答えは見つかりませんでしたが、幸運でした。「ブルドーザーから」と言うために、イベントに送ることを決めました



0.958

0.987

0.970

1.009

0.966

0.967

1.006












. > 1



ScaleGestureDetector



MotionEvent.ACTION_CANCEL



初期化直後:



 scaleDetector = new ScaleGestureDetector(getContext(), new ScaleListener()); long time = SystemClock.uptimeMillis(); MotionEvent motionEvent = MotionEvent.obtain(time - 100, time, MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0); scaleDetector.onTouchEvent(motionEvent); motionEvent.recycle();
      
      





...そしてそれはO_oの助けになりました...(ソースコード、Android SDKで)いろいろ調べました。ソースコードでは、最初のピンチにはスロットがありませんでした(はい、これは上記のものです)。なぜこれなのか-私にとっては、このハックが助けたのと同じように、謎のままです。おそらく、彼らは初期化で決心したどこかで、クラスの一部は最初のピンチがすでにスロップテストに合格したと考え、他の部分はそうではないと考え、その結果、「合格/失敗」タイプの熱い戦いの熱中に、それら。¯\ _(ツ)_ /¯©SDK



UPDcode.google.comのバグレポート



2.9。ScrollView.setScroll()



後にのみ動作しsuper.onLayout()



ますか?



ScrollView



子どもたちheight



集合を無視する問題に戻ります。状況は次のとおりです。ピンチが発生するとすぐに、現在のフォーカス(マウスをクリックした場所とズームを行うポイント)を同じままにしたいと思います。なんで? ちょうどそのように、ズームはよりユーザーフレンドリーに見えます。子要素の高さを変更するだけでなく、他のすべてのユーザーが彼から離れる間に、一部のユーザーにズームインすることを考慮してください。









それは難しいことではありません確認しますScaleGestureDetector.getFocusY()



ScrollView.getScrollY()



それからScrollView.setScrollY(newPosition)



、帽子の中でそれを行うのに十分であるように思われます...しかし、いいえ、ビューは最も低い子要素へのズームで奇妙にひきつり始めます:









ここで解決策が見つかりました次のようになります:実行した時点でsetScroll()



、スクロール位置が子要素のサイズを超えていないかどうかを確認、超えている場合は可能な最大位置を設定します。また、setScroll()



ズームで呼び出されるため、次の一連のアクションが取得されます。



  1. newHeightのカウント
  2. newScrollPosのカウント
  3. setLayoutParams()でnewHeightを設定します
  4. setScroll()でnewScrollPosを設定します


問題は段落番号3にあります。実数height



は次の値まで変更されないsuper.onLayout()



ため、setScroll()



期待されることは実行されません。以下のように修正されます。代わりに、setScroll()



これを行います:



 nextScrollPos = newScrollY;
      
      



...そしてonLayout()



afterメソッドでsuper.onLayout()



このメソッド呼び出します:



 private void updateScrollPosition() { if (nextScrollPos != null) { setScrollY(nextScrollPos.intValue()); nextScrollPos = null; } }
      
      





覚えているように、の後にメソッドを呼び出さない方が良いと書きましたsuper.onLayout()



これはまだ事実です。後で、このソリューションに関連する別の問題について説明します。しかし、事実は残っています。これはスクロールジャンプの問題の解決策です。



psただし、「height



子内部の子の変更」でハックを使用しない場合、ScrollView



このような問題は発生しません。しかし、その後、12個のサブクラスの問題に戻ります。



3.ユーザーをクラスタリングするタスク



ここで、タスク自体、アルゴリズム、およびその機能の一部について説明します。



3.1。 国境ユーザーをどうするか?



私は彼らのランクの境界にいるユーザーについて話している:









図では、境界ユーザーはポイント0%、30%、85%のユーザーです(ユーザーは80%のユーザーによってほぼ完全にブロックされました)。最も簡単な方法は、それらを正しい位置に描画することです(以下と同等:h e i g h t s c o r e / 100)、しかし、この場合、他の人のランクに落ち始めます。これを拒否する主な理由はグループ化です。 29%のポイントを持つユーザーを想像してください。彼は「初心者」のランクの境界にいます。しかし、今では彼は突然33%のポイントを持つユーザーと結合し、それらのジョイントグループは31%に対応する位置、つまり「良い」ランクに配置されています。ビューのグループがユーザーのランクを変更できるという考えがあまり好きではなかったので、私はそれを拒否し、上の写真で見たようにランク内のユーザーを制限することにしました。



今後、グループ化アルゴリズムとアプリケーション全体のロジックに多くの問題が追加されることに注意してください。



3.2。 グループにはいくつのポイントがありますか?



ポイントが40%と50%の2人のユーザーがグループに統合されたとします。共同グループをどこに配置し、どのようなポイントで配置しますか?答えは簡単です。グループは45%で、位置はグループポイントに対応しています。



タスクを複雑にしましょう。そして、これらをどのように組み合わせるのですか?







ここには2つの解決策があります。



  1. ユーザー40%および50%と同じです。つまり、グループを2.5%の位置に配置します。
  2. グループを定位置に置く (view1.positionYview2.positionY)/2 , .


アプローチの違いは、最初のケースではグループビューがユーザービューの中央に配置されないことです。2番目のケースではグループ化が中央に配置され、ユーザーエクスペリエンスの観点からはるかに有利です。UXを



優先して、私は2番の方法でそれを行うことにしましたが、このアプローチは重大な欠点を明らかにしました。グループポイントは完全に曲がって計算されます。実際、0%のユーザーは実際には位置0に位置していない(その場合、他の誰かのランクのテリトリーに入ったため)ので、ランク内のすべてのビューがsomeよりも少ないポイントを持つことはできません。また、ズームによって変化します。これは、式(簡易バージョン)によって計算されるためです。minScore





m i n S c o r e = u s e r V i e w H e i g h t / c o n t a i n e r H e i g h t r a n k S c o r e M a x







... userViewHeight



変更されていないが、containerHeight



変更されているため、ピンチの異なるポイントで境界ビューのポイントが異なる。

さらに、式自体:

viewGroup.positionY=(view1.positionYview2.positionY)/2







...丸めエラーが発生します 位置はピクセル単位で測定され、これによりポイント数の最後の桁にランダム性が追加されます(実際、すべてはそうではありません。使用される場合、ピクセルではなく、view.getY()



戻りますfloat



すべて使用MarginLayoutParams.topMargin



される場合、エラーが発生します。



方法No. 2のこれらすべての欠点を考慮すると、方法No. 1を使用することが決定されましたが、グループビューはユーザービュー間の中央に正確には表示されません。



3.3。 アルゴリズム



それはあなたが歩き回ることができるところです。 私がやった。少なくとも5つの異なる実装があり、「不快な」効果によって何らかの形で影響を受けていたため、新しい方法で何度も問題を解決する必要がありました。



最終的な実装を記述してこれを完了することはできますが、実装のいくつかを説明することをお勧めします。以下に説明するクラスタリングの問題に興味がある場合は、それをどのように解決するか(どのアルゴリズムを発明/使用するか)を考えてから、パラグラフごとに読んでください。私のように、アーティファクトが発生しやすい場合は決定を変更してください。



3.3.1。挑戦する



優れたUXを使用してクラスタリングを行います。



  1. 境界ビューはランク内に留まる必要があります。
  2. – , zoom in



    zoom out



    zoom in



    - .
  3. / ( №2).


3.3.2.



アルゴリズム自体を開始する前のすべての実装では、すべてのユーザーはポイントでソートされ、Group



実際には1人のユーザーが1人のグループであるため、各ユーザーがクラス変わることに注意することが重要です。また、「ユーザー」という言葉を使用すると、グループに参加する前の1人のユーザーと、ユーザーのグループの両方を意味できます。



表記について。ユーザーの番号に1つの番号「7」を付け、グループに2つの番号「78」を付けます。グループ番号の数字は、それに含まれるユーザーの数を示します。つまり、グループ78はユーザー7と8で構成されます。



各アルゴリズムには、言葉による説明と擬似コードの両方が与えられます。



3.3.3。アルゴリズム番号1



ユーザーが交差する場合、ユーザーを順番に結合する単純な順次アルゴリズム。



手順:



1. , i i+1.

2.1. – №1.

2.2. , .

3. 1 .








 for (int i = 0; i < groups.size() - 1; ) { if (isInersected(groups[i], groups[i+1])) { groups[i].addUsersToGroup(groups[i+1]); groups.remove(i+1); } else { ++i; } }
      
      





ただし、このアルゴリズムにはUXに問題があります。









最初は十分に近い場合、すべてのユーザーを結合します。これは、スペースの不合理な使用につながります。つまり、アルゴリズムはUXに適合しません。



3.3.4。アルゴリズム番号2



明らかに、問題はすべてのユーザーが順番にチェックされることです。そのため、アルゴリズム番号1をわずかに変更することにしました。これで、ユーザーのリストには少なくとも2つのパスがあります。偶数パスと奇数パスです。両方のパスで交差点が見つからないとすぐに、アルゴリズムは終了します。



手順:

0. i = 0.

1. , i i+1.

2.1. – , . 1.

2.2. , 2.

3. , 1.

4. i == 1 , .

5. 0, i ( 0 1 1 0 – ).








 bool didIntersected, isEvenPass = false; do { didIntersected = false; isEvenPass = !isEvenPass; int i = isEvenPass ? 0 : 1; while (i < (groups.size() - 1)) { if (isInersected(groups[i], groups[i+1)) { didIntersected = true; groups[i].addUsersToGroup(groups[i+1]); groups.remove(i+1); i += 1; } else { i += 2; } } } while(isEvenPass || didIntersected);
      
      





このアルゴリズムは以前の問題を解決しますが、結合後の不正確な切断に関連する(ズーム速度が異なるため)新しい問題が発生します。この問題を「問題21-12」と呼びます(名前については後で説明します)。









ここで何が起こったのかを説明します。アルゴリズムを実行すると、1人と2人のユーザーが交差してグループを形成することが明らかになりました。3番目のユーザーはグループと交差するため、すべてが別のグループを形成します。



ただし、より速いズームでそれらを解除しようとすると、画像の下部に表示されるものが発生します。2人のユーザーは交差しますが、グループを形成しません。



結合の別の実行を分割した後でも、以前は存在しなかったグループ23を取得します。以前は、ユーザーはサイズ2と1のグループに統合されていましたが、今では、分離後、ユーザーはサイズ1と2のグループを形成しています。したがって、名前は「問題21-12」です。これは、ズーム速度に応じてグループがさまざまな方法で結合される場合、UXにとっては悪いことです。つまり、アルゴリズムが適合しません。



3.3.5。アルゴリズムNo. 3



それから、額の中で問題を愚かに解決することは不可能であり、より重い砲を使用しなければならないことが明らかになりました。したがって、ユーザー間の距離(ビュー)に基づいてユーザーを結合することにしました。ユーザー間の距離が小さいほど、最初にユーザーが関連付けられます。また、グループを分割するには、タイプフィールドStack<Group> groupsStack;



入力して、そこに新しいグループをすべて追加します。したがって、切断するときは、スタックの一番上の項目のみをチェックする必要があります。



手順:

0. ( groups.size() – 1, / ).

1. . 0?

YES.1. 2- .

YES.2. . , 2- , .

YES.3. .

YES.4. 1.

NO.1. .








 distances.sortAsc(); while (!distances.isEmpty() && distances.getFirst().distance <= 0) { firstDistance = distances.getFirst(); mergedGroup = new Group(firstDistance.groupLeft, firstDistance.groupRight); groupsStack.push(mergedGroup); distances.removeFirst(); firstDistance.groupLeft.notifyPositionChanged(mergedGroup.position); firstDistance.groupRight.notifyPositionChanged(mergedGroup.position); distances.sortAsc(); }
      
      





「まあ、それで間違いなく十分だ」と思った。 しかし、ありません。それが判明し、問題があります:グループ化の順序が間違っています。「これはどうしたらいいの?!」と思った?はい、非常に簡単です。oo-o-o-oを非常にシャープにズームするとどうなりますか?









すべてのユーザーのシャープズームの結果、ズーム前でもユーザーが互いに十分に近いため、位置は同じになりました。このsortAsc()



ため、ユーザー1と2の間の距離は0ですが、ユーザー2と3の間の距離も0であるため、どうすればよいかわかりませんでした。その結果、グループは間違った順序で参加しました。これは、切断時に特に顕著になります。最初のものは最後にマージされるため12が切断されます(スタックはLIFOです。すべて覚えていますか?)。



3.3.6。アルゴリズム番号3.1



前のアルゴリズムを修正する最も簡単な方法sortAsc()



は、異なる「距離が0」の違いを明確にすることです。そして、これは単に余分を追加することで実行できます。並べ替えるときにチェックし、distance == 0



すぐに2組のユーザーがいる場合は、それらを比較しgroup.scoreAfterMerge



ます コードで説明したように、前のアルゴリズムの実装で使用されたGroupsDistanceをソートするための比較関数:



 @Override public int compareTo(@NonNull GroupsDistance obj) { GroupsDistance a = this, b = obj; final int distancesComparison = MathUtil.Compare(b.distance, a.distance); if (distancesComparison != 0) { return distancesComparison; } else { final int namesComparison = a.leftGroup.name.compareTo(b.leftGroup.name); return namesComparison; } }
      
      





...これをextに置き換えます。の比較group.scoreAfterMerge







 @Override public int compareTo(@NonNull GroupsDistance obj) { GroupsDistance a = this, b = obj; final int distancesComparison = MathUtil.Compare(b.distance, a.distance); if (distancesComparison != 0) { return distancesComparison; } else { final int scoresComparison = MathUtil.Compare(a.scoreAfterMerge, b.scoreAfterMerge); if (scoresComparison != 0) { return scoresComparison; } else { final int namesComparison = a.leftGroup.name.compareTo(b.leftGroup.name) return namesComparison; } } }
      
      





確かに、user.score



それらgroup.scoreAfterMerge



は常に等しくありません(そして、それらが等しい場合、それらが結合される順序に違いはありません-すべて同じであることは決して切断されません)。これは追加を意味します。比較score



は問題を解決するのに十分でなければなりません...しかし、いいえ、そしてこの場合には欠陥があります。



問題の状態を覚えておいてください:「境界ビューはランク内に留まる必要がありますか?」問題はそこにあります。この条件を満たさない場合、distance



ユーザー間は常に比例して変化し、ユーザーのビューの位置に明確に表示されますがscore



、そうではありません。この条件により、境界ビューでは位置の依存性が破られますscore



。これはかなり明白ではないため、このような「違反」の詳細な例を次に示します。



ユーザービューの高さが100であるとします。ユーザーの位置が0の場合、その下の座標は100です。[0、100]、つまり[start_view、end_view]の形式でユーザーをマークします。1D空間で作業することを考慮してください。2つのビュー([0、100]と[60、160]など)の交差を決定するには、ビューNo. 1の終わりからビューNo. 2の始まりを引きます。100 - 60 = 40



したがって、ランク1000と3ユーザーの高さのビューがあります。注:ユーザー3では、ランクの境界を超えることは許容できないため、位置は[1000、1100]ではありません。したがって、彼のビューは、ランクビュー内の最も近い位置に移動されました。ズームで、右...我々は0.5のズーム倍率(ダンパー品質スチール寸法500)を実行する前に、№1と№2№2と№3の間、90であるとの距離が10最後のクロスであるように思わ:№1の間の距離をと2番は-5、2番と3番の間は-95です。 No. 2とNo. 3は、No。1とNo. 2よりも早く合流しました。ズームを元に戻し、今度は係数0.2でズームします(ランクのビューのサイズは200になりました)。



№1 60%: [600, 700].

№2 79%: [790, 890].

№3 100%: [900, 1000] ( : [1000, 1100]).














№1 60%: [300, 400].

№2 79%: [395, 495].

№3 100%: [400, 500] ( : [500, 600]).












№1 60%: [100, 200] ( : [120, 220]).

№2 79%: [100, 200] ( : [158, 258]).

№3 100%: [100, 200] ( : [200, 300]).








No. 1とNo. 2の間の距離は0、No。2とNo. 3の間の距離も0です。アルゴリズムが示すように、user.scoreと...のビューを比較します。かなり突然、No。1とNo. 2はNo. 2よりも早くグループに参加しました3つ目は、切断されると最終的にぎくしゃくします。



状況はかなり総合的に見えるかもしれませんが、ランクの境界だけでなく、かなりシャープなズームを行うと中央でも発生します。たとえば、国境で問題を示す方が簡単でした。



「このアルゴリズムは適切ではありません。何をすべきか?ユーザービュー間の距離を並べ替えて比較するよりも優れているのは何score



ですか?!」-私は問題を見つけるときに考えました。



3.3.7。アルゴリズムNo. 4



時間を作りましょう。正確には、連続体。これは、コンピューター物理学の分野ではかなりよく知られているタスクです。長すぎるセグメントで時間を量子化すると、オブジェクト「A」はオブジェクト「B」と交差し、高速すぎるためこれは検出されません。以下に例を示します。



O BのBのE のT A P 、O 、S iは、tは、I 、O 、N = 0 sはP E E D = 0を

OのBのB 電子のR BがP 、O 、S iは、tは、I 、O 、N = - 100 S PのEのEのD = 10 12

(我々はナノ秒単位で時間を量子化する場合であっても10 - 9は次のように)、次いで、次の瞬間«B»-time位置が考慮されます。

ベッドとp個のOは、sのiは、tはiはoをN = B p個のOは、sのiは、tはiはoをN + B S P EとEのD * tはiがmをE DとEのLをtの= - 100 + 10 12 * 10 - 9 = - 100 + 10 3 = 900

であり、我々が得ました:

b B 電子トンのベッドをとP O S iのtは、私はoをN = 900 S P E EのD = 10 12



«Bは»ちょうど«A»を経て通過し、その位置は会ったことがなかったので、我々は、通知をしないでください近くさえありませんでした。



クラスタリングの問題も同じ問題です。移動の速度ではなくズームの速度であり、オブジェクトの位置ではなくビューの位置です。一般に、新しい最後のアルゴリズムの本質は、ユーザービュー間の距離ではなく、height



これら2人のユーザーが交わるところで!次に、このの配列をソートwillIntersectWhenHeight



し、前のアルゴリズムの距離の配列と同じことを行うだけです。主なものは、 -側(とも呼ばれることを忘れてはいけないborder



bound



。)は、ユーザー/グループの位置が異なって残りの部分よりも変更しますそれらと交差する場合は、個別に検討する必要があります。



アルゴリズムのステップは以前と同じwillIntersectWhenHeight



であるため、クラスの 'a をカウントする関数のコードのみを提供しますGroupCandidates



。おそらく、グループではなく配列ではなくバイナリツリーを使用したことに注意することが重要です。これにより、アルゴリズム自体とクラスターアニメーションの両方のロジックを簡素化することができました。



 private float getIntersectingHeght() { float intersectingHeight; final Float height = calcIntersectingHeightWithNoBound(); final boolean leftIsBorder = leftGroup.isLeftBorderWhen(height), rightIsBorder = rightGroup.isRightBorderWhen(height); if (!leftIsBorder && !rightIsBorder) { intersectingHeight = space; return; } if (leftIsBorder && !rightIsBorder) { intersectingHeight = (itemHeight + itemHalfHeight) / right.getNormalizedPos(); return; } if (!leftIsBorder && rightIsBorder) { intersectingHeight = (itemHeight + itemHalfHeight) / (1 - left.getNormalizedPos()); return; } if (leftIsBorder && rightIsBorder) { intersectingHeight = ((float) (itemHeight + itemHeight)); return; } return intersectingHeight; } private Float calcIntersectingHeightWithNoBound() { return itemHeight / (right.getNormalizedPos() - left.getNormalizedPos()); }
      
      



注:getNormalizedPos()



ランク内の正規化された(つまり、0から1)位置を返します。




このアルゴリズムは、私が思いつくすべてのチェックに合格し、非常に軽快な速度を示しました。一般に、問題は解決されます。



4.未解決の問題UPD:すでに解決済み)



そして今、悪いことについて。最初は、すべての問題を解決した後に記事を書くつもりでしたが、時間がないために「コメントで役立つなら時間を節約できます」と考えたため、そのままレイアウトすることにしました。時間が自由になったら、これらの問題に戻ります。忘れない場合は、ここで解決策を追加します。



4.1。ScrollView.setScroll()



内部ではScrollView.onLayout()



、呼び出し後にのみ正常に動作しますsuper.onLayout()



(この子の内側の子のサイズを変更した場合ScrollView





この問題は以前に説明されています。私はScrollView



知覚する方法を見つけませんでしsetScroll()



super.onLayout()



なぜこれが重要なのですか?のケースrequestLayout()



。この呼び出しは、「レイアウトパスを作成する必要があります」タイプのフラグを設定し、次の16msフレームまで計算ミスを延期します。つまり、どれだけrequestLayout()



呼び出さなくても、呼び出すsuper.onLayout()



(子ビューを配置してフラグを削除する)まで、レイアウトパスは生成されません。



ただし、requestLayout()



super.onLayout()



突然呼び出された場合、フラグは再度設定されます。 (16msの経由)、次のフレームのレンダリング中に、原因となる新しいレイアウトパス、があることを、この手段ScrollView.onLayout()



再度要求され、requestLayout()



後にsuper.onLayout()



も...そして、すべての16msのフレームは、完全な再集計となりLayout



、画面上のすべてのvyushekさん。要するに、静かな恐怖。しかし、それだけで行うための試みの後に発生した場合requestLayout()



の後super.onLayout()



、およびScrollView.setScroll()



が発生しない場合requestLayout()



、問題は何ですか?しかし、ここにあります:



4.2。View.setVisibility(GONE)



原因requestLayout()





アルゴリズムの現在の実装(連続体を使用)は非常に高速に動作しますが、レンダリングビューの実装は動作しません。理想的には、ユーザー/グループのリサイクルビューを追加する必要があり、画面外にあるビューをレンダリングしようとしないでください。



これView.getLocalVisibleRect()



は、(可視ウィンドウの座標を返す)inside を使用して行うことができUsersView



、画面にヒットする各ビューに対して、groupView.setUsersGroup(group)



insideを使用しますgroupsCountView.setVisiblity(…)







しかし!ScrollView.setScroll()



この呼び出しは結果を変更するため、作成後に使用する必要がありますView.getLocalVisibleRect()



。そして、ScrollView.setScroll()



super.onLayout()



呼び出されgroupView.setUsersGroup(group)



原因となる可能性View.setVisiblity(GONE)



があるためrequestLayout()



、前の問題で説明した無限ループが発生します。



さらに、各スクロールでズームが表示されない場合でも、表示されるウィンドウは変更されます。つまり、他のビューを表示する必要があります。もう一度View.getLocalVisibleRect()



呼び出すとView.setVisiblity(GONE)



呼び出しつながりますrequestLayout()



。つまり、各スクロールでそれが起こりますrequestLayout()



-これは単に考えられません



私はこれをさまざまな方法で解決しようとしましたが、requestLayout()



内部のメソッドをオーバーライドした場合でもGroupView



、半効果的な結果が得られます(内部の子を表示しますGroupView



定期的に「ジャンプ」し始めます...説明するのは難しいです。理由がわかりませんでした)、requestLayout()



私はそのようにあまり好きではありません。彼はいわば「厚かましすぎる」。おそらく別の解決策があるかもしれませんが、Googleとともにstackoverflowはパルチザンとして沈黙しています。



4.999。記載されている問題を解決する



上記の問題は、どういうわけかrequestLayout()



呼び出されるものに関連しています(または、行き詰まることさえあります)。コメントで正しく指摘されているように、RecyclerView



それを使用すれば、すべての問題を回避できますLayoutManager







しかし、私の目標はグラフィックではなくRecyclerView



グラフィックで作業することだったので、別の方法が見つかりました。だから、requestLayout()



くしゃみ(たとえばchild.setVisibility(GONE)



によって引き起こされる強迫観念を取り除くには、... child



Layout



何かに追加しないでくださいそれは原因にはありません:layout.addView(myChild)





同様のアプローチにより、以下が実現します。

  1. View



    自分で「棚child



    を保管する必要がある
  2. 手動で呼び出す必要がありmeasure()



    layout()



    そしてdrawChild()



    のためにchild



    適切なタイミングでの、
  3. child



    は完全に独立しているため、それらを変更してもが発生しませんrequestLayout()





上記のすべての問題を解決できるのは段落3です。そして-それは小さなものです。助けを借りてView.getLocalVisibleRect()



トラック現在画面上に表示、ユーザー/グループとそれらだけが/分離/描画をグループ化する原因について。



psここでは問題がなかったわけではありませんが、たとえば、以前に電話していなかったとしても、何時onScrollChanged()



に電話できるのか知っonLayout()



ていましたsetScrollY()



か?私の場合、これは彼がView.getLocalVisibleRect()



戻ってきたという事実につながったfalse



(彼らは、UsersView



今では画面上にない)。私はこれが一般的にどのように可能であるかを理解するために自分を苦しめなければなりませんでした。recycling



'omはgithubにあります。古い携帯電話の1,000人のユーザーでも非常に高速に動作します。しかし、すでに多くの人々が減速し始めています。ここでは、クラスタリングコードをNDKに移植するだけで役立ちます。



5.結論



タスクは、私が想像したよりもはるかに複雑で興味深いことがわかりました。交差点アニメーションの実行方法を学びたかっただけですが、最終的にはアルゴリズムを使用し、SDKのソースなどを調べました。 Androidに飽き飽きする必要はありません。



ユーザーがランクの境界を超えてはならないという条件により、ほとんどのアルゴリズムの問​​題がクロールされました。アルゴリズムを実行している間、この条件を何度も除外したかったのですが、-_- "...でも、私は満足しています。あなたも願っています。



アプリケーションコードはgithub.com/Nexen23/RankingListViewにあり



ます。 「未解決の問題」の解決策-コメントを書きます。解決策を確認し、問題がなければテキストを更新します



UPD 1:Mr. Artem_007が提案したとおり、上記の問題への機能リクエスト/バグレポートのリンクを追加しました。

UPD 24.999.



「未解決」の問題の解決策をパンクに追加しました



All Articles