MyException - 我的异常网
当前位置:我的异常网» Android » Android之场景圆桌面(二)-模拟时钟实现

Android之场景圆桌面(二)-模拟时钟实现

www.MyException.Cn  网友分享于:2013-10-09  浏览:265次
Android之场景桌面(二)----模拟时钟实现

之前关于场景桌面Android之场景桌面(一)作了一个大概的描述,总体实现比较简单。今天跟大家分享一下一个自定义View ----模拟时钟的具体实现,先来看看效果图吧,单独提取出来的,相比场景桌面中的模拟时钟,多加了一个秒针、多显示了日期和星期。在场景桌面中,为了桌面的整体效率,就忍痛割爱,把秒针去掉了,因为一秒刷新一次界面实在是有点没必要,而且还比较影响桌面的流畅性。这里仅是一个简单的例子,加上亦无伤大雅。



关于自定义View,不得不说说几个经常使用的函数了:

①.三个构造器:需要特别注意的一点是:这里在有三个参数的构造器里面做了所有的初始化工作,因此,另外两个构造器必须直接或间接的调用最后一个构造器,比如,在单个参数的构造器中调用this(context, null);即调用双参数的构造,再在双参数构造器中调用this(context, attrs, 0);最后实际上调用的第三个构造器。

另外,我们来详细分析一下第三个构造器,主要是自定义属性attrs。

public AnalogClock(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		Resources r = getContext().getResources();
		// 下面是从layout文件中读取所使用的图片资源,如果没有则使用默认的
		TypedArray a = context.obtainStyledAttributes(attrs,
				R.styleable.AnalogClock, defStyle, 0);
		mDial = a.getDrawable(R.styleable.AnalogClock_dial);
		mHourHand = a.getDrawable(R.styleable.AnalogClock_hand_hour);
		mMinuteHand = a.getDrawable(R.styleable.AnalogClock_hand_minute);
		mSecondHand = a.getDrawable(R.styleable.AnalogClock_hand_second);

		// 为了整体美观性,只要缺少一张图片,我们就用默认的那套图片
		if (mDial == null || mHourHand == null || mMinuteHand == null
				|| mSecondHand == null) {
			mDial = r.getDrawable(R.drawable.appwidget_clock_dial);
			mHourHand = r.getDrawable(R.drawable.appwidget_clock_hour);
			mMinuteHand = r.getDrawable(R.drawable.appwidget_clock_minute);
			mSecondHand = r.getDrawable(R.drawable.appwidget_clock_second);
		}
		a.recycle();// 不调用这个函数,则上面的都是白费功夫

		// 获取表盘的宽度和高度
		mDialWidth = mDial.getIntrinsicWidth();
		mDialHeight = mDial.getIntrinsicHeight();

		// 初始化画笔
		mPaint = new Paint();
		mPaint.setColor(Color.parseColor("#3399ff"));
		mPaint.setTypeface(Typeface.DEFAULT_BOLD);
		mPaint.setFakeBoldText(true);
		mPaint.setAntiAlias(true);

		// 初始化Time对象
		if (mCalendar == null) {
			mCalendar = new Time();
		}
	}

大家都知道,我们在layout中放一个TextView时,可以在布局中使用的 android:text="" 等属性,但我们又知道,这些属性是Android自带的,都是以android:打头的,其实,我们可以自定义一些属性,比如说这个模拟时钟,他有表盘、时针、分针、秒针等图片资源,如果我们通过自定义这些属性,就可以动态的更换一整套皮肤了,灵活性更大,下面是具体步骤:

首先,我们需要在res/values目录下新建一个attrs.xml文件,然后在其中声明我们需要自定义的属性,例如:

    <declare-styleable name="AnalogClock">
        <attr name="dial" format="reference" />
        <attr name="hand_hour" format="reference" />
        <attr name="hand_minute" format="reference" />
        <attr name="hand_second" format="reference" />
    </declare-styleable>
其次,在上述3参数的构造器中读取这些参数值:

		// 下面是从layout文件中读取所使用的图片资源,如果没有则使用默认的
		TypedArray a = context.obtainStyledAttributes(attrs,
				R.styleable.AnalogClock, defStyle, 0);
		mDial = a.getDrawable(R.styleable.AnalogClock_dial);
		mHourHand = a.getDrawable(R.styleable.AnalogClock_hand_hour);
		mMinuteHand = a.getDrawable(R.styleable.AnalogClock_hand_minute);
		mSecondHand = a.getDrawable(R.styleable.AnalogClock_hand_second);
		a.recycle();// 不调用这个函数,则上面的都是白费功夫
最后,就是给我们声明的这些属性赋值了,有两种方法,类似TextView,可以在layout中,也可以在style中,比如我这里就是在style中做的,

总共有两套图片资源,使用起来灵活性更高。

	<!--first style -->
    <style name="AppWidget">
        <item name="dial">@drawable/appwidget_clock_dial</item>
        <item name="hand_hour">@drawable/appwidget_clock_hour</item>
        <item name="hand_minute">@drawable/appwidget_clock_minute</item>
        <item name="hand_second">@drawable/appwidget_clock_second</item>
    </style>
	<!-- second style -->
    <style name="AppWidget2">
        <item name="dial">@drawable/appwidget_clock_dial2</item>
        <item name="hand_hour">@drawable/appwidget_clock_hour2</item>
        <item name="hand_minute">@drawable/appwidget_clock_minute2</item>
        <item name="hand_second">@drawable/appwidget_clock_second2</item>
    </style>
最最后是在layout中引用该style了:

<com.way.clock.AnalogClock xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:clock="http://schemas.android.com/apk/res-auto"
    android:id="@+id/analog_appwidget"
    style="@style/AppWidget2"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />


②.onMeasureonMeasure方法在控件的父元素正要放置它的子控件时调用,它会问一个问题:“你想要用多大地方啊?”,然后传入两个参数——widthMeasureSpec和heightMeasureSpec。它们指明控件可获得的空间以及关于这个空间描述的元数据。比返回一个结果要好的方法是你传递View的高度和宽度到setMeasuredDimension方法里。

边界参数——widthMeasureSpec和heightMeasureSpec ,效率的原因以整数的方式传入。

 MeasureSpec这个类封装了父布局传递给子布局的布局要求,每个MeasureSpec代表了一组宽度和高度的要求。一个MeasureSpec由大小和模式组成。它有三种模式:

UNSPECIFIED(未指定),     父元素不对自元素施加任何束缚,子元素可以得到任意想要的大小;

        EXACTLY(完全),父元素决定自元素的确切大小,子元素将被限定在给定的边界里而忽略它本身大小;

        AT_MOST(至多),子元素至多达到指定大小的值。

   它常用的三个函数:

  static int getMode(int measureSpec):根据提供的测量值(格式)提取模式(上述三个模式之一)

  static int getSize(int measureSpec):根据提供的测量值(格式)提取大小值(这个大小也就是我们通常所说的大小)

  static int makeMeasureSpec(int size,int mode):根据提供的大小值和模式创建一个测量值(格式)

这个类的使用呢,通常在view组件的onMeasure方法里面调用但也有少数例外(这里不多说例外情况),在它们使用之前,首先要做的是使用MeasureSpec类的静态方法getMode和getSize来译解,如下面的片段所示:

int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);

依据specMode的值,如果是AT_MOST,specSize 代表的是最大可获得的空间;如果是EXACTLY,specSize 代表的是精确的尺寸;如果是UNSPECIFIED,对于控件尺寸来说,没有任何参考意义。
  当以EXACT方式标记测量尺寸,父元素会坚持在一个指定的精确尺寸区域放置View。在父元素问子元素要多大空间时,AT_MOST指示者会说给我最大的范围。在很多情况下,你得到的值都是相同的。
  在两种情况下,你必须绝对的处理这些限制。在一些情况下,它可能会返回超出这些限制的尺寸,在这种情况下,你可以让父元素选择如何对待超出的View,使用裁剪还是滚动等技术。

我这个模拟时钟View的处理方式就是: 如果模式不是UNSPECIFIED,并且父元素提供的宽度比图片宽度小,就需要压缩一下子元素的宽度。

③.onDraw:很大一部分自定义View都需要实现这个函数,很多Android自带的空间满足不了我们需求的时候,我们就得自己来画这个控件,比如这个模拟时钟,我们是一层一层画上去的,首先在三个构造器函数中作一些初始化工作,获取所有的图片资源,然后通过onMeasure函数计算该控件所占的位置,最后再调用onDraw函数将所以图片根据当前时间画在View上,从而实现一个满足我们需求的自定义控件。

从上面截图,我们可以发现,最先画上去的肯定是表盘,因为他必须显示在最底层,紧接着我们把日期和星期画在表盘上,最后一次画时针、分针、秒针,然后通过注册一个线程,每一秒更新一次,即调用一次onDraw函数,从而实现秒针时时在动的效果。

值得一提的是,画时针、分针和秒针的时候,经常调用canvas.restore();和canvas.save();它们到底是干什么用的呢?

save:用来保存Canvas的状态。save之后,可以调用Canvas的平移、放缩、旋转、错切、裁剪等操作。

restore:用来恢复Canvas之前保存的状态。防止save后对Canvas执行的操作对后续的绘制有影响。

save和restore要配对使用(restore可以比save少,但不能多),如果restore调用次数比save多,会引发Error。

我们看看下面这段代码,在画分针之前,我们先调用save函数,把之前所作的操作保存起来,因为我们紧接着要调用canvas.rotate(mMinutes / 60.0f * 360.0f, x, y);把画板旋转一定角度来画分针,调用minuteHand.draw(canvas);将分针画在画板之后,我们需要canvas.restore();来释放画板,因为我们之前不是旋转此画板嘛,释放之后才能将画板还原到旋转之前状态,再执行画秒针的操作。

		canvas.save();
		// 然后画分针
		canvas.rotate(mMinutes / 60.0f * 360.0f, x, y);
		final Drawable minuteHand = mMinuteHand;
		if (changed) {
			w = minuteHand.getIntrinsicWidth();
			h = minuteHand.getIntrinsicHeight();
			minuteHand.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y
					+ (h / 2));
		}
		minuteHand.draw(canvas);
		canvas.restore();

OK,分步分析就到这里了,下面是完整的自定义View代码,注释什么的算比较详细吧,完整工程代码就不分享出来了,可以去下载我那个场景UI提取,多谢合作!

/**
 * 
 * This widget display an analogic clock with three hands for hours minutes and
 * seconds.
 * 
 * @author way
 */
@SuppressLint("NewApi")
public class AnalogClock extends View {
	private Time mCalendar;

	private Drawable mHourHand;// 时针
	private Drawable mMinuteHand;// 分针
	private Drawable mSecondHand;// 秒针
	private Drawable mDial;// 表盘

	private String mDay;// 日期
	private String mWeek;// 星期

	private int mDialWidth;// 表盘宽度
	private int mDialHeight;// 表盘高度

	private final Handler mHandler = new Handler();
	private float mHour;// 时针值
	private float mMinutes;// 分针之
	private float mSecond;// 秒针值
	private boolean mChanged;// 是否需要更新界面

	private Paint mPaint;// 画笔

	private Runnable mTicker;// 由于秒针的存在,因此我们需要每秒钟都刷新一次界面,用的就是此任务

	private boolean mTickerStopped = false;// 是否停止更新时间,当View从窗口中分离时,不需要更新时间了

	public AnalogClock(Context context) {
		this(context, null);
	}

	public AnalogClock(Context context, AttributeSet attrs) {
		this(context, attrs, 0);
	}

	public AnalogClock(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		Resources r = getContext().getResources();
		// 下面是从layout文件中读取所使用的图片资源,如果没有则使用默认的
		TypedArray a = context.obtainStyledAttributes(attrs,
				R.styleable.AnalogClock, defStyle, 0);
		mDial = a.getDrawable(R.styleable.AnalogClock_dial);
		mHourHand = a.getDrawable(R.styleable.AnalogClock_hand_hour);
		mMinuteHand = a.getDrawable(R.styleable.AnalogClock_hand_minute);
		mSecondHand = a.getDrawable(R.styleable.AnalogClock_hand_second);

		// 为了整体美观性,只要缺少一张图片,我们就用默认的那套图片
		if (mDial == null || mHourHand == null || mMinuteHand == null
				|| mSecondHand == null) {
			mDial = r.getDrawable(R.drawable.appwidget_clock_dial);
			mHourHand = r.getDrawable(R.drawable.appwidget_clock_hour);
			mMinuteHand = r.getDrawable(R.drawable.appwidget_clock_minute);
			mSecondHand = r.getDrawable(R.drawable.appwidget_clock_second);
		}
		a.recycle();// 不调用这个函数,则上面的都是白费功夫

		// 获取表盘的宽度和高度
		mDialWidth = mDial.getIntrinsicWidth();
		mDialHeight = mDial.getIntrinsicHeight();

		// 初始化画笔
		mPaint = new Paint();
		mPaint.setColor(Color.parseColor("#3399ff"));
		mPaint.setTypeface(Typeface.DEFAULT_BOLD);
		mPaint.setFakeBoldText(true);
		mPaint.setAntiAlias(true);

		// 初始化Time对象
		if (mCalendar == null) {
			mCalendar = new Time();
		}
	}

	/**
	 * 时间改变时调用此函数,来更新界面的绘制
	 */
	private void onTimeChanged() {
		mCalendar.setToNow();// 时间设置为当前时间
		// 下面是获取时、分、秒、日期和星期
		int hour = mCalendar.hour;
		int minute = mCalendar.minute;
		int second = mCalendar.second;
		mDay = String.valueOf(mCalendar.year) + "-"
				+ String.valueOf(mCalendar.month + 1) + "-"
				+ String.valueOf(mCalendar.monthDay);
		mWeek = this.getWeek(mCalendar.weekDay);

		mHour = hour + mMinutes / 60.0f + mSecond / 3600.0f;// 小时值,加上分和秒,效果会更加逼真
		mMinutes = minute + second / 60.0f;// 分钟值,加上秒,也是为了使效果逼真
		mSecond = second;

		mChanged = true;// 此时需要更新界面了

		updateContentDescription(mCalendar);// 作为一种辅助功能提供,为一些没有文字描述的View提供说明
	}

	@Override
	protected void onAttachedToWindow() {
		mTickerStopped = false;// 添加到窗口中就要更新时间了
		super.onAttachedToWindow();

		/**
		 * requests a tick on the next hard-second boundary
		 */
		mTicker = new Runnable() {
			public void run() {
				if (mTickerStopped)
					return;
				onTimeChanged();
				invalidate();
				long now = SystemClock.uptimeMillis();
				long next = now + (1000 - now % 1000);// 计算下次需要更新的时间间隔
				mHandler.postAtTime(mTicker, next);// 递归执行,就达到秒针一直在动的效果
			}
		};
		mTicker.run();
	}

	@Override
	protected void onDetachedFromWindow() {
		super.onDetachedFromWindow();
		mTickerStopped = true;// 当view从当前窗口中移除时,停止更新
	}

	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		// 模式: UNSPECIFIED(未指定),父元素不对子元素施加任何束缚,子元素可以得到任意想要的大小;
		// EXACTLY(完全),父元素决定自元素的确切大小,子元素将被限定在给定的边界里而忽略它本身大小;
		// AT_MOST(至多),子元素至多达到指定大小的值。
		// 根据提供的测量值(格式)提取模式(上述三个模式之一)
		int widthMode = MeasureSpec.getMode(widthMeasureSpec);
		// 根据提供的测量值(格式)提取大小值(这个大小也就是我们通常所说的大小)
		int widthSize = MeasureSpec.getSize(widthMeasureSpec);
		// 高度与宽度类似
		int heightMode = MeasureSpec.getMode(heightMeasureSpec);
		int heightSize = MeasureSpec.getSize(heightMeasureSpec);

		float hScale = 1.0f;// 缩放值
		float vScale = 1.0f;

		if (widthMode != MeasureSpec.UNSPECIFIED && widthSize < mDialWidth) {
			hScale = (float) widthSize / (float) mDialWidth;// 如果父元素提供的宽度比图片宽度小,就需要压缩一下子元素的宽度
		}

		if (heightMode != MeasureSpec.UNSPECIFIED && heightSize < mDialHeight) {
			vScale = (float) heightSize / (float) mDialHeight;// 同上
		}

		float scale = Math.min(hScale, vScale);// 取最小的压缩值,值越小,压缩越厉害
		// 最后保存一下,这个函数一定要调用
		setMeasuredDimension(
				resolveSizeAndState((int) (mDialWidth * scale),
						widthMeasureSpec, 0),
				resolveSizeAndState((int) (mDialHeight * scale),
						heightMeasureSpec, 0));
	}

	@Override
	protected void onSizeChanged(int w, int h, int oldw, int oldh) {
		super.onSizeChanged(w, h, oldw, oldh);
		mChanged = true;
	}

	@Override
	protected void onDraw(Canvas canvas) {
		super.onDraw(canvas);

		boolean changed = mChanged;

		if (changed) {
			mChanged = false;
		}

		int availableWidth = getRight() - getLeft();// view可用宽度,通过右坐标减去左坐标
		int availableHeight = getBottom() - getTop();// view可用高度,通过下坐标减去上坐标

		int x = availableWidth / 2;// view宽度中心点坐标
		int y = availableHeight / 2;// view高度中心点坐标

		final Drawable dial = mDial;// 表盘图片
		int w = dial.getIntrinsicWidth();// 表盘宽度
		int h = dial.getIntrinsicHeight();

		// int dialWidth = w;
		int dialHeight = h;
		boolean scaled = false;
		// 最先画表盘,最底层的要先画上画板
		if (availableWidth < w || availableHeight < h) {// 如果view的可用宽高小于表盘图片,就要缩小图片
			scaled = true;
			float scale = Math.min((float) availableWidth / (float) w,
					(float) availableHeight / (float) h);// 计算缩小值
			canvas.save();
			canvas.scale(scale, scale, x, y);// 实际上是缩小的画板
		}

		if (changed) {// 设置表盘图片位置。组件在容器X轴上的起点; 组件在容器Y轴上的起点; 组件的宽度;组件的高度
			dial.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y + (h / 2));
		}
		dial.draw(canvas);// 这里才是真正把表盘图片画在画板上
		canvas.save();// 一定要保存一下
		// 其次画日期
		if (changed) {
			w = (int) (mPaint.measureText(mWeek));// 计算文字的宽度
			canvas.drawText(mWeek, (x - w / 2), y - (dialHeight / 8), mPaint);// 画文字在画板上,位置为中间两个参数
			w = (int) (mPaint.measureText(mDay));
			canvas.drawText(mDay, (x - w / 2), y + (dialHeight / 8), mPaint);// 同上
		}
		// 再画时针
		canvas.rotate(mHour / 12.0f * 360.0f, x, y);// 旋转画板,第一个参数为旋转角度,第二、三个参数为旋转坐标点
		final Drawable hourHand = mHourHand;
		if (changed) {
			w = hourHand.getIntrinsicWidth();
			h = hourHand.getIntrinsicHeight();
			hourHand.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y
					+ (h / 2));
		}
		hourHand.draw(canvas);// 把时针画在画板上
		canvas.restore();// 恢复画板到最初状态

		canvas.save();
		// 然后画分针
		canvas.rotate(mMinutes / 60.0f * 360.0f, x, y);
		final Drawable minuteHand = mMinuteHand;
		if (changed) {
			w = minuteHand.getIntrinsicWidth();
			h = minuteHand.getIntrinsicHeight();
			minuteHand.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y
					+ (h / 2));
		}
		minuteHand.draw(canvas);
		canvas.restore();

		canvas.save();
		// 最后画秒针
		canvas.rotate(mSecond / 60.0f * 360.0f, x, y);
		final Drawable secondHand = mSecondHand;
		if (changed) {
			w = secondHand.getIntrinsicWidth();
			h = secondHand.getIntrinsicHeight();
			secondHand.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y
					+ (h / 2));
		}
		secondHand.draw(canvas);
		canvas.restore();

		if (scaled) {
			canvas.restore();
		}
	}

	/**
	 * 对这个view描述一下,
	 * 
	 * @param time
	 */
	private void updateContentDescription(Time time) {
		final int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_24HOUR;
		String contentDescription = DateUtils.formatDateTime(getContext(),
				time.toMillis(false), flags);
		setContentDescription(contentDescription);
	}

	/**
	 * 获取当前星期
	 * 
	 * @param week
	 * @return
	 */
	private String getWeek(int week) {
		switch (week) {
		case 1:
			return this.getContext().getString(R.string.monday);
		case 2:
			return this.getContext().getString(R.string.tuesday);
		case 3:
			return this.getContext().getString(R.string.wednesday);
		case 4:
			return this.getContext().getString(R.string.thursday);
		case 5:
			return this.getContext().getString(R.string.friday);
		case 6:
			return this.getContext().getString(R.string.saturday);
		case 0:
			return this.getContext().getString(R.string.sunday);
		default:
			return "";
		}
	}

}


文章评论

程序员都该阅读的书
程序员都该阅读的书
Java程序员必看电影
Java程序员必看电影
老程序员的下场
老程序员的下场
我的丈夫是个程序员
我的丈夫是个程序员
Web开发者需具备的8个好习惯
Web开发者需具备的8个好习惯
Web开发人员为什么越来越懒了?
Web开发人员为什么越来越懒了?
 程序员的样子
程序员的样子
科技史上最臭名昭著的13大罪犯
科技史上最臭名昭著的13大罪犯
“懒”出效率是程序员的美德
“懒”出效率是程序员的美德
什么才是优秀的用户界面设计
什么才是优秀的用户界面设计
程序员必看的十大电影
程序员必看的十大电影
一个程序员的时间管理
一个程序员的时间管理
当下全球最炙手可热的八位少年创业者
当下全球最炙手可热的八位少年创业者
为啥Android手机总会越用越慢?
为啥Android手机总会越用越慢?
程序猿的崛起——Growth Hacker
程序猿的崛起——Growth Hacker
60个开发者不容错过的免费资源库
60个开发者不容错过的免费资源库
总结2014中国互联网十大段子
总结2014中国互联网十大段子
“肮脏的”IT工作排行榜
“肮脏的”IT工作排行榜
程序员眼里IE浏览器是什么样的
程序员眼里IE浏览器是什么样的
那些争议最大的编程观点
那些争议最大的编程观点
不懂技术不要对懂技术的人说这很容易实现
不懂技术不要对懂技术的人说这很容易实现
程序员的鄙视链
程序员的鄙视链
要嫁就嫁程序猿—钱多话少死的早
要嫁就嫁程序猿—钱多话少死的早
鲜为人知的编程真相
鲜为人知的编程真相
5款最佳正则表达式编辑调试器
5款最佳正则表达式编辑调试器
代码女神横空出世
代码女神横空出世
写给自己也写给你 自己到底该何去何从
写给自己也写给你 自己到底该何去何从
聊聊HTTPS和SSL/TLS协议
聊聊HTTPS和SSL/TLS协议
如何成为一名黑客
如何成为一名黑客
10个调试和排错的小建议
10个调试和排错的小建议
亲爱的项目经理,我恨你
亲爱的项目经理,我恨你
Java 与 .NET 的平台发展之争
Java 与 .NET 的平台发展之争
为什么程序员都是夜猫子
为什么程序员都是夜猫子
编程语言是女人
编程语言是女人
程序员最害怕的5件事 你中招了吗?
程序员最害怕的5件事 你中招了吗?
漫画:程序员的工作
漫画:程序员的工作
十大编程算法助程序员走上高手之路
十大编程算法助程序员走上高手之路
我跳槽是因为他们的显示器更大
我跳槽是因为他们的显示器更大
程序员应该关注的一些事儿
程序员应该关注的一些事儿
中美印日四国程序员比较
中美印日四国程序员比较
做程序猿的老婆应该注意的一些事情
做程序猿的老婆应该注意的一些事情
初级 vs 高级开发者 哪个性价比更高?
初级 vs 高级开发者 哪个性价比更高?
如何区分一个程序员是“老手“还是“新手“?
如何区分一个程序员是“老手“还是“新手“?
老美怎么看待阿里赴美上市
老美怎么看待阿里赴美上市
团队中“技术大拿”并非越多越好
团队中“技术大拿”并非越多越好
软件开发程序错误异常ExceptionCopyright © 2009-2015 MyException 版权所有