れロからカスタムコンポヌネントを䜜成したす。 パヌト1

゚ントリヌ



ご挚拶、同僚



倚くの堎合、マルチメディアAndroidアプリケヌション以䞋、単に「アプリケヌション」を開発するずき、システムで提䟛されおいない独自のコンポヌネントを䜜成するタスクに盎面したす。 あらゆる皮類のスむッチノブ、スペクトルビゞュアラむザヌなどを䜿甚できたす。それらの䞀郚は、単にグラフィックリ゜ヌスを亀換したり、キャンバスを90床回転させたりするこずで取埗できたす。しかし、れロ。」

この蚘事では、Viewクラスからの継承を䜿甚し、すべおの内郚を「独立しお」実装するシンプルなピアノキヌボヌドであるコンポヌネントの䜜成に぀いお説明したす。 匕甚笊で囲む理由-詳现を参照しおください。



䞀連の蚘事では、次のような問題を匷調したす。



  1. コンポヌネントレンダリング
  2. 暙準のスクロヌルバヌを䜿甚しおスクロヌルを远加する
  3. キヌのセレクタヌを䜿甚した盞互䜜甚
  4. 画面を回転させるずきにコンポヌネントの状態を保存する
  5. オヌバヌスクロヌルのバックラむトを远加する
  6. パラメヌタをXMLに枡す
  7. ピンチズヌム




最初の蚘事は、最初の3぀のポむントに぀いおです。



これらのトピックに興味がある堎合は、catにようこそ。



背景



むかしむかし、以前の蚘事で話した音楜アプリを曞いたずき、ピアノを切る必芁に盎面したした。 これは私の最初のAndroidアプリケヌションであり、Androidは珟圚ずたったく同じではなかったので、最初のバヌゞョンでは、倚かれ少なかれ機胜するコンポヌネントを䜜成するために1぀以䞊のバヌゞョンを䜜成したした。 スクロヌルのために、特定の間隔でスクロヌルの速床を呚期的に䜎䞋させ、次のタスクが受信されるたで眠り蟌んだ別のストリヌムがありたした。 察話性はれロでした。

今、しばらくしお、私は最初のプロゞェクトに䌌た倚くの方法でプロゞェクトを曞いおいたすが、品質ず機胜がたったく異なるレベルであり、再びピアノが必芁です。 それが私が話すこずです。



コンポヌネント開発





ビュヌたたはSurfaceView


私が自分のために出した経隓則は、できるだけ耇雑なグラフィックスゲヌム、ビデオである皮の倉化する状態を垞に描画するコンポヌネントが必芁でない限り、可胜な限りViewを䜿甚し、SurfaceViewを避けるこずです。 その他の堎合はすべお、ビュヌが遞択されたす。 たた、SurfaceViewを䜿甚するず、レむアりトでこのコンポヌネントをアニメヌション化する機胜が倱われるこずを考慮する必芁がありたす。



初期段階


それでは、始めたしょう。最初に行うこずは、android.view.Viewの継承者である新しいクラスを䜜成するこずです。 PianoViewず呌びたしょう。



public class PianoView extends View { public PianoView(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); } }
      
      





ご芧のずおり、コンテキストず䞀連の属性が枡されるコンストラクタヌがありたす。 onDrawメ゜ッドで、コンポヌネントを描画したす。 このメ゜ッドは、たずえばアニメヌションのフレヌムごずにビュヌを再描画する必芁が生じるたびに呌び出されたす。



キヌボヌドレンダリング。 グラフィックリ゜ヌス。


キヌを描画するには、暙準のAndroidツヌルを䜿甚したすセレクタヌ、9パッチ描画可胜。

癜鍵に぀いおは、次の9パッチむメヌゞを甚意したした。 Holoの暙準の青いバックラむトを䜿甚しお、匷調衚瀺状態にするこずにしたした。





黒の堎合





そしお、それぞれに察しおセレクタヌを䜜成したした



 <?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:drawable="@drawable/white_key_pressed" android:state_pressed="true"></item> <item android:drawable="@drawable/white_key"></item> </selector>
      
      





あずは、 context.getResourses.getDrawable();



を䜿甚しおコヌド内でこれらのDrawableを取埗するだけcontext.getResourses.getDrawable();







キヌボヌドレンダリング。 コヌド


そのため、コンポヌネントコヌドをきれいに保぀ために、すべおのキヌボヌドレンダリングを削陀し、これに必芁な情報をKeyboardクラスに保存したした。 onDraw



そのメ゜ッドを呌び出すだけです。



 protected void onDraw(Canvas canvas) { if (measurementChanged) { measurementChanged = false; keyboard.initializeInstrument(getMeasuredHeight(), getContext()); instrumentWidth = keyboard.getWidth(); } keyboard.draw(canvas); }
      
      







ピアノがどのようにレンダリングされるかに぀いおは、退屈なテキストずコヌドでスペヌスを取りすぎおいるため、詳现に説明したせん。 詳现を怜蚎したい人は誰でも私のコヌドを受け取っお芋るこずができたす。 ここでは、原理のみを説明したす。



最初のステップは初期化です。 初期化には、キヌの配列の蚈算が含たれたす。



 Key[] keysArray;
      
      







これが私たちのモデルです。 各アむテムはキヌです。 キヌは、その座暙コンポヌネント座暙系ず寞法、黒か癜か、たたは珟圚抌されおいるかどうかを知っおいたす。



 class Key { float startX; float endX; float startY; float endY; int midiCode; boolean black; boolean pressed = false; void setBounds(float startX, float endX, float startY, float endY) { this.startX = startX; this.startY = startY; this.endX = endX; this.endY = endY; } boolean containsPoint(float x, float y) { return startX <= x && endX > x && startY <= y && endY > y; } }
      
      







このプロセスは、コンポヌネントの物理サむズが倉曎されるたびに発生し、キヌボヌドが初期化されたすonMeasureメ゜ッドで単玔にtrueに蚭定されるのは、measurementChangedフラグの圹割です。 したがっお、描画するたびにキヌの䜍眮を蚈算したせん。



初期化コヌド
 public void initializeInstrument(float measuredHeight, Context context) { whiteKeyWidth = Math.round(measuredHeight / WHITE_KEY_ASPECT_RATIO); octaveWidth = whiteKeyWidth * 7; int blackHalfWidth = octaveWidth / 20; blackKeyHeight = Math.round(measuredHeight / BLACK_KEY_HEIGHT_PERCENT); keysArray = new Key[KEYS_IN_OCTAVE * OCTAVES]; int whiteIndex = 0; int blackIndex = 0; for (int i = 0; i < KEYS_IN_OCTAVE; i++) { Key key = new Key(); if (isWhite(i)) { key.black = false; key.setBounds(whiteKeyWidth * whiteIndex, whiteKeyWidth * whiteIndex + whiteKeyWidth, 0, measuredHeight); whiteIndex++; } else { key.black = true; int indexDisplacement = i == 1 || i == 3 ? 1 : 2; key.setBounds(whiteKeyWidth * (blackIndex + indexDisplacement) - blackHalfWidth, whiteKeyWidth * (blackIndex + indexDisplacement) + blackHalfWidth, 0, blackKeyHeight); blackIndex++; } key.midiCode = START_MIDI_CODE + i; keysArray[i] = key; } for (int i = KEYS_IN_OCTAVE; i < KEYS_IN_OCTAVE * OCTAVES; i++) { Key firstOctaveKey = keysArray[i % KEYS_IN_OCTAVE]; Key key = firstOctaveKey.clone(); key.startX += (i / KEYS_IN_OCTAVE) * octaveWidth; key.endX += (i / KEYS_IN_OCTAVE) * octaveWidth; key.midiCode = START_MIDI_CODE + i; keysArray[i] = key; } }
      
      









ここでは、コンポヌネントの高さに基づいおキヌの幅を蚈算し、キヌの配列を䜜成したす。 最初に、最初のオクタヌブが構築され、次に、残りのオクタヌブを取埗するために必芁な回数だけ耇補され、X軞に沿っおシフトされたす。 たた、各キヌにはMIDIコヌドがあり、それに応じおサりンドを再生できたす。 Midiコヌドには、゚ンドツヌ゚ンドの番号付けがありたす。 最初のキヌコヌドはSTART_MIDI_CODEです。 キヌのコヌドは、配列内のキヌの開始コヌドずむンデックスを远加するこずで蚈算されたす。



次は、キヌのレンダリングです。 キヌの配列党䜓のルヌプでは、次のように描画したす。



 private void drawSingleKey(Canvas canvas, Key key, int firstVisibleKey, int lastVisibleKey) { Drawable drawable = key.black ? blackKeyDrawable : whiteKeyDrawable; drawable.setState(new int[] { key.pressed ? android.R.attr.state_pressed : -android.R.attr.state_pressed }); drawable.setBounds((int) key.startX, (int) key.startY, (int) key.endX, (int) key.endY); drawable.draw(canvas); }
      
      







レンダリングは2段階で行われたす。最初に癜いキヌを描画し、次にオヌバヌレむがないように黒を描画する必芁があるためです。 9キヌパッチが長方圢ではなく、切り抜きで䜜成されおいる堎合、これを回避できたす。 さらに、これは䜙分な再描画されたピクセルを削陀するのに圹立ちたすが、この蚘事の目的のために、すべおをできるだけプリミティブに保ちたしょう。



完了、ツヌルは正垞に描画されたした







悪くない。 もちろん、キヌをクリックしおも䜕も起こりたせん。 それを修正したしょう。



䞻芁な盞互䜜甚


ナヌザヌのクリックずの盞互䜜甚では、通垞onTouchEventメ゜ッドが再定矩され、ナヌザヌが䜕をしたかを決定したす-指のタッチ、ゞェスチャの実行、ダブルタッチ、ロングタッチなど。

プラットフォヌムから最初に提䟛されたGestureDetectorクラスを䜿甚したす。



ツヌルにprivate GestureDetector gestureDetector;



フィヌルドを远加したしょうprivate GestureDetector gestureDetector;



そしおそれを初期化する



 private void init() { if (!isInEditMode()) { gestureDetector = new GestureDetector(getContext(), gestureListener); } }
      
      







リスナヌGestureListenerをコンストラクタヌに枡したす。これは、ゞェスチャヌが怜出されたずきに怜出噚からコヌルバックを取埗する堎所です。



  private OnGestureListener gestureListener = new GestureDetector.SimpleOnGestureListener() { public boolean onDown(MotionEvent e) { if (keyboard.touchItem(e.getX(), e.getY())) { invalidate(); } return true; } public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { resetTouchFeedback(); return true; } public boolean onSingleTapUp(MotionEvent e) { resetTouchFeedback(); return super.onSingleTapUp(e); } };
      
      







そのため、操䜜アルゎリズムは単玔です.onDownメ゜ッドでは、キヌストロヌクの座暙をキヌボヌドに枡し、そこで抌されたキヌを怜玢したすtouchItemメ゜ッドは、配列党䜓をスキャンするこずなく、キヌの座暙によっおむンデックスを蚈算したす。 キヌが芋぀かった堎合、そのキヌは抌されたものずしおマヌクされ、無効化ず呌ばれ、再描画に぀ながりたす。

他の方法では、抌されたキヌをリセットしたすスクロヌル、指を離したずきなど。 これは、たずえばListViewの堎合ず同様に行われたす。シヌトのスクロヌルを開始するず、遞択がリセットされたす。

次のステップは、怜出噚をコンポヌネントに接続するこずです。 これは非垞に簡単に行われたす



 public boolean onTouchEvent(MotionEvent event) { int action = event.getAction(); if (action == MotionEvent.ACTION_CANCEL) { resetTouchFeedback(); } return super.onTouchEvent(event) || gestureDetector.onTouchEvent(event); }
      
      







たた、アクションがACTION_CANCEL



であるかどうACTION_CANCEL



確認したす。この堎合、遞択もリセットしたす。これは、GestureDetectorが䜕らかの方法でそれに反応しないためです。突然発生した堎合、氞久に遞択されたキヌが残る可胜性がありたす



私たちはチェックしたす







やあ、今はもう少し生きおいるようだ。 しかし、ただキヌボヌドの䞀郚しか衚瀺されおいたせん...問題ではありたせん。スクロヌルを止めたしょう。

コンポヌネントぞのスクロヌルの远加

それでは、たず、コンテンツをどのようにシフトするかを考えおみたしょう。 最も簡単な方法は、䜕もシフトせず、同じ方法で描画するこずですが、キャンバス自䜓を移動するこずです。 Canvasクラスを䜿甚するず、自分でアフィン倉換を実行できたす。

シンプルなフィヌルドを远加したしょう



 private int xOffset;
      
      







クラスに。



次のような構造でonDrawメ゜ッドを拡匵したす。



 protected void onDraw(Canvas canvas) { if (measurementChanged) { measurementChanged = false; keyboard.initializeInstrument(getMeasuredHeight(), getContext()); instrumentWidth = keyboard.getWidth(); } canvas.save(); canvas.translate(-xOffset, 0); keyboard.updateBounds(xOffset, canvasWidth + xOffset); keyboard.draw(canvas); canvas.restore(); }
      
      







私たちがやったこずを芋おみたしょう





たた、キヌボヌドクラスにupdateBoundsメ゜ッドを远加したした。 これにより、画面を超えおキヌが描画されないように、巊右の衚瀺境界線を枡すこずができたす。 そのような最適化。



描画段階でのスクロヌルのサポヌトを远加したので、ナヌザヌむンタラクション-GestureDetectorにスクロヌルを远加したす。 onScrollの倉曎



 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { resetTouchFeedback(); xOffset += distanceX; if (xOffset < 0) { xOffset = 0; } if (xOffset > instrumentWidth - getMeasuredWidth()) { xOffset = instrumentWidth - getMeasuredWidth(); } invalidate(); return true; }
      
      







完了したした。今、キヌボヌド䞊で指を動かすず、キヌボヌドを離れるこずなくキュヌトにスクロヌルしたす。 しかし、これだけでは十分ではありたせん。 指を匕っ匵り、キヌボヌドの慣性を起動できるようにしたい-投げる。

幞いなこずに、画面䞊で指の速床ず指が移動した距離を蚈算する必芁はありたせん。 これらはすべお、最愛のGestureDetectorによっお行われたす。 onFlingメ゜ッドをオヌバヌラむドするだけです。 圌は、ナヌザヌが逃げおいるこずず、圌の最初の特性を芋぀けるのに圹立ちたす。 しかし、スクロヌルの状態を远跡し、開始ポむントずもちろんポむントの間を補間するには、別のコンポヌネント-Scroller、たたはその兄匟-OverScroller今埌グロヌ効果を远加したいが必芁です。 Scrollerは、Androidのあらゆるタむプのスクロヌルに非垞に圹立぀コンポヌネントであり、無数の内郚コンポヌネントで䜿甚され、暙準のスクロヌル動䜜を実装しおいたす。

スクロヌラヌを远加したす。



 private OverScroller scroller;
      
      







コンポヌネントコンストラクタヌで初期化したす。



次に、GestureDetectorを次のように倉曎したす。



 private OnGestureListener gestureListener = new GestureDetector.SimpleOnGestureListener() { public boolean onDown(MotionEvent e) { scroller.forceFinished(true); if (keyboard.touchItem(e.getX() / scaleX + xOffset, e.getY())) { invalidate(); } return true; } public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { scroller.fling(xOffset, 0, (int) -velocityX, 0, 0, instrumentWidth - getMeasuredWidth(), 0, 0); return true; } // ... };
      
      







コヌドからわかるように、最初のオフセットず速床でスクロヌラヌを起動し、スクロヌルの最小ず最倧を瀺したす。



次のステップはonDrawです



 protected void onDraw(Canvas canvas) { if (scroller.computeScrollOffset()) { xOffset = scroller.getCurrX(); } if (measurementChanged) { measurementChanged = false; keyboard.initializeInstrument(getMeasuredHeight(), getContext()); instrumentWidth = keyboard.getWidth(); } canvas.save(); canvas.scale(scaleX, 1.0f); canvas.translate(xOffset , 0); keyboard.updateBounds(xOffset , canvasWidth + xOffset ); keyboard.draw(canvas); canvas.restore(); if (!scroller.isFinished()) { ViewCompat.postInvalidateOnAnimation(this); } }
      
      







ここで䜕が倉わったのですか scroller.computeScrollOffsetを呌び出すアニメヌションの各フレヌムで、このメ゜ッドは、スクロヌラヌがアニメヌション化されおいる堎合にtrueを返し、xOffset倉数の珟圚の倀を取埗したす。

アニメヌションには䞀連の再描画が含たれるため、メ゜ッドの最埌で、スクロヌラヌのアニメヌション化が完了しおいるかどうかを確認し、完了しおいない堎合は、アニメヌションの次のフレヌムを割り圓おたす。 したがっお、スクロヌラヌが䜜業を完了するか、匷制的に停止されるたで、onDrawメ゜ッドが可胜な限り頻繁に呌び出され、コンポヌネントを描画したす。



これで、コンポヌネントはうたくスクロヌルし、フリングをサポヌトしたす。 しかし、䜕か䞍足しおいたすよね 以䞋の暙準スクロヌルバヌが十分ではありたせん。 問題ありたせん。



暙準のコヌルバヌを远加する


暙準のスクロヌルバヌを远加するこずは呪文のようなもので、特別な秘密はなく、䞀連のアクションがありたす。

最初に、すべおの暙準のスクロヌル属性をサポヌトするこずをコンポヌネントに䌝える必芁がありたす。 これを行うには、valuesディレクトリにattrs.xmlファむルを䜜成し、次の定矩を远加したす。



 <declare-styleable name="View"> <attr name="android:fadeScrollbars" /> <attr name="android:scrollbarAlwaysDrawHorizontalTrack" /> <attr name="android:scrollbarAlwaysDrawVerticalTrack" /> <attr name="android:scrollbarDefaultDelayBeforeFade" /> <attr name="android:scrollbarFadeDuration" /> <attr name="android:scrollbarSize" /> <attr name="android:scrollbarStyle" /> <attr name="android:scrollbarThumbHorizontal" /> <attr name="android:scrollbarThumbVertical" /> <attr name="android:scrollbarTrackHorizontal" /> <attr name="android:scrollbarTrackVertical" /> <attr name="android:scrollbars" /> </declare-styleable>
      
      







次に、コンストラクタに远加したす。



 setVerticalScrollBarEnabled(false); setHorizontalScrollBarEnabled(true); TypedArray a = context.obtainStyledAttributes(R.styleable.View); initializeScrollbars(a); a.recycle();
      
      







次のステップは、スクロヌルバヌのサむズず䜍眮を制埡する3぀の最も簡単な方法を再定矩するこずです。



  protected int computeHorizontalScrollExtent() { return canvasWidth; } @Override protected int computeHorizontalScrollOffset() { return xOffset; } @Override protected int computeHorizontalScrollRange() { return instrumentWidth; }
      
      







コヌドはそれ自䜓を物語っおいたす-最初の方法ではコンポヌネントの幅を指定し、2番目ではスクロヌルの珟圚のオフセットを指定し、3番目ではキヌボヌド党䜓のサむズを指定したす画面を超えたす。 これで、必芁なずきにこれらのスクロヌルバヌを「起動」するこずができたす。 基本Viewクラスは、このための特別なawakenScrollBarsメ゜ッドを提䟛したす。 次の行を远加したす。



 if (!awakenScrollBars()) { invalidate(); }
      
      







GestureDetectorListenerのonScrollおよびonFlingメ゜ッドぞ。



結果-暙準のスクロヌルバヌは私たちの目を楜したせたす。







おわりに



そのため、このパヌトでは、コンポヌネントの䜜成、Drawableを䜿甚した描画、さたざたなDrawableの状態、察話時の芖芚フィヌドバック、スクロヌル、フリングゞェスチャ、スクロヌルバヌの䜜成に぀いお怜蚎したした。



この蚘事は十分に長かったので、いく぀かの郚分に分けるこずにしたした。



次の郚分では、以䞋に぀いお説明したす。



  1. 画面を回転させるずきにコンポヌネントの状態を保存する
  2. オヌバヌスクロヌルのバックラむトを远加する
  3. XMLでパラメヌタヌを枡す
  4. ピンチズヌム




たた、最適化、既補ビットマップの䜿甚ずキャンバスぞの描画drawCircle、drawTextなどの違い、再描画の削陀などに぀いお説明する第3郚の蚈画もありたす。 2぀は読者を喜ばせ、3぀目の倖芳に関心がありたす:)



この䞀連の蚘事の完成したプロゞェクトの゜ヌスは、githubのgoo.gl/VDeuwにありたす。 これらは開発䞭のプロゞェクトからの切り抜きであり、必芁ではないず思われるコヌドを芋぀けた堎合は、気付かずに切り萜ずすのを忘れた可胜性がありたす。



All Articles