Android里已经有足够多的控件供开发者使用,但有时候我们还是会想要一些不一样的东西,比如一些UI特效,比如一些3D动画,今天就讲讲比较basic的东西:自定义控件。

1.效果图

如果项目里需要一个通用的控件,然后UI给你这样一个效果图,你接下来会打算怎么做?

  用户可以按住拖动

点击要切换的状态,然后自动滑动到那一端

(本来是没有这个效果图的,又不想一张张贴不同的状态,就画了一下这个gif图,关于怎么在ubuntu下画gif图,可以看一下下面这篇)

程序媛也会画图 之 在ubuntu下用GIMP制作gif

2.分析

看一下有没有现成的widget,这似乎和android.widget.Switch有点类似,可是Swithc是水平的,水平没有关系,改成垂直的问题不大,先来尝试下好了,就先把背景和button的图片换一下,来看一下结果是怎样:

额。。。这个切换似乎生硬了点,没有渐变的动画。好吧,那还是重新自己写一个控件吧。

3.创建Andriod自定义控件的步骤

怎么建立一个自定义的控件,说起来并不难,有三个内容需要实现:

3.1新建一个控件类,继承android.view.View类:

 public class XXXView extends View {
...
protected void onDraw(Canvas canvas) {
...
} public boolean onTouchEvent(MotionEvent event) {
...
} public interface OnXXXListener { //状态回调,同View.OnClickListener
public abstract void xxx();
public abstract void xxx();
}
}

3.2 在布局文件xml里使用这个控件:

<com.xxx.xxx.XXXView  android:id=”@+id/xxx”
android:layoutWidth=”...”
android:layoutHeight=”...”>
</com.xxx.xxx.XXXView>

3.3 在Activity类里获得这个控件:

 mXXXView = (XXXView) findViewById(R.id.xxx);
mXXXView.setListener(mXXXViewListener);

以上这简单的3个步骤就是创建和使用控件的内容了,到这里,如果你是个喜欢着急写代码的人,你也可以先搭一个程序框架出来跑跑看啦。

4.考虑怎么画?
4.1拖动
用户需要能拖动Button,那也就是说我们在控件里需要捕获用户的touch event,知道用户到底是做了什么动作(ACTION_DOWN, ACTION_MOVE, UP), 还有操作的位置在哪里(getX(), getY()).
这些信息从哪里可以知道?--》onTouchEvent()回调!

4.2动画
动画的本质就是图片+位置+时间差。
在效果图中,用户也可以点击一个状态,让控件滑动。那这个滑动的过程就是一个动画的。
图片我们有,那怎么把图片画到Canvas上?-》在onDraw()回调里面画。在主线程里只要调用invalidate(),就会重新触发onDraw()的执行。如果我们在一定的时间间隔,在不同的位置重新画图片,不就是动画了?
位置可以从用户行为获得,或者自己计算;
时间差,在Android里面控制时间最容易的是什么?当然是Handler啦,因为它可以发送delay的消息。

4.3渐变的实现
效果图中还有个渐变的过程,这个看起来好像蛮麻烦,其实也好办。因为有Alpha的存在。我们可以在画的时候根据不同的位置,设置Paint不同的Alpha值,一个图片Alpha慢慢减小,另一个图片Alpha慢慢增大。

ok,分析到这里,就大概知道该怎么做了,在onTouchEvent()回调里,获得用户的行为和位置,并记录下来,在适当的时候发送Message给Handler,或者直接调用invalidate()重新画。在Handler里,接收到信息,就根据当前的状态,更新图片下一个应该出现的位置,然后调用invalidate()触发重新画。

5.计算位置
ok,上面已经确定以什么方式做了,接下来就要用到一点点数学的计算了。我们要确定图片从哪里开始动,动到哪里结束,还有在什么位置开始切换状态。

先切下图:

         

(文字也做成了图片,其实凡是涉及到文字的都不应该做成图片,如果有人切换到中文,然后他又不认识on off呢,而且这些文字应该要可设置的才对。这里图方便就做成图片了。)

然后就是一些重要坐标位置啦:

图1    蓝色是那个长条的图片,绿色两块是在两个状态下Button所在的位置。

图2   黄色的区域是两个小的灰色文字图片

图3   这个区域就是文字开始切换的区域

6.伪代码

现在方法也有了,数据也有了,就可以开始写代码了。
为了叙述方便,就用伪代码代替了,下面是最重要的三个部分的伪码:

处理用户行为的逻辑:

 public boolean onTouchEvent(MotionEvent event) {  //处理用户行为
case ACTION_DOWN:
if (坐标在图1中蓝色区域) { //touch在无效的区域
return;
} if (坐标在图1中绿色区域中Button在的区域) { //当前状态是on,就是上面的区域,否则,就是下面的区域
获得坐标与上边缘的距离gap;
} else {
设置正在滑动标志;
设置动画的方向,发送Message; //会执行到这里的情况是,比如当前状态是on,用户点击了off那一端,那接下来控件就要自动滑动切换到off状态。
}
break; case ACTION_MOVE:
if (上次Down是在无效区域 | 正在切换状态) { //此时不用响应Move动作。
return;
} if (根据当前的坐标计算,滑块将不在背景区域) {
return;
} if (根据当前的坐标计算,在文字交换的区域) {
设置交换标记;
}
记录滑块当前位置;
invalidate();
break; case ACTION_UP:
if (上次Down是在无效区域 | 正在切换状态) { //此时不用响应Up动作。
return;
} 取消交换标识;
if(根据当前坐标计算,最后的状态是on) {
设置滑块位置为on状态时的位置;
修改状态为on;
invalidate();
} else {
设置滑块位置为off状态时的位置;
修改状态为off;
invalidate();
}
}

处理自动滑动:

 private Handler mHandler = new Handler() {  //用于处理自动滑动那部分逻辑
public void handleMessage(Message msg) {
if (计数 > 20) {
设置当前状态;
设置滑块的位置;
取消正在滑动的标志;
计数归0;
return;
} 根据计数,获得interpolator.getInterpolation;//这里用了AccelerateDecelerateInterpolator,让动画有一个加速的效果,其实这么短的距离效果看不出来。
计算滑块的位置;
invalidate();
计数+1;
sendMessageDelayed(0, 20); //20ms后画下一帧。
} };

画:

 protected void onDraw(Canvas canvas) { //具体画的代码
画背景; if (在状态交换区域) {
根据滑块位置这是Paint的Alpha值;
用上面设置的Paint画那四个小图; //在状态交换的时候,四个小图都是显示的。
} else {
根据当前的状态,画on滑块或off滑块;
}
}

ok,有上面3部分的内容,基本上就可以了。

下面就是运行起来的效果,(不好表示啦,其实就是效果图那样的)

贴个对应的代码段:

Handler:

     private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (drawCount > 20) {
if (button_status == STATUS_OFF) {
button_status = STATUS_ON;
buttonY = buttonTopY;
if (listener != null) {
listener.slipToTop();
}
} else {
button_status = STATUS_OFF;
buttonY = buttonBottomY;
if (listener != null) {
listener.slipToBottom();
}
} isTouchDownAnotherSide = false;
drawCount = 0;
return;
} float p;
if (isToBottom) {
p = (float) (drawCount * 0.05);
} else {
p = (float) (1 - drawCount * 0.05);
}
float inter = interpolator.getInterpolation(p);
buttonY = buttonTopY + (buttonBottomY - buttonTopY) * inter; if (buttonY >= exchangeBeginY && buttonY <= exchangeEndY) {
isExchange = true;
} else {
isExchange = false;
}
invalidate();
drawCount++;
sendEmptyMessageDelayed(0, 20);
}
};

onDraw():

     @Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
canvas.drawBitmap(mBackBitmap, 0, 0, null); if (isExchange) {
// in exchange area, we should set alpha
Paint mPaint = new Paint(); int alpha = (int) (255 - 255 * (buttonY - 25.5) / 50);
mPaint.setAlpha(alpha);
canvas.drawBitmap(mONBitmap, buttonTopX, buttonY, mPaint);
canvas.drawBitmap(mOFFTextBitmap, textBottomX, textBottomY, mPaint); mPaint.setAlpha(255 - alpha);
canvas.drawBitmap(mOFFBitmap, buttonBottomX, buttonY, mPaint);
canvas.drawBitmap(mONTextBitmap, textTopX, textTopY, mPaint);
} else {
if (getNearLocation(0, buttonY) == STATUS_ON) {
canvas.drawBitmap(mONBitmap, buttonTopX, buttonY, null);
canvas.drawBitmap(mOFFTextBitmap, textBottomX, textBottomY, null);
} else {
canvas.drawBitmap(mOFFBitmap, buttonBottomX, buttonY, null);
canvas.drawBitmap(mONTextBitmap, textTopX, textTopY, null);
}
}
}

onTouchEvent():

     @Override
public boolean onTouchEvent(MotionEvent event) {
// TODO Auto-generated method stub float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: if (isTouchDownAnotherSide) {
return true;
} // check if touch right place
if (isOutOfFrontBitmap(x, y)) {
isTouchDownValid = false;
return true;
} if (listener != null) {
listener.touchedDown();
} if (isInFrontBitmap(x, y)) {
// touch in current mode
Log.e("Slip", "ACTION_DOWN : yes! infrontBitmap");
isTouchDownValid = true;
touchDownGap = getGap(x, y);
} else {
// touch anther side
Log.e("Slip", "ACTION_DOWN : no! infrontBitmap");
isTouchDownValid = false;
isTouchDownAnotherSide = true;
if (button_status == STATUS_ON) {
isToBottom = true;
mHandler.sendEmptyMessage(0);
} else {
isToBottom = false;
mHandler.sendEmptyMessage(0);
}
}
break;
case MotionEvent.ACTION_MOVE:
// if touch down wrong place, we ignore next action
if (!isTouchDownValid || isTouchDownAnotherSide) {
return true;
}
if (!isInBackBitmap(x, y)) {
Log.e("Slip", "ACTION_MOVE : no! isInBackBitmap");
return true;
}
if (isInExchangeArea(x, y)) {
isExchange = true;
} else {
isExchange = false;
}
buttonY = y - touchDownGap;
this.invalidate(); break;
case MotionEvent.ACTION_UP:
// if touch down wrong place, we ignore next action
if (!isTouchDownValid || isTouchDownAnotherSide) {
Log.e("Slip", "ACTION_UP : no! isTouchDownValid");
return true;
} isExchange = false; if (getFinalLocation(x, y) == STATUS_ON) {
buttonY = buttonTopY;
if (button_status != STATUS_ON) {
button_status = STATUS_ON;
Log.e("Slip", "ACTION_UP : STATUS_ON! getFinalLocation");
if (listener != null) {
listener.slipToTop();
}
} else {
if (listener != null) {
listener.touchedUp();
}
}
this.invalidate(); } else {
buttonY = buttonBottomY;
if (button_status != STATUS_OFF) {
button_status = STATUS_OFF;
Log.e("Slip", "ACTION_UP : STATUS_OFF! getFinalLocation");
if (listener != null) {
listener.slipToBottom();
}
} else {
if (listener != null) {
listener.touchedUp();
}
}
this.invalidate();
} break;
default:
break;
} return true;
}

Over,Thanks.

程序媛也话Android 之 自定义控件(垂直方向滑动条)的更多相关文章

  1. Android GridView 滑动条设置一直显示状态

    模拟GridView控件: <GridView android:id="@+id/picture_grid" android:layout_width="match ...

  2. 2017年"程序媛和工程狮"绝对不能忽视的编程语言、框架和工具

      2017年"程序媛和工程狮"绝对不能忽视的编程语言.框架和工具 在过去的一年里,软件开发行业继续大踏步地向前迈进.回顾 2016 年,我们看到了更多新兴的流行语言.框架和工具, ...

  3. Android中自定义控件TextSize属性问题

    本文主要说明一个自定义控件添加TextSize属性的坑,刚刚从坑里面爬出来,写个随笔,记录一下: *************************************************** ...

  4. Android RecyclerView(瀑布流)水平/垂直方向分割线

     Android RecyclerView(瀑布流)水平/垂直方向分割线 Android RecyclerView不像过去的ListView那样随意的设置水平方向的分割线,如果要实现Recycle ...

  5. .Net程序猿玩转Android开发---(3)登陆页面布局

    这一节我们来看看登陆页面如何布局.对于刚接触到Android开发的童鞋来说.Android的布局感觉比較棘手.须要结合各种属性进行设置,接下来我们由点入面来 了解安卓中页面如何布局,登陆页面非常eas ...

  6. 新年Flag,零基础程序媛编程学习计划(持续更新ing)~~

    新的一年立下了转行做程序媛的Flag,我是文科妹子,专业是做市场传销…哦不,是市场营销,算是零基础转行,目标是半年内完成自学进入公司工作,目前打算从事的方向短期目标以入行为主,以前端(可以发挥自身审美 ...

  7. 粉红色界面的vscode,程序媛的必备利器

    vscode都是黑漆漆的界面,对于一个喜欢花花草草的程序媛来说,长时间对着这样的界面,简直是一种折磨啊 有的时候,也会不自觉的想要看看一些粉色的东西,毕竟有着单纯的少女心 今天看到了一篇博客,作者是自 ...

  8. 关于Apache显示port 80 in use 无法解决的情况,这个世界对程序媛太不友好了

    学到Ajax时下载了Apache,百度的安装教程,配置文件参数分别是: 1. httpd.conf里的80改为8000或者其他的,共三处(用记事本打开,按ctrl+F找方便) 2. httpd-ssl ...

  9. Android RecyclerViewSwipeDismiss:水平、垂直方向的拖曳删除item

     Android RecyclerViewSwipeDismiss:水平.垂直方向的拖曳删除item RecyclerViewSwipeDismiss是一种支持RecyclerView的水平.垂直 ...

随机推荐

  1. Oracle安装及使用入门

    新手Oracle安装及使用入门   一.安装Oracle Step1 下载oracle压缩包并解压到同一文件夹下面 Step2 双击setup.exe进行安装 Step3:进入如下界面配置: 邮箱可不 ...

  2. Oracle使用并行索引需要注意的问题

    当索引的结构.我们要建立索引快.它将并行加,加平行后.这将平行的列索引. 当并行度索引访问,CBO你可能会考虑并行运行,这可能会导致一些问题.作为server候用并行会引起更加严重的争用.当使用并行后 ...

  3. SlidingMenu 左侧滑动菜单

    1.MainActivity package loveworld.slidingmenu; import java.util.ArrayList; import android.app.Activit ...

  4. hdoj 1226 超级password 【隐图BFS】

    称号:hdoj 1226 超级password 分析:这题属于隐式图搜索,状态不是非常明显,须要自己建立. 事实上搜索说白了就是暴力. 这个题目就是,首先对给出的能够组成的全部的数依次枚举.长度从小到 ...

  5. Session与Caching

    Session与Caching 在之前的版本中,Session存在于System.Web中,新版ASP.NET 5中由于不在依赖于System.Web.dll库了,所以相应的,Session也就成了A ...

  6. 使用myeclipse将Javaj项目标ar套餐邂逅classnotfound解决问题的方法

    做一件事的今天,该Java项目打包成jar文件.折腾2小时,最终运行jar文件报告classnotfound异常,我觉得程序写入依赖jar包不玩成,但是,我手动添加.或不.网上找了很多办法.或不.后. ...

  7. Codeforces 328B-Sheldon and Ice Pieces(馋)

    B. Sheldon and Ice Pieces time limit per test 1 second memory limit per test 256 megabytes input sta ...

  8. GitHub Top 100 简介

    主要对当前 GitHub 排名前 100 的项目做一个简单的简介, 方便初学者快速了解到当前 Objective-C 在 GitHub 的情况. GitHub 地址:https://github.co ...

  9. SVN & Git (一)

    (一)SVN的使用.CornerStone图形化管理工具! SVN是Subversion的简称,是一个开放源代码的版本控制系统,相较于RCS.CVS,它采用了分支管理系统,它的设计目标就是取代CVS. ...

  10. 常用批处理命令总结3之Find和FindStr

    原文:常用批处理命令总结3之Find和FindStr find 作用:从文件中收索字符串 格式:find 参数 "字符串" 路径\文件名 参数: /V 显示所有未包含指定字符串的行 ...