android手机上实现竖直seekbar的EQ均衡器
做音乐播放器,有时会要求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超出进度条的情况。
现在的效果如图:
大家可以根据自己的需要,自定义自己想要的样式。
因为是菜鸟,很多东西都是一笔带过,写得不好或者是有错的,还请各位大神指教。