做音樂播放器,有時會要求EQ均衡器,但android默認的樣式是水平的,這時就需要費點心思了。
先是實現默認SeekBar樣式的EQ均衡器:
這是4.0以上默認樣式的SeekBar,2.3或以下就像是進度條一樣。
要實現這樣的效果,其實並不難,先貼上源碼:
public class MainActivity extends Activity { private MediaPlayer mMediaPlayer; private Equalizer mEqualizer; private LinearLayout mLayout; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setVolumeControlStream(AudioManager.STREAM_MUSIC); mLayout = new LinearLayout(this); mLayout.setOrientation(LinearLayout.VERTICAL); setContentView(mLayout); mMediaPlayer = new MediaPlayer(); try { mMediaPlayer.setDataSource("/sdcard/陪我去流浪.mp3"); mMediaPlayer.prepare(); mMediaPlayer.start(); } catch (IllegalArgumentException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (SecurityException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IllegalStateException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } setEqualize(); } @TargetApi(Build.VERSION_CODES.GINGERBREAD) private void setEqualize() { mEqualizer = new Equalizer(0, mMediaPlayer.getAudioSessionId()); mEqualizer.setEnabled(true); short bands = mEqualizer.getNumberOfBands(); final short minEqualizer = mEqualizer.getBandLevelRange()[0]; final short maxEqualizer = mEqualizer.getBandLevelRange()[1]; for (short i = 0; i < bands; i++) { final short band = i; TextView freqTextView = new TextView(this); freqTextView.setLayoutParams(new ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); freqTextView.setGravity(Gravity.CENTER_HORIZONTAL); freqTextView .setText((mEqualizer.getCenterFreq(band) / 1000) + "HZ"); mLayout.addView(freqTextView); LinearLayout row = new LinearLayout(this); row.setOrientation(LinearLayout.HORIZONTAL); TextView minDbTextView = new TextView(this); minDbTextView.setLayoutParams(new ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); minDbTextView.setText((minEqualizer / 100) + " dB"); TextView maxDbTextView = new TextView(this); maxDbTextView.setLayoutParams(new ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); maxDbTextView.setText((maxEqualizer / 100) + " dB"); LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); SeekBar seekbar = new SeekBar(this); seekbar.setLayoutParams(layoutParams); seekbar.setMax(maxEqualizer - minEqualizer); seekbar.setProgress(mEqualizer.getBandLevel(band)); seekbar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() { @Override public void onStopTrackingTouch(SeekBar seekBar) { } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @TargetApi(Build.VERSION_CODES.GINGERBREAD) @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { // TODO Auto-generated method stub mEqualizer.setBandLevel(band, (short) (progress + minEqualizer)); } }); row.addView(minDbTextView); row.addView(seekbar); row.addView(maxDbTextView); mLayout.addView(row); } } @TargetApi(Build.VERSION_CODES.GINGERBREAD) @Override protected void onPause() { // TODO Auto-generated method stub super.onPause(); if (isFinishing() && mMediaPlayer != null) { mMediaPlayer.release(); mEqualizer.release(); mMediaPlayer = null; } } }
這里我采用了手動設置Layout布局的方式,因為上面的Layout采用LinearLayout實現,並且每個SeekBar的屬性設置都一樣,考慮到SeekBar的數量較多,采用這個方式可能會更好點,至少我們不需要寫太繁瑣的Layout布局文件。
撇開那些Layout的布局代碼,我們看看最主要的部分:SeekBar和Equalizer的設置。
android系統提供內置的Equalizer支持,我們可以直接聲明並且使用。但必須注意,當我們在代碼中使用Equalizer的時候,其實就是調整音量(EQ均衡器是改變音頻使得聲音發生變化,像是洪亮或者低沉)。所以,我們需要在我們的代碼中聲明這么一句:
setVolumeControlStream(AudioManager.STREAM_MUSIC);
因為涉及到硬件方面的修改,我們需要權限:
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
它允許我們進行全局的音頻設置。
我們再來看看這句:
mEqualizer = new Equalizer(0, mMediaPlayer.getAudioSessionId());
每個MediaPlayer都有自己獨一無二的SessionId,我們需要將Equalizer附加到這個MediaPlayer上,就必須獲取該SessionId。然后我們再創建一個優先級為0的Equalizer對象。所謂的優先級,是因為一個Equalizer engine可以被多個應用程序共享,所以我們必須設置優先級,優先級0代表該應用程序為正常級別。
要想啟動Equalizer,我們還必須這樣子:
mEqualizer.setEnabled(true);
這就是學過單片機的同學非常熟悉的"使能"。
然后我們再獲取支持的頻譜:
short bands = mEqualizer.getNumberOfBands();
不同的硬件設備支持的頻譜是不一樣的,像是電腦能支持的頻譜就比手機要多得多。
接着就是獲取頻譜中的等級范圍,我們只需要獲取最低和最高即可。
final short minEqualizer = mEqualizer.getBandLevelRange()[0];
final short maxEqualizer = mEqualizer.getBandLevelRange()[1];
接下來就是遍歷頻譜,設置SeekBar了:
seekbar.setMax(maxEqualizer - minEqualizer); seekbar.setProgress(mEqualizer.getBandLevel(band)); seekbar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() { @Override public void onStopTrackingTouch(SeekBar seekBar) {} @Override public void onStartTrackingTouch(SeekBar seekBar) {} @TargetApi(Build.VERSION_CODES.GINGERBREAD) @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { mEqualizer.setBandLevel(band, (short) (progress + minEqualizer)); } });
我們必須設置SeekBar的最大進度范圍,也就是進度頂端所代表的值。接着就是實現SeekBar進度改變時,Equalizer的音頻也跟着相應的改變,這就需要監聽SeekBar。
好了,到了這里,基本的EQ均衡器已經實現了,但我們想要的並不是功能的實現,而是界面的實現(這往往是我們這些移動互聯網開發者的惡夢,美工設計師們從來都沒有考慮過我們要設計出他們的界面有時是多么難甚至不可能的一件事啊!)。
既然默認的SeekBar樣式無法滿足我們的要求,我們只能自定義自己想要的SeekBar了。幸好,android還是提供了這種支持:只要繼承自AbsSeekBr,我們就能得到自己想要的SeekBar了。
依然先上源碼:
public class VerticalSeekBar extends AbsSeekBar { private Drawable mThumb; private int height; private int width; public interface OnSeekBarChangeListener { void onProgressChanged(VerticalSeekBar VerticalSeekBar, int progress, boolean fromUser); void onStartTrackingTouch(VerticalSeekBar VerticalSeekBar); void onStopTrackingTouch(VerticalSeekBar VerticalSeekBar); } private OnSeekBarChangeListener mOnSeekBarChangeListener; public VerticalSeekBar(Context context) { this(context, null); } public VerticalSeekBar(Context context, AttributeSet attrs) { this(context, attrs, android.R.attr.seekBarStyle); } public VerticalSeekBar(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } public void setOnSeekBarChangeListener(OnSeekBarChangeListener l) { mOnSeekBarChangeListener = l; } void onStartTrackingTouch() { if (mOnSeekBarChangeListener != null) { mOnSeekBarChangeListener.onStartTrackingTouch(this); } } void onStopTrackingTouch() { if (mOnSeekBarChangeListener != null) { mOnSeekBarChangeListener.onStopTrackingTouch(this); } } void onProgressRefresh(float scale, boolean fromUser) { Drawable thumb = mThumb; if (thumb != null) { setThumbPos(getHeight(), thumb, scale, Integer.MIN_VALUE); invalidate(); } if (mOnSeekBarChangeListener != null) { mOnSeekBarChangeListener.onProgressChanged(this, getProgress(), fromUser); } } private void setThumbPos(int w, Drawable thumb, float scale, int gap) { int available = w + getPaddingLeft() - getPaddingRight(); int thumbWidth = thumb.getIntrinsicWidth(); int thumbHeight = thumb.getIntrinsicHeight(); available -= thumbWidth; available += getThumbOffset() / 2; int thumbPos = (int) (scale * available); int topBound, bottomBound; if (gap == Integer.MIN_VALUE) { Rect oldBounds = thumb.getBounds(); topBound = oldBounds.top; bottomBound = oldBounds.bottom; } else { topBound = gap; bottomBound = gap + thumbHeight; } thumb.setBounds(thumbPos, topBound, thumbPos + thumbWidth, bottomBound); } protected void onDraw(Canvas c) { c.rotate(-90); c.translate(-height, 0); super.onDraw(c); } protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { height = View.MeasureSpec.getSize(heightMeasureSpec) / 2; width = 50; this.setMeasuredDimension(width, height); } @Override public void setThumb(Drawable thumb) { mThumb = thumb; super.setThumb(thumb); } protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(h, w, oldw, oldh); } public boolean onTouchEvent(MotionEvent event) { if (!isEnabled()) { return false; } switch (event.getAction()) { case MotionEvent.ACTION_DOWN: setPressed(true); onStartTrackingTouch(); trackTouchEvent(event); break; case MotionEvent.ACTION_MOVE: trackTouchEvent(event); attemptClaimDrag(); break; case MotionEvent.ACTION_UP: trackTouchEvent(event); onStopTrackingTouch(); setPressed(false); break; case MotionEvent.ACTION_CANCEL: onStopTrackingTouch(); setPressed(false); break; } return true; } private void trackTouchEvent(MotionEvent event) { final int Height = getHeight(); final int available = Height - getPaddingBottom() - getPaddingTop(); int Y = (int) event.getY(); float scale; float progress = 0; if (Y > Height - getPaddingBottom()) { scale = 0.0f; } else if (Y < getPaddingTop()) { scale = 1.0f; } else { scale = (float) (Height - getPaddingBottom() - Y) / (float) available; } final int max = getMax(); progress = scale * max; setProgress((int) progress); } private void attemptClaimDrag() { if (getParent() != null) { getParent().requestDisallowInterceptTouchEvent(true); } } public boolean dispatchKeyEvent(KeyEvent event) { if (event.getAction() == KeyEvent.ACTION_DOWN) { KeyEvent newEvent = null; switch (event.getKeyCode()) { case KeyEvent.KEYCODE_DPAD_UP: newEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_RIGHT); break; case KeyEvent.KEYCODE_DPAD_DOWN: newEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_LEFT); break; case KeyEvent.KEYCODE_DPAD_LEFT: newEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_DOWN); break; case KeyEvent.KEYCODE_DPAD_RIGHT: newEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_UP); break; default: newEvent = new KeyEvent(KeyEvent.ACTION_DOWN, event.getKeyCode()); break; } return newEvent.dispatch(this); } return false; } }
代碼很長,但關鍵是我上面標記的幾個方法。
onDraw()該方法就是實現豎直SeekBar的關鍵。我們可以看到,關鍵就是那個c.rotate(-90),它實現了我們的豎直:通過反轉-90度。setThumbPos()方法是為了調整thumb的大小,這個很重要,尤其是當我們開始排版的時候,thumb很可能因為布局的問題而顯示不完全或者根本就不見了。onMeasure()針對的是進度條的設置。至於具體的大小怎樣設置,得看大家自己的具體應用了。
這個是實現的效果:
前面的代碼基本保持不變,只要將SeekBar改為VerticalSeekBar就可以了:
for (short i = 0; i < bands; i++) { final short band = i; int index = (int) i; VerticalSeekBar seekBar = (VerticalSeekBar) findViewById(seekBars[index]); seekBar.setMax(maxEqualizer - minEqualizer); seekBar.setProgress(mEqualizer.getBandLevel(band)); seekBar.setOnSeekBarChangeListener(new VerticalSeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(VerticalSeekBar VerticalSeekBar, int progress, boolean fromUser) { mEqualizer.setBandLevel(band, (short) (progress + minEqualizer)); } @Override public void onStartTrackingTouch(VerticalSeekBar VerticalSeekBar) { // TODO Auto-generated method stub } @Override public void onStopTrackingTouch(VerticalSeekBar VerticalSeekBar) { // TODO Auto-generated method stub } });
為了實現我上面的布局效果,我這次使用了RelativeLayout布局文件,至於那些間距的調整,就留給讀者們了。
這樣的效果還是不行的,因為有個致命的缺陷:android2.3的默認樣式和android4.0的默認樣式是不一樣的!別看我上面的效果挺美觀的,在android2.3上可不是這樣。
最好的解決方法就是替換默認樣式。
替換默認樣式的方法很簡單:在res目錄下,新建drawable文件夾,然后我們在該目錄下新建一個xml文件:
<?xml version="1.0" encoding="utf-8"?> <layer-list xmlns:android="http://schemas.android.com/apk/res/android" > <item android:id="@android:id/secondaryProgress"> <clip android:drawable="@drawable/progress2" /> </item> <item android:id="@android:id/progress"> <clip android:drawable="@drawable/progress1" /> </item> </layer-list>
我們可以使用layer-list來替換默認樣式,secondaryProgress指的是我們進度條中剩下的進度,progress指的就是我們當前的進度。
分別設置好它們的替換圖片后,我們再在SeekBar中這樣設置:
<com.example.eqpratice.VerticalSeekBar android:id="@+id/sec" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_toRightOf="@id/fir" android:progressDrawable="@drawable/progress" android:secondaryProgress="100" android:thumb="@drawable/thumb" />
這里android: secondaryProgress="100"設置的是我們thumb能達到的最大進度,這個是要設置的,不然會出現thumb超出進度條的情況。
現在的效果如圖:
大家可以根據自己的需要,自定義自己想要的樣式。
因為是菜鳥,很多東西都是一筆帶過,寫得不好或者是有錯的,還請各位大神指教。