源码:html
http://www.2cto.com/kf/201211/171417.htmljava
最近在研究收音机的源码,原本想把收音机从源码中提取出来,作成一个单独的应用,可是,收音机须要底层的支持,因此,就有点无能为力了,不过发现UI作得比较有个性(以下图所示),图片比较丑(这是作图片MM的责任哦,哈哈),就单独拿出来跟你们分享一下,顺便本身也学习一下自定义view。
今天咱们主要实现的是中间红色区域的部分,下面咱们来看一下它的布局文件radio.xml,它最外围是一个绝对布局,其实我我的是比较反对用绝对布局的,用其余布局也绝对能解决这个问题,大不了能够把图片资源分割一下吗,可见作这个应用的公司作美工的MM是何等的奇缺啊!O(∩_∩)O哈哈~(可怜的程序猿,又少了一个跟MM相处的机会了)。
<?xml version="1.0" encoding="utf-8"?>
<AbsoluteLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="@drawable/background_main"
android:orientation="vertical" >
<RelativeLayout www.2cto.com
android:layout_width="320dip"
android:layout_height="113dip"
android:layout_x="0dip"
android:layout_y="180dip"
android:background="@drawable/led_disabled" />
<RelativeLayout
android:id="@+id/led_background"
android:layout_width="320dip"
android:layout_height="113dip"
android:layout_x="0dip"
android:layout_y="180dip"
android:background="@drawable/led_enabled" >
<ImageView
android:id="@+id/fm_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginLeft="14dip"
android:layout_marginTop="12dip"
android:src="@drawable/fm" />
<ImageView
android:id="@+id/headset_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dip"
android:layout_toRightOf="@id/fm_indicator"
android:src="@drawable/headset_indicator" />
<TextView
android:id="@+id/text_hour_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:layout_marginRight="13dip"
android:layout_marginTop="10dip"
android:textColor="#000000" />
<com.way.view.DigitalTextView
android:id="@+id/digital_clock"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_marginTop="13dip"
android:layout_toLeftOf="@+id/text_hour_type" />
<ImageView
android:id="@+id/searching_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="13dip"
android:layout_toLeftOf="@id/digital_clock"
android:src="@drawable/searching" />
<com.way.view.DigitalTextView
android:id="@+id/digital_freq"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:layout_marginRight="84dip"
android:layout_marginTop="15dip" />
<SeekBar
android:id="@+id/freq_indicator"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginBottom="12dip"
android:layout_marginLeft="7dip"
android:layout_marginRight="7dip"
android:progressDrawable="@drawable/blank"
android:thumb="@drawable/pointer"
android:thumbOffset="0dip" />
</RelativeLayout>
<FrameLayout
android:layout_width="220dip"
android:layout_height="30dip"
android:layout_x="50dip"
android:layout_y="291dip"
android:background="@drawable/scroller_bg" >
<com.way.view.TuneWheel
android:id="@+id/tune_wheel"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
<ImageView
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:src="@drawable/tune_wheel_highlight" />
</FrameLayout>
<ImageButton
android:id="@+id/add_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_x="39dip"
android:layout_y="332dip"
android:background="#00000000"
android:src="@drawable/add_button" />
<ImageButton
android:id="@+id/prev_station_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_x="108dip"
android:layout_y="332dip"
android:background="#00000000"
android:src="@drawable/prev_station_button" />
<ImageButton
android:id="@+id/search_prev_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_x="176dip"
android:layout_y="332dip"
android:background="#00000000"
android:src="@drawable/search_prev_button" />
<com.way.view.CheckableImageButton
android:id="@+id/headset_toggle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_x="255dip"
android:layout_y="332dip"
android:background="#00000000" />
<com.way.view.CheckableImageButton
android:id="@+id/power_toggle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_x="0dip"
android:layout_y="403dip"
android:background="#00000000" />
<ImageButton
android:id="@+id/radio_list_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_x="80dip"
android:layout_y="402dip"
android:background="#00000000"
android:src="@drawable/radio_list_button" />
<ImageButton
android:id="@+id/next_station_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_x="146dip"
android:layout_y="404dip"
android:background="#00000000"
android:src="@drawable/next_station_button" />
<ImageButton
android:id="@+id/search_next_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_x="215dip"
android:layout_y="402dip"
android:background="#00000000"
android:src="@drawable/search_next_button" />
</AbsoluteLayout> android
接下来让咱们来看一下那个自定义滚轮的代码TuneWheel:
/**
* 调频的滚轮
*
* @author way
*
*/
public class TuneWheel extends View {
/**
* 滚轮滚动监听接口
*/
public interface OnTuneWheelValueChangedListener {
public void onTuneWheelValueChanged(View v, float changedBy);
}
private static final String TAG = "TuneWheel";
private static final int LINE_SPAN = 8;//线性跨度
private static final int DRAG_THRESHOLD = 5;//滚动的最小阈值,即在屏幕x轴上至少滑动5个坐标点才有效
private static final float CHANGE_THRESHOLD = 0.1f;//最小的改变阈值,即须要传递出去的那个值的最小值
public static final int DIRECTION_PREV = 1;//向左滚动的方向值
public static final int DIRECTION_NEXT = 2;//向右滚动的方向值
private boolean mIsMoving = false;// 是否在滑动
private float mLastDragPos;//最终的滚动位置
private float mCurrentPos = 0;// 当前位置
private float mCurrentChange = 0;// 当前改变值
private int mDragEnable = DIRECTION_PREV | DIRECTION_NEXT;
private OnTuneWheelValueChangedListener mListener = null;// 监听接口对象
public void setOnValueChangedListener(
OnTuneWheelValueChangedListener listener) {// 提供监听方法
mListener = listener;
}
/**
* 根据传入的方向设置是否容许滚动
*
* @param direction
* 传入的方向
* @param enable
* 是否能滚动
*/
public void setDragEnable(int direction, boolean enable) {
if (enable) {
mDragEnable = mDragEnable | direction;
} else {
mDragEnable = mDragEnable ^ direction;
}
}
/**
* 根据输入的方向判断是否容许滚动
*
* @param direction
* 传入的方向
* @return 是否能滚动
*/
public boolean getDragEnable(int direction) {
return (mDragEnable & direction) != 0;
}
// 三个构造器
public TuneWheel(Context context) {
super(context);
}
public TuneWheel(Context context, AttributeSet attrs) {
super(context, attrs);
}
public TuneWheel(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
// 重写onDraw方法
protected void onDraw(Canvas canvas) {
Paint mLinePaint = new Paint();
mLinePaint.setARGB(0xEC, 0x00, 0, 0);
mLinePaint.setStrokeWidth(0.5f);
for (int pos = 0; pos + mCurrentPos < getWidth(); pos += LINE_SPAN) {
canvas.drawLine(pos + mCurrentPos, 0, pos + mCurrentPos,
getHeight(), mLinePaint);
}
}
// 重写onTouchEvent方法,实现滚动效果
public boolean onTouchEvent(MotionEvent event) {
if (!this.isEnabled())
return false;
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN://按下
mLastDragPos = event.getX();//获取按下时的x轴坐标值,滚轮只能左右滑动,因此只要获取x值便可
break;
case MotionEvent.ACTION_MOVE://移动
float currentDragPos = event.getX();
if (!getDragEnable(getDirection(currentDragPos, mLastDragPos)))//若是当前方向不容许滚动
return false;
if (!mIsMoving) {//若是当前状态为没有滚动
if (Math.abs(currentDragPos - mLastDragPos) > DRAG_THRESHOLD) {//若是滚动范围大于最小阈值
mIsMoving = true;
mLastDragPos = currentDragPos;
} else {
return false;
}
} else {//若是当前状态为一直在滚动
float tempPos = (mCurrentPos + currentDragPos - mLastDragPos)
% LINE_SPAN;//保存一个临时变化值
mCurrentChange += calculateChange(currentDragPos, mLastDragPos);
if (Math.abs(mCurrentChange) > CHANGE_THRESHOLD) {
if (mListener != null) {
mListener.onTuneWheelValueChanged(this, mCurrentChange);//将滚动变化值经过监听接口传递出去
}
mCurrentChange = 0;
}
mCurrentPos = tempPos;
invalidate();//千万要记得刷新一下界面
mLastDragPos = currentDragPos;
}
break;
case MotionEvent.ACTION_UP://松开
if (mIsMoving) {
mIsMoving = false;//松开后更新状态为中止滚动0.
}
break;
}
return true;
}
/**
* 根据改变计算滚动方向
*
* @param current
* 当前位置值
* @param last
* 最终位置值
* @return 方向值
*/
private int getDirection(float current, float last) {
return (int) (Math.signum(current - last) + 3) / 2;
}
/**
* 根据改变计算改变值
*
* @param current
* 当前位置值
* @param last
* 最终位置值
* @return 改变值
*/
private float calculateChange(float current, float last) {
float sub = current - last;
return Math.signum(sub) * (float) Math.pow(Math.abs(sub), 1.3) / 100;
}
} git
第三,是中间那个显示时间和调频自定义的View,DigitalTextView.java:
/**
* 自定义的一个显示调频或时间的view
*
* @author way
*
*/
public class DigitalTextView extends LinearLayout {
private static final String TAG = "DigitalTextView";
private String mResourcePrefix = "";// 资源前缀,时间或调频
// 两个构造器,自定义view中至少要有2个构造器
public DigitalTextView(Context context) {
super(context);
init();
}
public DigitalTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
// 初始化
private void init() {
this.setOrientation(LinearLayout.HORIZONTAL);
}
/**
* 获取对应的时间图片
*
* @param index
* 时间值
* @return 对应的时间图片id
*/
private int getTimeDrawable(int index) {
int drawableId = -1;
switch (index) {
case 0:
drawableId = R.drawable.time_0;
break;
case 1:
drawableId = R.drawable.time_1;
break;
case 2:
drawableId = R.drawable.time_2;
break;
case 3:
drawableId = R.drawable.time_3;
break;
case 4:
drawableId = R.drawable.time_4;
break;
case 5:
drawableId = R.drawable.time_5;
break;
case 6:
drawableId = R.drawable.time_6;
break;
case 7:
drawableId = R.drawable.time_7;
break;
case 8:
drawableId = R.drawable.time_8;
break;
case 9:
drawableId = R.drawable.time_9;
break;
}
return drawableId;
}
/**
* 获取调频图片
*
* @param index
* 频率值
* @return 对应频率值的图片id
*/
private int getFreqDrawable(int index) {
int drawableId = -1;
switch (index) {
case 0:
drawableId = R.drawable.freq_0;
break;
case 1:
drawableId = R.drawable.freq_1;
break;
case 2:
drawableId = R.drawable.freq_2;
break;
case 3:
drawableId = R.drawable.freq_3;
break;
case 4:
drawableId = R.drawable.freq_4;
break;
case 5:
drawableId = R.drawable.freq_5;
break;
case 6:
drawableId = R.drawable.freq_6;
break;
case 7:
drawableId = R.drawable.freq_7;
break;
case 8:
drawableId = R.drawable.freq_8;
break;
case 9:
drawableId = R.drawable.freq_9;
break;
}
return drawableId;
}
/**
* 根据传递进来的字符,返回对应的图片资源
*
* @param c
* 传递进来的字符
* @return 对应的图片id
*/
private int getResourceForChar(char c) {
if (c == '.') {
if (!mResourcePrefix.equals("time")) {
return R.drawable.freq_dot;
}
} else if (c == ':') {
if (mResourcePrefix.equals("time")) {
return R.drawable.time_colon;
}
} else if (c >= '0' && c <= '9') {
if (mResourcePrefix.equals("time")) {
return getTimeDrawable(c - '0');
} else {
return getFreqDrawable(c - '0');
}
} else {
return -1;
}
return -1;
}
// 建立一个ImageView
private ImageView createImageView() {
ImageView imageView = new ImageView(getContext());
LayoutParams param = new LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
imageView.setLayoutParams(param);
return imageView;
}
/**
* 设置资源前缀,是时间仍是调频
*
* @param resourcePrefix
*/
public void setResourcePrefix(String resourcePrefix) {
mResourcePrefix = resourcePrefix;
}
/**
* 设置数字值,相似TextView的setText()
*
* @param text
* 传递进来的字符串
*/
public void setDigitalText(String text) {
updateView(text);
}
/**
* 更新自定义TextView
*
* @param text
* 传递进来的字符串
*/
private void updateView(String text) {
int startIndex = getChildCount() - text.length();// 起始位置,由于imageView的数量是根据字符串的长度建立的
if (startIndex < 0)//第一次更新的时候确定是小于0的
startIndex = 0;
for (int i = 0; i < startIndex; i++) {
getChildAt(i).setVisibility(View.GONE);//把以前的图片隐藏起来,我的感受这样作得不怎么样
}
//下面是根据字符串的长度,循环更换为对应的图片
for (int i = 0; i < text.length(); i++) {
int childId = i + startIndex;
int resId = getResourceForChar(text.charAt(i));//将每一个字符转换为数字
if (resId != -1) {
if (childId == getChildCount()) {
addView(createImageView());//添加到LinearLayout中
}
ImageView child = ((ImageView) getChildAt(childId));
child.setVisibility(View.VISIBLE);
child.setImageResource(resId);
}
}
}
} canvas
最后,就是把这些自定义View综合起来的MainActivity了:
public class Main extends Activity implements OnTuneWheelValueChangedListener {
private static final int FREQ_POINTER_SCALE = 10;
private TuneWheel mTuneWheel;// 滚轮
private DigitalTextView mFreqView;// 显示调频波段的view
private SeekBar mFreqPointer;// 进度条指针
private TextView mClockType;// 显示时间的格式pm、am
private DigitalTextView mDigitalClock;// 显示时间的view
private String mClockTypeString;// 时间
private View mSearchingIndicator;// 加载时间的时候刷新图标
private CheckableImageButton mPowerToggle, mHeadsetToggle;// 开关键和耳机扬声器切换键
private DigitalClockUpdater mDigitalClockUpdater;// 更新时间的对象
private float channel = 87.5f;// 默认初始的波段
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.radio);
initView();// 初始化view
}
private void initView() {
// 开关键
mPowerToggle = (CheckableImageButton) findViewById(R.id.power_toggle);
mPowerToggle.setDrawable(R.drawable.power_button_checked,
R.drawable.power_button_unchecked,
R.drawable.power_button_disabled);
// 耳机扬声器切换键
mHeadsetToggle = (CheckableImageButton) findViewById(R.id.headset_toggle);
mHeadsetToggle.setDrawable(R.drawable.headset_button_checked,
R.drawable.headset_button_unchecked,
R.drawable.headset_button_disabled);
mClockType = (TextView) findViewById(R.id.text_hour_type);// 时间类型
Date curDate = new Date(java.lang.System.currentTimeMillis());
int hours = curDate.getHours();
if (!DateFormat.is24HourFormat(Main.this)) {// 若是不是24小时制
if (hours > 12) {
mClockTypeString = getString(R.string.pm);
} else {
mClockTypeString = getString(R.string.am);
}
mClockType.setText(mClockTypeString);// 设置pm、am格式
} else {
mClockType.setText("");// 若是所24小时制,就不要任何标识
}
mDigitalClock = (DigitalTextView) findViewById(R.id.digital_clock);
mDigitalClock.setResourcePrefix("time");
mDigitalClockUpdater = new DigitalClockUpdater(mDigitalClock);
mDigitalClockUpdater.run();// 启动更新时间
mTuneWheel = (TuneWheel) findViewById(R.id.tune_wheel);// 滚轮
mTuneWheel.setOnValueChangedListener(this);// 监听滚动事件
// 显示波段的view
mFreqView = (DigitalTextView) findViewById(R.id.digital_freq);
mFreqView.setResourcePrefix("freq");
mFreqView.setDigitalText(String.valueOf(channel));// 初始化为默认值,实际从配置文件中读取上次保存的频段
mSearchingIndicator = findViewById(R.id.searching_indicator);
mSearchingIndicator.setVisibility(View.INVISIBLE);// 加载完时间后,自动隐藏这个图标
// 频段进度条
mFreqPointer = (SeekBar) findViewById(R.id.freq_indicator);
// 设置进度条范围
mFreqPointer
.setMax((int) ((WheelConfig.RADIO_MAX_FREQUENCY - WheelConfig.RADIO_MIN_FREQUENCY) * FREQ_POINTER_SCALE));
mFreqPointer.setEnabled(false);
}
@Override
public void onTuneWheelValueChanged(View v, float changedBy) {// 滚轮滑动监听事件
// TODO Auto-generated method stub
float freq = adjustFreq(WheelConfig.format(channel + changedBy));// 调整滚轮滑动变化值
channel = freq;// 用一个全局变量保存一下当前的调频
setFreqForUi(freq);// 更新ui
}
/**
* 调整和限制滚轮滑动的值
*
* @param freq
* 须要调整的值
* @return 调整后的值
*/
private float adjustFreq(float freq) {
float result = ((int) (freq * 10)) / 10f;
if (result < WheelConfig.RADIO_MIN_FREQUENCY)
result = WheelConfig.RADIO_MIN_FREQUENCY;
if (result > WheelConfig.RADIO_MAX_FREQUENCY)
result = WheelConfig.RADIO_MAX_FREQUENCY;
return result;
}
/**
* 经过传入的滚轮值,更新ui
*
* @param freq
* 调整后的滚轮值
*/
private void setFreqForUi(float freq) {
if (freq == WheelConfig.RADIO_MIN_FREQUENCY) {
mTuneWheel.setDragEnable(TuneWheel.DIRECTION_PREV, false);
mTuneWheel.setDragEnable(TuneWheel.DIRECTION_NEXT, true);
} else if (freq == WheelConfig.RADIO_MAX_FREQUENCY) {
mTuneWheel.setDragEnable(TuneWheel.DIRECTION_PREV, true);
mTuneWheel.setDragEnable(TuneWheel.DIRECTION_NEXT, false);
} else {
mTuneWheel.setDragEnable(TuneWheel.DIRECTION_PREV, true);
mTuneWheel.setDragEnable(TuneWheel.DIRECTION_NEXT, true);
}
// 更新进度条位置
mFreqPointer
.setProgress((int) ((freq - WheelConfig.RADIO_MIN_FREQUENCY) * FREQ_POINTER_SCALE));
mFreqView.setDigitalText(String.valueOf(freq));// 更新当前频段值
}
@Override
protected void onDestroy() {
super.onDestroy();
mDigitalClockUpdater.stop();// 中止更新时间
}
/**
* 更新时间的类
*
* @author way
*
*/
private class DigitalClockUpdater {
private static final int MSG_UPDATE_TIME = 0x001;
private static final int UPDATE_INTERNAL = 1000;
DigitalTextView mView = null;
boolean mRunning = false;
Handler mUpdateHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (msg.what == MSG_UPDATE_TIME) {
Date curDate = new Date(
java.lang.System.currentTimeMillis());
int hours = curDate.getHours();
int minutes = curDate.getMinutes();
if (DateFormat.is24HourFormat(Main.this)) {
if (minutes < 10) {
mView.setDigitalText("" + hours + ":0" + minutes);
} else {
mView.setDigitalText("" + hours + ":" + minutes);
}
mClockTypeString = "";
} else {
hours = hours > 12 ? hours - 12 : hours;
if (minutes < 10) {
mView.setDigitalText("" + hours + ":0" + minutes);
} else {
mView.setDigitalText("" + hours + ":" + minutes);
}
}
if (mRunning)
this.sendEmptyMessageDelayed(MSG_UPDATE_TIME,
UPDATE_INTERNAL);// 每隔一秒就更新一下时间,保持与系统同步,其实若是只须要每分钟更新时间,能够经过接收系统广播同步,这个广播每分钟发送一次。
}
}
};
public DigitalClockUpdater(DigitalTextView view) {
mView = view;
}
public void run() {
mRunning = true;
mUpdateHandler.removeMessages(MSG_UPDATE_TIME);
mUpdateHandler.sendEmptyMessage(MSG_UPDATE_TIME);
}
public void stop() {
mRunning = false;
mView = null;
mUpdateHandler.removeMessages(MSG_UPDATE_TIME);
}
}
}<span style="font-size:18px;">
</span> ide