> 對應項目:`carousel`
[TOC]
# 1. 前言
也就是十分常見的輪播圖,可以自動播放,有對應指示點和標題,可以無限播放。
# 2. 實現
## 2.1 基礎ViewPager
首先定義一個布局文件:
~~~
<?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">
<androidx.viewpager.widget.ViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="200dp"
/>
<LinearLayout
android:layout_width="match_parent"
android:orientation="vertical"
android:background="#33000000"
android:gravity="center"
android:layout_alignBottom="@id/viewPager"
android:layout_height="wrap_content">
<TextView
android:id="@+id/textView_title"
android:text="@string/app_name"
android:textColor="@color/white"
android:textSize="22sp"
android:padding="4dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<LinearLayout
android:id="@+id/linearLayout_pointer"
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
</LinearLayout>
</RelativeLayout>
~~~
然后完成數據的裝載:
~~~
val imageDrawableId = listOf<Int>(
R.drawable.a,
R.drawable.b,
R.drawable.c,
R.drawable.d,
R.drawable.e
)
val imageViewList = mutableListOf<ImageView>()
fun initialization(){
for (imageId in imageDrawableId) {
val imageView = ImageView(this)
imageView.setBackgroundResource(imageId)
imageViewList.add(imageView)
}
}
~~~
調用`initialization`方法之后,就可以為`ViewPager`設置一個適配器:
~~~
inner class MyViewPagerAdapter: PagerAdapter() {
// 初始化 container->ViewPager
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val currentPosition = (position % imageViewList.size)
val view = imageViewList[currentPosition]
if(view.parent != null){
(view.parent as ViewGroup).removeView(view)
}
container.addView(view)
return view
}
// 返回圖片個數
override fun getCount(): Int {
return imageViewList.size
}
override fun isViewFromObject(view: View, `object`: Any): Boolean {
return view == `object`
}
// 移除
override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
container.removeView(`object` as View)
}
}
~~~
最后為找到的`ViewPager`示例設置適配器即可:
~~~
viewPager.adapter = MyViewPagerAdapter()
~~~
這樣可以簡單做到簡單使用。但是在文章開頭所說的無限輪播、指示器、標題等均還沒有實現。下面就繼續在其上添加功能。
## 2.2 添加指示器和標題
為了添加指示器,我們可以用`ImageView`來替代每一個指示器的點。這里在初始化方法中進行添加:
~~~
fun initialization(){
for (imageId in imageDrawableId) {
val imageView = ImageView(this)
imageView.setBackgroundResource(imageId)
imageViewList.add(imageView)
}
// 添加指示器
for(i in 0.until(imageDrawableId.size)){
val textView = ImageView(this)
textView.setBackgroundResource(R.drawable.pointer)
val layoutParams = LinearLayout.LayoutParams(20, 20)
if(i == 0){
Log.e("TAG", "isEnable=true: " )
textView.isEnabled = true
} else{
textView.isEnabled = false
layoutParams.marginStart = 8
}
textView.layoutParams = layoutParams
linearLayout_pointer.addView(textView)
}
}
~~~
對于設置的`pointer.xml`文件設置為:
~~~
// res/drawable/pointer.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="false" android:drawable="@drawable/pointer_normal"/>
<item android:state_enabled="true" android:drawable="@drawable/pointer_pressed"/>
</selector>
~~~
然后就是指定背景顏色的兩個文件:
~~~
// res/drawable/pointer_normal.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<solid
android:color="@color/gray"
/>
<size
android:width="8dp"
android:height="8dp"
/>
</shape>
~~~
至于`pointer_pressed.xml`文件就只是顏色不同,這里不再給出。
然后就可以看到靜態的指示器:

為了實現動態效果,我們這里可以使用`ViewPager`的監聽器來監聽滑動,然后對應的來改變`ImageView`的樣式:
~~~
viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener{
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) {
}
override fun onPageSelected(position: Int) {
// 這里關聯指示器的動態效果
linearLayout_pointer.getChildAt(position).isEnabled = true
// 之前的設置為灰色
linearLayout_pointer.getChildAt(currentPointerIndex).isEnabled = false
// 更新當前下標
currentPointerIndex = position
// 更新標題
textView_title.setText(textViewTitle[position])
}
override fun onPageScrollStateChanged(state: Int) {
}
})
~~~
設置后就完成關聯指示器和標題了。現在的任務就是最后一個,也就是無限輪播功能。
## 2.3 添加無限輪播功能
可以將任務拆解為兩個部分,一個為無限循環,一個為自動輪播。首先我們需要解決無限循環問題。
### 2.3.1 無限循環
對于我們這里的五張圖片,我們期望在第一張的前面可以滑動到最后一張;同時,對于最后一張圖片期望可以滑動到第一張。對于指示器,由于是我們自己所控制的,很容易就能做到這一點。這里的難點就在于`ViewPager`這個控件。不妨來做一個簡單的測試,首先在布局文件下面添加一個按鈕:
~~~
<Button
android:id="@+id/btn"
android:layout_below="@id/t"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="按鈕"
android:layout_marginTop="10dp"
android:layout_centerHorizontal="true"
/>
~~~
然后設置點擊事件:
~~~
findViewById<Button>(R.id.btn).setOnClickListener {
viewPager.currentItem += 1
}
~~~
就可以發現當我們點擊一次按鈕,`ViewPager`就自動移動到下一張圖片。但是到最后一張圖片的時候不再移動。所以這里我們如果重新測試:
~~~
findViewById<Button>(R.id.btn).setOnClickListener {
var temp = viewPager.currentItem + 1
temp %= imageViewList.size
viewPager.currentItem = temp
}
~~~
這樣雖然可以實現從最后一張圖片到第一張圖片,但是中間所產生的動畫著實不美觀。

也就是這里我們需要一個更加平滑的過渡效果。在網上有兩種做法:
(1)采用`Adapter`內的`getCount()`方法返回`Integer.MAX_VALUE`。
(2)在列表的最前面插入最后一條數據,在列表末尾插入第一個數據,造成循環的假象。
#### 第一種方法實現無限滑動
~~~
class MainActivity : AppCompatActivity() {
val viewPager by lazy { findViewById<ViewPager>(R.id.viewPager) }
val textView_title by lazy { findViewById<TextView>(R.id.textView_title) }
val linearLayout_pointer by lazy { findViewById<LinearLayout>(R.id.linearLayout_pointer) }
val imageDrawableId = listOf<Int>(
R.drawable.a,
R.drawable.b,
R.drawable.c,
R.drawable.d,
R.drawable.e
)
val textViewTitle = listOf<String>(
"尚硅谷波河爭霸賽!",
"凝聚你我,放飛夢想!",
"抱歉沒座位了!",
"7月就業名單全部曝光!",
"平均起薪11345元"
)
var currentPointerIndex = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 為recyclerView添加圖片資源
initialization()
currentPointerIndex = Int.MAX_VALUE/2 - Int.MAX_VALUE/2%(imageViewList.size)
// 設置適配器
viewPager.adapter = MyViewPagerAdapter()
// 設置從第一個開始
viewPager.currentItem = currentPointerIndex
viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) {
}
override fun onPageSelected(position: Int) {
// 這里需要對應更新指示器和標題的下標
updatePointer(position)
}
override fun onPageScrollStateChanged(state: Int) {
}
})
}
fun updatePointer(position: Int) {
Log.e("TAG", "updatePointer: ${position}" )
// 這里關聯指示器的動態效果
linearLayout_pointer.getChildAt(position%(imageViewList.size)).isEnabled = true
// 之前的設置為灰色
linearLayout_pointer.getChildAt(currentPointerIndex%(imageViewList.size)).isEnabled = false
// 更新標題
textView_title.setText(textViewTitle[currentPointerIndex%(imageViewList.size)])
// 更新當前下標
currentPointerIndex = position
}
val imageViewList = mutableListOf<ImageView>()
fun initialization() {
// DABCDEA
for (imageId in imageDrawableId) {
val imageView = ImageView(this)
imageView.setBackgroundResource(imageId)
imageViewList.add(imageView)
}
// 添加指示器
for (i in 0.until(imageDrawableId.size)) {
val imageView = ImageView(this)
imageView.setBackgroundResource(R.drawable.pointer)
val layoutParams = LinearLayout.LayoutParams(20, 20)
if (i == 0) {
imageView.isEnabled = true
} else {
imageView.isEnabled = false
layoutParams.marginStart = 8
}
imageView.layoutParams = layoutParams
linearLayout_pointer.addView(imageView)
}
// 設置標題
textView_title.setText(textViewTitle[currentPointerIndex%(imageViewList.size)])
}
inner class MyViewPagerAdapter : PagerAdapter() {
// 初始化 container->ViewPager
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val view = imageViewList[position%(imageViewList.size)]
if (view.parent != null) {
(view.parent as ViewGroup).removeView(view)
}
container.addView(view)
return view
}
// 返回圖片個數
override fun getCount(): Int {
return Int.MAX_VALUE
}
override fun isViewFromObject(view: View, `object`: Any): Boolean {
return view == `object`
}
// 移除
override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
container.removeView(`object` as View)
}
}
}
~~~
這里需要注意的是,所有的`position`或者`currentPointerIndex`都需要進行取模運算。其中,設置`currentPointerIndex = Int.MAX_VALUE/2 - Int.MAX_VALUE/2%(imageViewList.size)`就是為了保證為第一個下標,即0的倍數。但是,設置這么大的數,在使用`setCurrentItem()`的時候會發生`ANR`,比如下面的測試案例:
~~~
findViewById<Button>(R.id.btn).setOnClickListener {
val temp = viewPager.currentItem + 1
// 判斷temp的位置
viewPager.currentItem = temp%(imageViewList.size)
}
~~~
所以還是設置一個適中的值比較好。
#### 第二種方法實現無限滑動
比如我們這里放置的圖片為`ABCDE`,那么可以在第一張前面放置最后一張,在最后一張后面放置第一張。也就是:`EABCDEA`這么一個列表。
* 當滑動到最后一個`A`的時候,就將其設置當前頁為下標`1`的`A`頁面。
* 當滑動到第一個`E`的時候,同理切換到最后一個`E`頁面;
至于為什么可以這么做呢,這里我們做一個簡單的示范案例,還是對上面的監聽函數做處理:
~~~
findViewById<Button>(R.id.btn).setOnClickListener {
var temp = viewPager.currentItem + 1
temp %= imageViewList.size
// 第二個參數為:boolean smoothScroll
viewPager.setCurrentItem(temp, false)
}
~~~
這里傳入`smoothScroll = false`表示不使用平滑過渡效果。也就是對于整體而言均為:

正是因為我們可以設置沒有平滑過渡效果,所以這里才可以在前后插入,然后替換。
~~~
class MainActivity : AppCompatActivity() {
val viewPager by lazy { findViewById<ViewPager>(R.id.viewPager) }
val imageDrawableId = listOf<Int>(
R.drawable.e,
R.drawable.a,
R.drawable.b,
R.drawable.c,
R.drawable.d,
R.drawable.e,
R.drawable.a
)
var currentPointerIndex = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 為recyclerView添加圖片資源
initialization()
// 設置適配器
viewPager.adapter = MyViewPagerAdapter()
// 設置從第一個開始
viewPager.currentItem = currentPointerIndex
viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) {
}
override fun onPageSelected(position: Int) {
currentPointerIndex = position
}
override fun onPageScrollStateChanged(state: Int) {
//驗證當前的滑動是否結束
if (state == ViewPager.SCROLL_STATE_IDLE) {
if (currentPointerIndex == 0) {
viewPager.setCurrentItem(imageViewList.size - 2, false);//切換,不要動畫效果
} else if (currentPointerIndex == imageViewList.size - 1) {
viewPager.setCurrentItem(1, false);//切換,不要動畫效果
}
}
}
})
}
val imageViewList = mutableListOf<ImageView>()
fun initialization() {
// EABCDEA
for (imageId in imageDrawableId) {
val imageView = ImageView(this)
imageView.setBackgroundResource(imageId)
imageViewList.add(imageView)
}
}
inner class MyViewPagerAdapter : PagerAdapter() {
// 初始化 container->ViewPager
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val view = imageViewList[position]
if (view.parent != null) {
(view.parent as ViewGroup).removeView(view)
}
container.addView(view)
return view
}
// 返回圖片個數
override fun getCount(): Int {
return imageViewList.size
}
override fun isViewFromObject(view: View, `object`: Any): Boolean {
return view == `object`
}
// 移除
override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
container.removeView(`object` as View)
}
}
}
~~~
為了簡化一下,這里將指示器和文字都去掉了。
### 2.3.2 自動輪播
前面我們知道指定`viewPager.currentItem`屬性可以做到切換,所以這里只需要隔一段事件就調用一下這個方法就可以實現自動輪播。那么就可以使用`Handler`來延遲發送一條消息。
~~~
handler = object: Handler(mainLooper){
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
if(msg.what == 0){
// 設置下一頁
val item = (viewPager.currentItem + 1) % imageViewList.size
viewPager.currentItem = item
// 延遲發送消息,一直回調自己
sendEmptyMessageDelayed(0, 4000)
}
}
}
~~~
然后調用啟動這個`Handler`:
~~~
// 啟動自動Handler
handler!!.sendEmptyMessage(0)
~~~
就可以做到自動輪播的效果。接下來需要完成點擊某張圖片后暫停當前的輪播。
### 2.3.3 暫停輪播
需要暫停輪播就需要對用戶的觸摸事件進行處理。所以這里可以為每個`ImageView`注冊觸摸事件:
~~~
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val view = imageViewList[position]
// 注冊觸摸事件
view.setOnTouchListener(object : View.OnTouchListener {
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
when(event?.action){
MotionEvent.ACTION_DOWN -> { // 按下
// 按下需要暫停,所以需要清空消息隊列中的所有消息
handler?.removeCallbacksAndMessages(null) // 傳入null,表示清空所有消息
}
MotionEvent.ACTION_UP -> { // 離開
// 離開,也就是再次發送一個延遲的消息到消息隊列中
handler?.removeCallbacksAndMessages(null) // 清空可能存在的消息
handler?.sendEmptyMessageDelayed(0, 4000)
}
}
return true; // 表示消費掉事件
}
})
if (view.parent != null) {
(view.parent as ViewGroup).removeView(view)
}
container.addView(view)
return view
}
~~~
這里雖然可以實現按下清空消息也就是暫停輪播,釋放重新開始輪播。但是沒有處理用戶滑動/拖拽事件,也就是說在用戶拖拽的時候,由于我們在按下后沒有做處理,所以這里會有`bug`。也即是:拖拽后再釋放就沒有輪播的效果了。
為了解決這個問題,這里可以監聽`ViewPager`的三種狀態,在`ViewPager`的三種狀態分別為:
* `ViewPager.SCROLL_STATE_DRAGGING` : 當用戶按下`ViewPager`視圖并且需要滑動第一下時;
* ` ViewPager.SCROLL_STATE_SETTLING`: 當用戶滑動的放手讓其慣性滑動的時候,比如滑了放手觸發。如果用戶滑了左手邊一點然后不松手滑回原點將不會觸發
* `ViewPager.SCROLL_STATE_IDLE` : 當用戶滑動的時候松手
所以為了實現用戶拖拽后還可以繼續,這里需要對拖拽進行處理。在用戶拖拽的時候,清空消息隊列,在用戶松手的時候繼續發送消息。即:
~~~
// 為viewpager注冊一個監聽器,用來監聽其狀態發生改變的時候
viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener{
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) {
}
override fun onPageSelected(position: Int) {
}
// viewpager狀態發生改變
override fun onPageScrollStateChanged(state: Int) {
when(state){
ViewPager.SCROLL_STATE_DRAGGING -> { // 開始拖拽的時候
handler?.removeCallbacksAndMessages(null)
}
ViewPager.SCROLL_STATE_IDLE -> { // 滑動釋放的時候
handler?.removeCallbacksAndMessages(null)
handler?.sendEmptyMessageDelayed(0, 4000)
}
}
}
})
~~~
## 2.4 完整版
~~~
/**
* 完整版,包括指示器和標題,自動輪播,點擊暫停等
*/
class MainActivity : AppCompatActivity() {
val viewPager by lazy { findViewById<ViewPager>(R.id.other_viewpager) }
val other_linearlayout by lazy { findViewById<LinearLayout>(R.id.other_linearlayout) }
val other_textview by lazy { findViewById<TextView>(R.id.other_textview) }
val imageDrawableId = listOf<Int>(
R.drawable.e,
R.drawable.a,
R.drawable.b,
R.drawable.c,
R.drawable.d,
R.drawable.e,
R.drawable.a
)
var currentPointerIndex = 0
var handler: Handler? = null
/**
* 因為采用圖片前后加一個,所以實際上和指示器的對應關系需要計算
*/
fun mappingImageIndexToLogisticIndex(index: Int): Int{
if(index > 0 && index < imageViewList.size - 1){
return index - 1
} else if( index == 0) {
return imageViewList.size - 3
} else {
return 0
}
}
/**
* toNext: 表示是否是用戶手動觸發的滾動
* right: 表示滑動的方向是否是向右
*/
fun updateInfo(toNext: Boolean, right: Boolean){
// 計算下一頁下標
var item = 0
item = if(right) (currentPointerIndex + 1) % imageViewList.size
else if (currentPointerIndex - 1 < 0) imageViewList.size - 3 else ((currentPointerIndex - 1) % imageViewList.size)
if(toNext) viewPager.currentItem = item
// 切換指示器
for(i in 0.until(textViewTitle.size)){
other_linearlayout.getChildAt(i)?.isEnabled = false
}
other_linearlayout.getChildAt(mappingImageIndexToLogisticIndex(item))?.isEnabled = true
// 設置標題
other_textview.text = textViewTitle[mappingImageIndexToLogisticIndex(item)]
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_other)
// 為recyclerView添加圖片資源
initialization()
handler = object: Handler(mainLooper){
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
if(msg.what == 0){
updateInfo(true, true)
// 延遲發送消息,一直回調自己
sendEmptyMessageDelayed(0, 4000)
}
}
}
// 設置適配器
viewPager.adapter = MyViewPagerAdapter()
// 設置從第一個開始
viewPager.currentItem = currentPointerIndex
viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
var currentPosition = 0
var left_direction = false
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) {
left_direction = position <= currentPosition
currentPosition = position
}
override fun onPageSelected(position: Int) {
currentPointerIndex = position
// 更新一下指示器和標題
updateInfo(false, !left_direction)
}
override fun onPageScrollStateChanged(state: Int) {
//驗證當前的滑動是否結束
if (state == ViewPager.SCROLL_STATE_IDLE) {
if (currentPointerIndex == 0) {
viewPager.setCurrentItem(imageViewList.size - 2, false);//切換,不要動畫效果
} else if (currentPointerIndex == imageViewList.size - 1) {
viewPager.setCurrentItem(1, false);//切換,不要動畫效果
}
}
}
})
// 為viewpager注冊一個監聽器,用來監聽其狀態發生改變的時候
viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener{
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) {
}
override fun onPageSelected(position: Int) {
}
// viewpager狀態發生改變
override fun onPageScrollStateChanged(state: Int) {
when(state){
ViewPager.SCROLL_STATE_DRAGGING -> { // 開始拖拽的時候
handler?.removeCallbacksAndMessages(null)
}
ViewPager.SCROLL_STATE_IDLE -> { // 滑動釋放的時候
handler?.removeCallbacksAndMessages(null)
handler?.sendEmptyMessageDelayed(0, 4000)
}
}
}
})
// 啟動自動Handler
handler!!.sendEmptyMessage(0)
}
val textViewTitle = listOf<String>(
"標題1!",
"標題2!",
"標題3!",
"標題4!",
"標題5",
)
val imageViewList = mutableListOf<ImageView>()
fun initialization() {
// EABCDEA
for (imageId in imageDrawableId) {
val imageView = ImageView(this)
imageView.setBackgroundResource(imageId)
imageViewList.add(imageView)
}
// 初始化指示器
for(i in 0.until(textViewTitle.size)){
val imageView = ImageView(this)
imageView.setBackgroundResource(R.drawable.pointer)
// 因為外層為LinearLayout,所以這里為LinearLayout
val layoutParams = LinearLayout.LayoutParams(20, 20)
if(i == 0){
imageView.isEnabled = true
} else{
imageView.isEnabled = false
layoutParams.marginStart = 8
}
imageView.layoutParams = layoutParams
other_linearlayout.addView(imageView)
}
// 設置標題為第1個
other_textview.text = textViewTitle[0]
}
inner class MyViewPagerAdapter : PagerAdapter() {
// 初始化 container->ViewPager
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val view = imageViewList[position]
// 注冊觸摸事件
view.setOnTouchListener(object : View.OnTouchListener {
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
when(event?.action){
MotionEvent.ACTION_DOWN -> { // 按下
// 按下需要暫停,所以需要清空消息隊列中的所有消息
handler?.removeCallbacksAndMessages(null) // 傳入null,表示清空所有消息
}
MotionEvent.ACTION_UP -> { // 離開
// 離開,也就是再次發送一個延遲的消息到消息隊列中
handler?.removeCallbacksAndMessages(null) // 清空可能存在的消息
handler?.sendEmptyMessageDelayed(0, 4000)
}
}
return true; // 表示消費掉事件
}
})
if (view.parent != null) {
(view.parent as ViewGroup).removeView(view)
}
container.addView(view)
return view
}
// 返回圖片個數
override fun getCount(): Int {
return imageViewList.size
}
override fun isViewFromObject(view: View, `object`: Any): Boolean {
return view == `object`
}
// 移除
override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
container.removeView(`object` as View)
}
}
}
~~~
- 介紹
- 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特效