この記事では、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はまだ私たちのすべてです。
UPD : code.google.comでの機能リクエスト 。
2.2。 測定とレイアウトのパス
ビューの作成経験がなかったにもかかわらず、このトピックに関する人目を引く記事を時々読んでおり、 「ビューがどのように描かれるか」というトピックに関するドキュメントのセクションも見ました。 後者を信じるなら、すべてが可能な限り単純です:
-
onMeasure()
数回onMeasure()
てサイズを決定します。 -
onLayout()
は、コンテナ内にアイテムを配置するために呼び出されます。 -
onDraw()
が呼び出されてレンダリングされます。
まあ非常に簡単です。 このため、私自身の見解を実現することは難しくないと信じていました。 しかし、それはありました(ネタバレ:著者は、本文の後半のonLayoutメソッドから大きな問題をつかむでしょう)。 ビューを作成した後に推測したヒントとルールのリストを次に示します。
-
onMeasure()
はサイジングのみを目的としています。 他に何かを入れることは全く意味がありません。 その理由は、メソッドが複数回呼び出されるだけでなく、onLayout()
前に再度呼び出されるか、現在の計算サイズが最終かどうかを確実に判断できないためでもあります。
- いくつかの信じられない理由でカスタムビューの記事で言及されていない
onLayout()
、onLayout()
前に呼び出されますが、親によって呼び出されるlayout()
メソッド内で呼び出されます。 一番下の行は、layout()
がsetFrame()
を呼び出し、これがonSizeChanged()
を呼び出し、(setFrame()
終了した後)onLayout()
呼び出されることです。 つまり、onSizeChanged()
メソッド内では、ビュー内のすべての子ビューが必要に応じて配置されているという事実に依拠することはできません。 さらに、onSizeChanged()
はonSizeChanged()
、onSizeChanged()
まだ呼び出されていません。
-
onLayout()
内でonLayout()
自分でmeasureChildren()
呼び出すことができます。 つまり、必要に応じて、測定パスをさらに数回実行できます。
-
onLayout()
の最後のメソッドは、onLayout()
を残すほうonLayout()
良いです。 それ以外の場合は、子ビューがonSizeChanged()
で2回onSizeChanged()
呼び出される状況を作成できます。 運が悪い場合は、完全にループして、16ms(結局60 fps)requestLayout()
が呼び出されるようにすることができます(ネタバレ:作者はここで自分自身を撃ちましたが、それについては後で詳しく説明します)
ビューのトピックについて読んだ記事では、これらのメソッドのそのような説明を見たことはありません。 これに遭遇しないか、これがカスタムビュークラブの最初のルールです
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つの解決策を見つけました:
-
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があります。
- 子を
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)
-それだけです。
UPD : code.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;
UPD : code.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は最高のリフレクションチュートリアルです。
UPD:code.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
UPD:code.google.comのバグレポート。
2.9。ScrollView.setScroll()
後にのみ動作しsuper.onLayout()
ますか?
ScrollView
子どもたち
height
の集合を無視する問題に戻ります。状況は次のとおりです。ピンチが発生するとすぐに、現在のフォーカス(マウスをクリックした場所とズームを行うポイント)を同じままにしたいと思います。なんで? ちょうどそのように、ズームはよりユーザーフレンドリーに見えます。子要素の高さを変更するだけでなく、他のすべてのユーザーが彼から離れる間に、一部のユーザーにズームインすることを考慮してください。
それは難しいことではありません確認します
ScaleGestureDetector.getFocusY()
と
ScrollView.getScrollY()
。それから
ScrollView.setScrollY(newPosition)
、帽子の中でそれを行うのに十分であるように思われます...しかし、いいえ、ビューは最も低い子要素へのズームで奇妙にひきつり始めます:
ここで解決策が見つかりました。次のようになります:実行した時点で
setScroll()
、スクロール位置が子要素のサイズを超えていないかどうかを確認し、超えている場合は可能な最大位置を設定します。また、
setScroll()
ズームで呼び出されるため、次の一連のアクションが取得されます。
- newHeightのカウント
- newScrollPosのカウント
- setLayoutParams()でnewHeightを設定します
- 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つの解決策があります。
- ユーザー40%および50%と同じです。つまり、グループを2.5%の位置に配置します。
- グループを定位置に置く (view1.positionY–view2.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.positionY–view2.positionY)/2
...丸めエラーが発生します 位置はピクセル単位で測定され、これによりポイント数の最後の桁にランダム性が追加されます(実際、すべてはそうではありません。使用される場合、ピクセルではなく、
view.getY()
戻ります。
float
すべて使用
MarginLayoutParams.topMargin
される場合、エラーが発生します。
方法No. 2のこれらすべての欠点を考慮すると、方法No. 1を使用することが決定されましたが、グループビューはユーザービュー間の中央に正確には表示されません。
3.3。 アルゴリズム
それはあなたが歩き回ることができるところです。 私がやった。少なくとも5つの異なる実装があり、「不快な」効果によって何らかの形で影響を受けていたため、新しい方法で何度も問題を解決する必要がありました。
最終的な実装を記述してこれを完了することはできますが、実装のいくつかを説明することをお勧めします。以下に説明するクラスタリングの問題に興味がある場合は、それをどのように解決するか(どのアルゴリズムを発明/使用するか)を考えてから、パラグラフごとに読んでください。私のように、アーティファクトが発生しやすい場合は決定を変更してください。
3.3.1。挑戦する
優れたUXを使用してクラスタリングを行います。
- 境界ビューはランク内に留まる必要があります。
- – ,
zoom in
zoom out
zoom in
- . - / ( №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)
。
同様のアプローチにより、以下が実現します。
View
自分で「棚child
」を保管する必要がある。- 手動で呼び出す必要があり
measure()
、layout()
そしてdrawChild()
のためにchild
適切なタイミングでの、 -
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 2:
4.999.
「未解決」の問題の解決策をパンクに追加しました。