现在的位置: 首页 > 综合 > 正文

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

2013年10月11日 ⁄ 综合 ⁄ 共 12695字 ⁄ 字号 评论关闭

之前关于场景桌面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 "";
		}
	}

}

抱歉!评论已关闭.