# 1. 圖片資源:


# 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>
~~~
然后可以看見預覽:

接著,我們需要在代碼中進行簡單切換處理:
~~~
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)
}
}
~~~
效果:

然后就是為其添加動態的交互效果。
## 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特效