前言

魅族手机的闹钟应用中有个倒计时,这个控件还是蛮有趣的。左边是魅族闹钟,右边是我们最终实现的效果,虽然有些细节还需优化,不过基本上已经达到了想要的效果,我们先来就来看看如何实现吧。

分析

确定宽高

对一个Android自定义控件来说,一般都经过三个步骤

  • onLayout()

  • onMeasure()

  • onDraw()

onLayout明确子控件在父控件中的位置(本控件不需要重写),onMeasure是确定控件的大小(宽、高),而onDraw是我们重点关注的方法,我们需要在这个方法中写入显示View的逻辑代码。

对于本控件,控件的高度 应该等于细线的高度(mLineHeight)加上数字的高度(mFontHeight),当然为了好看,中间需要设上一些边距(mPadding),因此本控件的高度应该为 mFontHeight + mLineHeight + 10 + mPadding,测量代码如下

    private int measureHeight(int heightMeasureSpec) {
int mode = MeasureSpec.getMode(heightMeasureSpec);
int size = MeasureSpec.getSize(heightMeasureSpec);
switch (mode) {
case MeasureSpec.EXACTLY:
return size ;
case MeasureSpec.AT_MOST:
return Math.min(size, mFontHeight + mLineHeight + 10 + mPadding) ;
}
return size ;
}

同样地,控件的宽度其实就是0~1000的间隔,测量代码如下

    private int measureWidth(int widthMeasureSpec) {
int mode = MeasureSpec.getMode(widthMeasureSpec);
int size = MeasureSpec.getSize(widthMeasureSpec);
switch (mode) {
case MeasureSpec.EXACTLY:
return size ;
case MeasureSpec.AT_MOST:
int result = getPaddingLeft() + mContentWidth + getPaddingRight() ;
return Math.min(size, result) ;
}
return size ;
}

画刻度尺

重点在于刻度尺的计算。思路是先draw上头的数字,然后再draw下边的线条,判断位置确定是否需要draw上头的数字即可。其实就是坐标的计算。代码如下

        int startX = mPadding;
int stopX = mPadding;
int stopY =mHeight - mPadding;
for (int i = 0 ; i<=mContentWidth ; i += mFontWidth)
if (i % (mFontWidth *10) == 0) {
canvas.drawLine(startX + i, mFontHeight + mPadding + 5 , stopX + i, stopY, mTextPaint);
canvas.drawText(i + "", startX + i, mFontHeight + mPadding, mTextPaint);
} else if (i % mFontWidth == 0) {
canvas.drawLine(startX + i, mFontHeight + mPadding + 10, stopX + i, stopY, mPaint);
}

让View动起来

Android本身提供了移动View的API,因此让View动起来也是不难的。两种思路

  • 监听Touch事件,当Touch坐标变化时,计算坐标位置,不断调用scrollTo(x,0)达到变换坐标的目的

  • 监听Touch事件,记录上次横坐标和本次横坐标的差值,然后调用scrollBy(delta, 0) 即可移动

其实两种方法本质上都是一样的。ScrollBy其实也是调用了scrollTo方法。本文采用方法二。其代码如下

@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastX = (int) event.getX();
break;
case MotionEvent.ACTION_MOVE:
int x = (int) event.getX();
int deltaX = x - mLastX;
scrollBy(-deltaX, 0);
mLastX = x;
break;
}
return true;
}

当然了,我们是不能让View无限移动的,因此需要重写scrollBy方法,限制View不能超过边界。

代码如下

    @Override
public void scrollBy(int x, int y) {
super.scrollBy(x, y);
if (x < 0) { // drag to left
if (getScrollX() < -getCenter() + mPadding) {
scrollTo(-getCenter() + mPadding, 0);
}
} else
if (x >0) { // drag to right
if (mContentWidth - getScrollX() + x < getCenter()) {
scrollTo(mContentWidth - getCenter() + mFontWidth, 0);
}
}
}

当超过边界时,直接调用scrollTo,让View停留在特定的位置即可。需要注意的一点是,View往左滑动时,ScrollX的值是负的。

完整代码

package com.nancyyihao.demo;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Scroller; public class RulerView extends View { private static final String TAG = RulerView.class.getSimpleName() ;
private TextPaint mTextPaint;
private Paint mPaint ; private int mWidth;
private int mHeight;
private int mPadding = 10;
private Scroller mScroller ;
private int mLastX;
private int mContentWidth = 1000;
private int mLineHeight = 50;
private int mFontHeight;
private int mFontWidth = 10; private onValueChangedListener mValueChangedListener; public interface onValueChangedListener {
void onValueChanged(int newValue);
} public RulerView(Context context) {
super(context);
init(context);
} public RulerView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
} public RulerView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
} public void setOnValueChangedListener(onValueChangedListener listener) {
this.mValueChangedListener = listener ;
} private void init(Context context) {
mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | Paint.LINEAR_TEXT_FLAG);
mTextPaint.setTextAlign(Paint.Align.CENTER);
mTextPaint.setColor(Color.parseColor("#FF4081"));
mTextPaint.setTextSize(30);
mTextPaint.setStrokeWidth(2f);
Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
mFontHeight = Math.round(Math.abs(fontMetrics.top) + Math.abs(fontMetrics.bottom)) ; mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mPaint.setColor(Color.DKGRAY);
mPaint.setStrokeWidth(2f);
mPaint.setTextSize(30);
mPaint.setTextAlign(Paint.Align.CENTER); //mScroller = new Scroller(context) ;
} @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
mWidth = measureWidth(widthMeasureSpec);
mHeight = measureHeight(heightMeasureSpec);
setMeasuredDimension(mWidth , mHeight );
} private int measureWidth(int widthMeasureSpec) {
int mode = MeasureSpec.getMode(widthMeasureSpec);
int size = MeasureSpec.getSize(widthMeasureSpec);
switch (mode) {
case MeasureSpec.EXACTLY:
return size ;
case MeasureSpec.AT_MOST:
int result = getPaddingLeft() + mContentWidth + getPaddingRight() ;
return Math.min(size, result) ;
}
return size ;
} private int measureHeight(int heightMeasureSpec) {
int mode = MeasureSpec.getMode(heightMeasureSpec);
int size = MeasureSpec.getSize(heightMeasureSpec);
switch (mode) {
case MeasureSpec.EXACTLY:
return size ;
case MeasureSpec.AT_MOST:
return Math.min(size, mFontHeight + mLineHeight + 10 + mPadding) ;
}
return size ;
} // private void smoothScrollTo(int destX, int destY) {
// int scrollX = getScrollX() ;
// int delta = destX - scrollX ;
// mScroller.startScroll(scrollX, 0, delta, 0 , 1000);
// invalidate();
// }
//
// @Override
// public void computeScroll() {
// super.computeScroll();
// if (mScroller.computeScrollOffset()) {
// smoothScrollTo(mScroller.getCurrX(), mScroller.getCurrY());
// postInvalidate();
//// ((View) getParent()).scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
//// invalidate();
// }
// } @Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int startX = mPadding;
int stopX = mPadding;
int stopY =mHeight - mPadding;
for (int i = 0 ; i<=mContentWidth ; i += mFontWidth)
if (i % (mFontWidth *10) == 0) {
canvas.drawLine(startX + i, mFontHeight + mPadding + 5 , stopX + i, stopY, mTextPaint);
canvas.drawText(i + "", startX + i, mFontHeight + mPadding, mTextPaint);
} else if (i % mFontWidth == 0) {
canvas.drawLine(startX + i, mFontHeight + mPadding + 10, stopX + i, stopY, mPaint);
}
} private int calcValue() {
return ( getCenter() + getScrollX() - mPadding) ; //minus startX
} private int getCenter() {
return (getRight() - getLeft()) / 2 ;
} @Override
public void scrollBy(int x, int y) {
super.scrollBy(x, y);
if (x < 0) { // drag to left
if (getScrollX() < -getCenter() + mPadding) {
scrollTo(-getCenter() + mPadding, 0);
}
} else
if (x >0) { // drag to right
if (mContentWidth - getScrollX() + x < getCenter()) {
scrollTo(mContentWidth - getCenter() + mFontWidth, 0);
}
}
} @Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastX = (int) event.getX();
break;
case MotionEvent.ACTION_MOVE:
int x = (int) event.getX();
int deltaX = x - mLastX;
scrollBy(-deltaX, 0);
mLastX = x;
if (mValueChangedListener != null)
mValueChangedListener.onValueChanged(calcValue());
break;
}
return true;
}
}

总结

整体上还是比较粗糙,原形虽然有了,但是还需要优化。

参考

【Android】自定义View —— 滑动的次数选择器

android 滚轮刻度尺的实现

Android View自定义专题二(View滑动的实现)

Android自定义View(二)的更多相关文章

  1. Android自定义View (二) 进阶

    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/24300125 继续自定义View之旅,前面已经介绍过一个自定义View的基础的例 ...

  2. Android 自定义View (二) 进阶

    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/24300125 继续自定义View之旅,前面已经介绍过一个自定义View的基础的例 ...

  3. Android 自定义View二(深入了解自定义属性attrs.xml)

    1.为什么要自定义属性 要使用属性,首先这个属性应该存在,所以如果我们要使用自己的属性,必须要先把他定义出来才能使用.但我们平时在写布局文件的时候好像没有自己定义属性,但我们照样可以用很多属性,这是为 ...

  4. Android 自定义view(二) —— attr 使用

    前言: attr 在前一篇文章<Android 自定义view -- attr理解>已经简单的进行了介绍和创建,那么这篇文章就来一步步说说attr的简单使用吧 自定义view简单实现步骤 ...

  5. Android 自定义View及其在布局文件中的使用示例(二)

    转载请注明出处 http://www.cnblogs.com/crashmaker/p/3530213.html From crash_coder linguowu linguowu0622@gami ...

  6. Android自定义View(二、深入解析自定义属性)

    转载请标明出处: http://blog.csdn.net/xmxkf/article/details/51468648 本文出自:[openXu的博客] 目录: 为什么要自定义属性 怎样自定义属性 ...

  7. Android 自定义 view(三)—— onDraw 方法理解

    前言: 上一篇已经介绍了用自己定义的属性怎么简单定义一个view<Android 自定义view(二) -- attr 使用>,那么接下来我们继续深究自定义view,下一步将要去简单理解自 ...

  8. Android 自定义View及其在布局文件中的使用示例(三):结合Android 4.4.2_r1源码分析onMeasure过程

    转载请注明出处 http://www.cnblogs.com/crashmaker/p/3549365.html From crash_coder linguowu linguowu0622@gami ...

  9. android自定义View之仿通讯录侧边栏滑动,实现A-Z字母检索

    我们的手机通讯录一般都有这样的效果,如下图: OK,这种效果大家都见得多了,基本上所有的android手机通讯录都有这样的效果.那我们今天就来看看这个效果该怎么实现. 一.概述 1.页面功能分析 整体 ...

  10. Android自定义View研究--View中的原点坐标和XML中布局自定义View时View触摸原点问题

    这里只做个汇总~.~独一无二 文章出处:http://blog.csdn.net/djy1992/article/details/9715047 Android自定义View研究--View中的原点坐 ...

随机推荐

  1. FormsAuthentication类

    理解代码: string cookieName = FormsAuthentication.FormsCookieName; FormsAuthentication类说明: // 摘要: // 为 W ...

  2. Servlet 中利用阿里云包fastjson-1.2.43.jar把map转为Json并返回前端

    1.引入fastjson-1.2.43.jar 包到lib下面,下载地址链接: https://pan.baidu.com/s/1EgAOikoG4VJRJrnUw83SNA  密码: n2fr im ...

  3. iOS大神班笔记03-UIApplication

    UIApplication简介: UIApplication对象是应用程序的象征. 每一个应用程序都有自己的UIApplication对象,而且是单例. 一个iOS程序启动后创建的第一个对象就是UIA ...

  4. Agile已死, Agility长存

    注:本文系作者独立翻译,可以随意转载.如有雷同,纯属巧合.原文地址:http://pragdave.me/blog/2014/03/04/time-to-kill-agile/ P.s. 第一次自己翻 ...

  5. 转:Android Log

    在调试代码的时候我们需要查看调试信息,那我们就需要用Android Log类. android.util.Log常用的方法有以下5个:Log.v() Log.d() Log.i() Log.w() 以 ...

  6. Maven入门指南(一)—— Maven下载与安装

    Maven下载与安装 1.下载1)Maven的系统要求: Maven对内存和操作系统没有要求 Maven安装本身仅需大约10MB,本地仓库视使用情况有所不同 Maven3.3及以上版本需要JDK1.7 ...

  7. 接下来打算写一下visual stuido 2013使用git进行远端管理。

    虽然我有了vs的账号,也vs2013开始已经可以进行远端的账户管理了,可是vs的版控毕竟有些依赖vs,想想还是用git吧 今天把这个环境的整套都弄地基本熟了.记录一下,算是一个小结.开始搭建系统框架

  8. 俄罗斯方块(NOIP模拟赛)(水·模拟)

    真是一道神奇的题目233~ 原题传送门 迫不得已贴了个题解的链接.. 好吧,这道题就是分情况讨论,纯模拟,, 没有什么难的.. 脑洞要大,四面都要考虑,不能漏! #include<iostrea ...

  9. Generator函数的语法

    简介 Generator函数是ES6关于异步编程的解决方案.Generator函数能够让函数暂停执行(即交出函数的执行权),简单直白点来理解,Generator函数就是一个状态机,内部封装了多个状态( ...

  10. android hook 框架 ADBI 如何实现so函数挂钩

    上一篇 android 5 HOOK 技术研究之 ADBI 项目 02 分析了hijack.c, 这个文件编译为一个可执行程序 hijack, 该程序实现了向目标进程注入一个动态库的功能.这一篇继续研 ...