# 1. 图片资源:
![](https://img.kancloud.cn/41/2b/412ba42096c55f3fb9d4633d8963cb48_114x66.png)
![](https://img.kancloud.cn/0f/a0/0fa0375e59f07ddd5536f52ca1ccc91e_180x66.png)
# 2. 简单尝试
由于比较简单,所以这里就直接开始。
首先需要将两个图片给相对位置布局放置好,还是放置在xml文件中:
~~~
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<ImageView
android:id="@+id/btn_b"
android:background="@drawable/switch_background"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!" />
<ImageView
android:id="@+id/btn_f"
android:background="@drawable/slide_button"
android:layout_width="wrap_content"
android:layout_alignBottom="@id/btn_b"
android:layout_height="wrap_content"
android:text="Hello World!" />
</RelativeLayout>
~~~
然后可以看见预览:
![](https://img.kancloud.cn/d2/d3/d2d38445453272704df24a628c73f80f_162x71.png)
接着,我们需要在代码中进行简单切换处理:
~~~
fun calcMargin(): Int{
return btn_b.width - btn_f.width
}
~~~
~~~
// 为背景层设置点击事件
btn_b.setOnClickListener(object : View.OnClickListener {
override fun onClick(v: View?) {
val layoutParams = RelativeLayout.LayoutParams(btn_f.width, btn_f.height)
if (isOpen) {
layoutParams.marginStart = 0
} else {
layoutParams.marginStart = calcMargin()
}
isOpen = !isOpen
btn_f.layoutParams = layoutParams
}
})
~~~
然后就可以实现点击切换。但是有时候用户也会使用拖拽,所以这里还需要简单处理一下拖拽的效果。但是这种处理还会随着外层布局的改变而改变,也就是每次我们都需要自己去处理控件的边距,或许要做到动态效果还需要进行添加动画。比如下面的简单处理:
~~~
class MainActivity : AppCompatActivity() {
val gb by lazy { findViewById<ImageView>(R.id.btn_b)}
val qg by lazy { findViewById<ImageView>(R.id.btn_f)}
var startX = 0f
var marginLeftValue = 0f
var margin = 0f
var isOpen = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_other)
qg.setOnTouchListener(object : View.OnTouchListener {
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
margin = (gb.width - qg.width).toFloat()
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
// 记录起始位置
startX = event.x
}
MotionEvent.ACTION_MOVE -> {
// 计算逻辑位置
val offset = event.x - startX
marginLeftValue += offset
// 屏蔽边界
if (marginLeftValue < 0) {
marginLeftValue = 0f
} else if (marginLeftValue > margin) {
marginLeftValue = margin
}
refresh()
}
}
return true
}
})
}
fun refresh(){
val parms = RelativeLayout.LayoutParams(qg.width, qg.height)
parms.marginStart = marginLeftValue.toInt()
qg.layoutParams = parms
}
}
~~~
但是这不是我们所期望的,这里将其封装一下。以方便调用。
# 3. 回顾
就需要自定View,当然对于自定义的属性这里再次复习一下:
* 定义一个继承自View的类,并复写对应的构造方法;
* 在res/values/attrs.xml文件中定义需要用的属性;
* 在主布局文件中使用;
* 在自定义的View类中写自己需要的一些操作功能;
当然,这里简单复写一下,就以测试为主。比如首先定义对应的attrs.xml文件:
~~~
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MyButton">
<attr name="textInfo" format="string"/>
<attr name="textSize" format="integer"/>
<attr name="background" format="reference|color"/>
</declare-styleable>
</resources>
~~~
对应的在主布局文件中使用:
~~~
<com.weizu.switchbutton.MyButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:textInfo="Hello!"
app:textSize="32"
app:textBg="#FF00FF"
/>
~~~
然后我们再看下自定义的View,这里由于我只需要xml方式,所以这里就不需要其余的构造函数,只需要一个即可。这里首先来看看使用命名空间方式获取配置的属性值:
## 3.1 命名空间方式获取属性值
~~~
class MyButton: View{
// 读取xml中配置的属性
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs){
// 方法一,直接获取字符串类型数据
val textInfo =
attrs?.getAttributeValue("http://schemas.android.com/apk/res-auto", "textInfo")
val textSize =
attrs?.getAttributeValue("http://schemas.android.com/apk/res-auto", "textSize")
val background =
attrs?.getAttributeValue("http://schemas.android.com/apk/res-auto", "textBg")
Log.e("TAG", "textInfo: ${textInfo}, textSize: ${textSize}, textBg: ${background}")
}
}
~~~
运行后可以看见日志信息:
> textInfo: Hello!, textSize: 32, textBg: #ffff00ff
值得注意的是,对于背景资源这里是引用,但是这种按照命名空间取出值的方式得到的还是一个字符串,比如如果我们传入的是一个资源ID,再次测试一下,修改下主布局文件:
~~~
<com.weizu.switchbutton.MyButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:textInfo="Hello!"
app:textSize="32"
app:textBg="@drawable/slide_button"
/>
~~~
然后再次运行:
> textInfo: Hello!, textSize: 32, textBg: @2131165328
从上面结果可以看出对于引用类型资源,就不适用了,因为返回的也只是一个资源ID,没多大意义。
## 3.2 TypedArray
~~~
class MyButton: View{
// 读取xml中配置的属性
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
// 方法二,TypedArray
val typedArray = context?.obtainStyledAttributes(attrs, R.styleable.MyButton)
for (i in 0..typedArray!!.indexCount) {
val index = typedArray.getIndex(i)
when (index) {
R.styleable.MyButton_textInfo -> {
Log.e("TAG", "MyButton_textInfo: ${typedArray.getText(index)}")
}
R.styleable.MyButton_textSize -> {
Log.e("TAG", "MyButton_textInfo: ${typedArray.getInt(index, 0)}")
}
R.styleable.MyButton_textBg -> {
Log.e("TAG", "MyButton_textInfo: ${typedArray.getDrawable(index)}")
}
}
}
}
}
~~~
当然需要回收一下资源:
~~~cpp
//释放TypedArray
typedArray.recycle();
~~~
# 4. 封装
回到正题,这里简单分析可以知道我们需要定义的只有三个属性:
* 前景图层资源
* 背景图层资源
* 滑块拖动时间
## 4.1 预备工作
~~~
res/values/attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MyButton">
<attr name="bgImg" format="reference"/>
<attr name="qgImg" format="reference"/>
<attr name="DTime" format="integer"/>
</declare-styleable>
</resources>
~~~
~~~
main_activity.xml
<com.weizu.switchbutton.MyButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:bgImg="@drawable/switch_background"
app:qgImg="@drawable/slide_button"
app:DTime="500"
/>
~~~
然后我们就可以简单的让其绘制出来:
~~~
class MyButton(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
lateinit var mQg: Bitmap
lateinit var mBg: Bitmap
lateinit var paint: Paint
var mDTime: Int = 0
var margin: Int = 0
init {
// 方法二,TypedArray// 读取xml中配置的属性
val typedArray = context?.obtainStyledAttributes(attrs, R.styleable.MyButton)
for (i in 0..typedArray!!.indexCount) {
val index = typedArray.getIndex(i)
when (index) {
R.styleable.MyButton_qgImg -> {
mQg = (typedArray.getDrawable(index)!! as BitmapDrawable).bitmap
}
R.styleable.MyButton_bgImg -> {
mBg = (typedArray.getDrawable(index)!! as BitmapDrawable).bitmap
}
R.styleable.MyButton_DTime -> {
mDTime = typedArray.getInt(index, mDTime)
}
}
}
// 计算距离
margin = mBg.width - mQg.width
// 初始化画笔
paint = Paint()
paint.isAntiAlias = true
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// 设置宽度为背景图片的宽度
setMeasuredDimension(mBg.width, mBg.height)
}
override fun onDraw(canvas: Canvas?) {
// 绘制前景和背景
canvas?.drawBitmap(mBg, 0f, 0f, paint)
canvas?.drawBitmap(mQg, 0f, 0f, paint)
}
}
~~~
效果:
![](https://img.kancloud.cn/36/7a/367a141134998f152eb170d825ca7052_227x70.png)
然后就是为其添加动态的交互效果。
## 4.2 添加点击
类似的,我们可以很容易的为之添加点击事件,因为这里是整个view,所以我们可以在其中直接设置监听即可。首先定义两个状态:
~~~
// 开关状态
var isOpen = false
// 前景层距离左边界的值
var marginLeftValue: Float
~~~
然后修改onDraw:
~~~
override fun onDraw(canvas: Canvas?) {
// 绘制前景和背景
canvas?.drawBitmap(mBg, 0f, 0f, paint)
canvas?.drawBitmap(mQg, marginLeftValue, 0f, paint)
}
~~~
然后设置一下监听:
~~~
// 设置点击监听
setOnClickListener(object : OnClickListener{
override fun onClick(v: View?) {
if(isOpen){
// 设置边距为最大,然后重新绘制
marginLeftValue = margin.toFloat()
} else{
// 设置边距为0,然后重新绘制
marginLeftValue = 0f
}
isOpen = !isOpen
postInvalidate()
}
})
~~~
就可以做到按钮的简单开关切换。
## 4.3 添加拖拽事件
对于拖拽事件,我们需要使用到触摸事件,所以这里需要重写一下onTouchEvent方法,然后处理的简单逻辑为:
* 按下记录位置,然后根据移动的下一个点位置来判断移动方向;
* 根据手指移动距离来计相应的移动前景照片;
* 设置有效移动范围;
* 判断开关状态,然后重新绘制;
~~~
// 触摸事件
override fun onTouchEvent(event: MotionEvent?): Boolean {
super.onTouchEvent(event)
when(event?.action){
MotionEvent.ACTION_DOWN -> {
// 手指按下,记录起始值
currentX = event.x
}
MotionEvent.ACTION_MOVE -> {
// 手指移动,计算偏移量
val endX = event.x
val offset = endX - currentX
// 对应逻辑上移动的距离
marginLeftValue += offset
// 但是要屏蔽非法值
if(marginLeftValue < 0){
marginLeftValue = 0f
} else if(marginLeftValue > margin){
marginLeftValue = margin.toFloat()
}
// 请求重新绘制
postInvalidate()
// 更新
currentX = endX
}
MotionEvent.ACTION_UP -> {
// 手指离开屏幕,判断当前状态
isOpen = marginLeftValue > margin / 2
if(isOpen){
// 设置边距为最大,然后重新绘制
marginLeftValue = margin.toFloat()
} else{
// 设置边距为0,然后重新绘制
marginLeftValue = 0f
}
postInvalidate()
}
}
return true; // 表示事件已经处理
}
~~~
然后就可以实现这个效果。至于拖动时间这里感觉没有这个必要。
# 5. 完整代码:
~~~
class MyButton(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
lateinit var mQg: Bitmap
lateinit var mBg: Bitmap
var paint: Paint
var mDTime: Int = 0
var margin: Int = 0
// 开关状态
var isOpen = false
// 前景层距离左边界的值
var marginLeftValue: Float
init {
// 初始化边距值为0
marginLeftValue = 0f
// 方法二,TypedArray// 读取xml中配置的属性
val typedArray = context?.obtainStyledAttributes(attrs, R.styleable.MyButton)
for (i in 0..typedArray!!.indexCount) {
val index = typedArray.getIndex(i)
when (index) {
R.styleable.MyButton_qgImg -> {
mQg = (typedArray.getDrawable(index)!! as BitmapDrawable).bitmap
}
R.styleable.MyButton_bgImg -> {
mBg = (typedArray.getDrawable(index)!! as BitmapDrawable).bitmap
}
R.styleable.MyButton_DTime -> {
mDTime = typedArray.getInt(index, mDTime)
}
}
}
// 计算距离
margin = mBg.width - mQg.width
// 初始化画笔
paint = Paint()
paint.isAntiAlias = true
// 设置点击监听
setOnClickListener(object : OnClickListener{
override fun onClick(v: View?) {
if(isOpen){
// 设置边距为最大,然后重新绘制
marginLeftValue = margin.toFloat()
} else{
// 设置边距为0,然后重新绘制
marginLeftValue = 0f
}
isOpen = !isOpen
postInvalidate()
}
})
}
var currentX = 0f
// 触摸事件
override fun onTouchEvent(event: MotionEvent?): Boolean {
super.onTouchEvent(event)
when(event?.action){
MotionEvent.ACTION_DOWN -> {
// 手指按下,记录起始值
currentX = event.x
}
MotionEvent.ACTION_MOVE -> {
// 手指移动,计算偏移量
val endX = event.x
val offset = endX - currentX
// 对应逻辑上移动的距离
marginLeftValue += offset
// 但是要屏蔽非法值
if(marginLeftValue < 0){
marginLeftValue = 0f
} else if(marginLeftValue > margin){
marginLeftValue = margin.toFloat()
}
// 请求重新绘制
postInvalidate()
// 更新
currentX = endX
}
MotionEvent.ACTION_UP -> {
// 手指离开屏幕,判断当前状态
isOpen = marginLeftValue > margin / 2
if(isOpen){
// 设置边距为最大,然后重新绘制
marginLeftValue = margin.toFloat()
} else{
// 设置边距为0,然后重新绘制
marginLeftValue = 0f
}
postInvalidate()
}
}
return true; // 表示事件已经处理
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// 设置宽度为背景图片的宽度
setMeasuredDimension(mBg.width, mBg.height)
}
override fun onDraw(canvas: Canvas?) {
// 绘制前景和背景
canvas?.drawBitmap(mBg, 0f, 0f, paint)
canvas?.drawBitmap(mQg, marginLeftValue, 0f, paint)
}
}
~~~
- 介绍
- UI
- MaterialButton
- MaterialButtonToggleGroup
- 字体相关设置
- Material Design
- Toolbar
- 下拉刷新
- 可折叠式标题栏
- 悬浮按钮
- 滑动菜单DrawerLayout
- NavigationView
- 可交互提示
- CoordinatorLayout
- 卡片式布局
- 搜索框SearchView
- 自定义View
- 简单封装单选
- RecyclerView
- xml设置点击样式
- adb
- 连接真机
- 小技巧
- 通过字符串ID获取资源
- 自定义View组件
- 使用系统控件重新组合
- 旋转菜单
- 轮播图
- 下拉输入框
- 自定义VIew
- 图片组合的开关按钮
- 自定义ViewPager
- 联系人快速索引案例
- 使用ListView定义侧滑菜单
- 下拉粘黏效果
- 滑动冲突
- 滑动冲突之非同向冲突
- onMeasure
- 绘制字体
- 设置画笔Paint
- 贝赛尔曲线
- Invalidate和PostInvalidate
- super.onTouchEvent(event)?
- setShadowLayer与阴影效果
- Shader
- ImageView的scaleType属性
- 渐变
- LinearGradient
- 图像混合模式
- PorterDuffXfermode
- 橡皮擦效果
- Matrix
- 离屏绘制
- Canvas和图层
- Canvas简介
- Canvas中常用操作总结
- Shape
- 圆角属性
- Android常见动画
- Android动画简介
- View动画
- 自定义View动画
- View动画的特殊使用场景
- LayoutAnimation
- Activity的切换转场效果
- 属性动画
- 帧动画
- 属性动画监听
- 插值器和估值器
- 工具
- dp和px的转换
- 获取屏幕宽高
- JNI
- javah命令
- C和Java相互调用
- WebView
- Android Studio快捷键
- Bitmap和Drawable图像
- Bitmap简要介绍
- 图片缩放和裁剪效果
- 创建指定颜色的Bitmap图像
- Gradle本地仓库
- Gradle小技巧
- RxJava+Okhttp+Retrofit构建网络模块
- 服务器相关配置
- node环境配置
- 3D特效