Android scene of the desktop (two) ---- realize analog clock

Before the scene on the desktop Android scene of the desktop (a) made a rough description of overall relatively simple to achieve. Today to share with you a concrete realization View ---- custom analog clock, take a look at renderings of it, alone extracted, compared to a desktop analog clock in the scene, to pay more a second, multi-display date and weeks. In the scene the desktop, to the overall efficiency of the desktop, then reluctantly part, the second hand removed, because one second refresh interface is a bit not necessary, but also more influence fluency desktop. Here is a simple example only, plus also harmless.



About Custom View, you have to talk about a few of the frequently used functions:

①. Three constructors: to note is this: there are three parameters in the constructor which does all the initialization, therefore, the other two constructors must call either directly or indirectly the last constructor, for example, single parameter in the constructor call of this (context, null); i.e., dual call configuration parameters, and then calls this constructor in the two-parameter (context, attrs, 0); and finally a third actually calls the constructor.

In addition, we analyze in detail the third constructor, primarily custom attributes 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();
		}
	}

As we all know, we put a TextView in the layout, you can in android using layout: text = "" attributes, but we also know that these attributes are built-in Android, are based on android: beginning, in fact, we can customize a number of attributes, such as the analog clock, he has a dial, hour, minute and second hands and other image resources, if we define these properties by self, you can replace a set of dynamic skin, and greater flexibility, Here are the steps:

First of all, we need to create a attrs.xml file under res / values ​​directory, and then declare the properties in which we need to customize, such as:

<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>
Secondly, these parameter values ​​read in the constructor of the three parameters:

// 下面是从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();// 不调用这个函数,则上面的都是白费功夫
Finally, we declare these properties is to give the assignment, there are two ways, similar TextView, in the layout can also be in style, such as I have here is to do in the style of,

There are two sets of pictures resources, flexibility to use higher.

<!--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>
The last is the most quoted in the layout style in which:

<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" />


②. OnMeasure : onMeasure method was about to place it in the parent's child controls elements of the control of the call, it will ask a question: "Do you want a little place ah?", And then two arguments --widthMeasureSpec and heightMeasureSpec. They indicate the space available controls and metadata about the space below. Much better than the method returns a result that you pass View of the height and width to setMeasuredDimension method in.

The reason boundary parameters --widthMeasureSpec and heightMeasureSpec, efficiency incoming integer way.

 MeasureSpec This class encapsulates pass the parent layout to the sub layout layout requirements, each MeasureSpec represent the width and height requirements set. A MeasureSpec by the size and pattern of the composition. It has three modes:

UNSPECIFIED ( not specified ),      the parent element from the element does not impose any constraints, sub-elements can be obtained in any desired size;

        EXACTLY the ( complete ) , decided the exact size of the parent element of the elements, sub-elements will be limited to a given boundary in ignoring its own size;

        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函数,从而实现秒针时时在动的效果。

It is worth mentioning that the painting hour, minute and second hands, we often call canvas.restore (); and canvas.save (); which in the end is doing with it?

save: to save the state of the Canvas. After the save, the translation may call the Canvas, scaling, rotation, shearing, cutting and other operations.

restore: to restore Canvas previously saved state. After the save prevent influence on the subsequent drawing operation performed Canvas.

To save and restore paired (restore may be less than the save, but no more), if restore the number of calls than save, cause Error.

We take a look at the following code, before painting the minute, let's call the save function to save up operation done before, because we want to immediately call canvas.rotate (mMinutes / 60.0f * 360.0f, x, y); the drawing board rotated by a certain angle to draw the minute hand, call minuteHand.draw (canvas); the minute hand after painting the drawing board, we need canvas.restore (); after releasing the drawing board, because we do not turn this before drawing board Well, in order to release the drawing board restore the state before the rotation, and then perform the operation of the second hand painting.

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, step by step analysis on here, Here is the complete code for a custom View, what's more detailed comments it count, complete project code is not shared out, I can go to download the UI scene extraction. Thank you!

/**
 * 
 * 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 "";
		}
	}

}


Reproduced in: https: //my.oschina.net/cjkall/blog/195799

Guess you like

Origin blog.csdn.net/weixin_34375251/article/details/91756418