Android中FlowLayout組件實(shí)現(xiàn)瀑布流效果
紙上得來終覺淺,絕知此事要躬行。
動(dòng)手實(shí)踐是學(xué)習(xí)的最好的方式,對(duì)于自定義View來說,聽和看只能是過一遍流程,能掌握個(gè)30%、40%就不錯(cuò)了,而且很快就會(huì)遺忘,想變成自己的東西必須動(dòng)手來寫幾遍,細(xì)細(xì)體會(huì)其中的細(xì)節(jié)和系統(tǒng)API的奧秘、真諦。
進(jìn)入主題,今天來手寫一個(gè)瀑布流組件FlowLayout,溫習(xí)下自定義view的流程和關(guān)鍵點(diǎn),先來張效果圖

FlowLayout實(shí)現(xiàn)關(guān)鍵步驟:
1、創(chuàng)建一個(gè)view繼承自ViewGroup
class ZSFlowLayout : ViewGroup {
constructor(context: Context) : super(context) {}
/**
* 必須的構(gòu)造函數(shù),系統(tǒng)會(huì)通過反射來調(diào)用此構(gòu)造方法完成view的創(chuàng)建
*/
constructor(context: Context, attr: AttributeSet) : super(context, attr) {}
constructor (context: Context, attr: AttributeSet, defZStyle: Int) : super(
context,
attr,
defZStyle
) {
}
}這里注意兩個(gè)參數(shù)的構(gòu)造函數(shù)是必須的構(gòu)造函數(shù),系統(tǒng)會(huì)通過反射來調(diào)用此構(gòu)造方法完成view的創(chuàng)建,具體調(diào)用位置在LayoutInflater 的 createView方法中,如下(基于android-31):
省略了若干不相關(guān)代碼,并寫了重要的注釋信息,請(qǐng)留意
public final View createView(@NonNull Context viewContext, @NonNull String name,
@Nullable String prefix, @Nullable AttributeSet attrs)
throws ClassNotFoundException, InflateException {
Objects.requireNonNull(viewContext);
Objects.requireNonNull(name);
//從緩存中取對(duì)應(yīng)的構(gòu)造函數(shù)
Constructor<? extends View> constructor = sConstructorMap.get(name);
Class<? extends View> clazz = null;
try {
if (constructor == null) {
// 通過反射創(chuàng)建class對(duì)象
clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
mContext.getClassLoader()).asSubclass(View.class);
//創(chuàng)建構(gòu)造函數(shù) 這里的mConstructorSignature 長這個(gè)樣子
//static final Class<?>[] mConstructorSignature = new Class[] {
// Context.class, AttributeSet.class};
//看到了沒 就是我們第二個(gè)構(gòu)造方法
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
//緩存構(gòu)造方法
sConstructorMap.put(name, constructor);
} else {
...
}
try {
//執(zhí)行構(gòu)造函數(shù) 創(chuàng)建出view
final View view = constructor.newInstance(args);
...
return view;
} finally {
mConstructorArgs[0] = lastContext;
}
} catch (Exception e) {
...
} finally {
...
}
}對(duì)LayoutInflater以及setContentView、DecorView、PhoneWindow相關(guān)一整套源碼流程感興趣的可以看下我這篇文章:
Activity setContentView背后的一系列源碼分析
2、重寫并實(shí)現(xiàn)onMeasure方法
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
}(1)先了解下 MeasureSpec的含義
MeasureSpec是View中的內(nèi)部類,基本都是二進(jìn)制運(yùn)算。由于int是32位的,用高兩位表示mode,低30位表示size。
(2)重點(diǎn)解釋下 兩個(gè)參數(shù)widthMeasureSpec 和 heightMeasureSpec是怎么來的
這個(gè)是父類傳給我們的尺寸規(guī)則,那父類是如何按照什么規(guī)則生成的widthMeasureSpec、heightMeasureSpec呢?
答:父類會(huì)結(jié)合自身的情況,并且結(jié)合子view的情況(子類的寬是match_parent、wrap_content、還是寫死的值)來生成的。生成的具體邏輯 請(qǐng)見:ViewGroup的getChildMeasureSpec方法
相關(guān)說明都寫在了注釋中,請(qǐng)注意查看:
/**
* 這里的spec、padding是父類的尺寸規(guī)則,childDimension是子類的尺寸
* 舉個(gè)例子,如果我們寫的FlowLayout被LinearLayout包裹,那這里spec、padding就是LinearLayout的
* spec 可以是widthMeasureSpec 也可以是 heightMeasureSpec 寬和高是分開計(jì)算的,childDimension
* 則是我們?cè)诓季治募袑?duì)FlowLayout設(shè)置的對(duì)應(yīng)的寬、高
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//獲取父類的尺寸模式
int specMode = MeasureSpec.getMode(spec);
//獲取父類的尺寸大小
int specSize = MeasureSpec.getSize(spec);
//去掉padding后的大小 最小不能低于0
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// 如果父類的模式是MeasureSpec.EXACTLY(精確模式,父類的值是可以確定的)
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
//此時(shí)子view的大小就是我們?cè)O(shè)置的值,超過父類也沒事,開發(fā)人員自定義設(shè)置的
//比如父view的寬是100dp,子view寬你非要設(shè)置200dp,那就給200dp,這么做有什么
//意義?這樣是可以擴(kuò)展的,不至于限制死,比如子view可能具有滾動(dòng)屬性或者其他高級(jí)
//玩法
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// MATCH_PARENT 則子view和父view大小一致 模式是確定的
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// WRAP_CONTENT 則子view和父view大小一致 模式是最大不超過這個(gè)值
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// 按子view值執(zhí)行,確定模式
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//按父view值執(zhí)行 模式是最多不超過指定值模式
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//按父view值執(zhí)行 模式是最多不超過指定值模式
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// 按子view值執(zhí)行,確定模式
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 按父view值執(zhí)行 模式是未定義
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 按父view值執(zhí)行 模式是未定義
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}其實(shí)就是網(wǎng)上的這張圖

3、重寫并實(shí)現(xiàn)onLayout方法
我們要在這個(gè)方法里面,確定所有被添加到我們的FlowLayout里面的子view的位置,這里沒有特殊要注意的地方,控制好細(xì)節(jié)就可以。
三個(gè)關(guān)鍵步驟介紹完了,下面上實(shí)戰(zhàn)代碼:
ZSFlowLayout:
/**
* 自定義瀑布流布局 系統(tǒng)核心方法
* ViewGroup getChildMeasureSpec 獲取子view的MeasureSpec信息
* View measure 對(duì)view進(jìn)行測量 測量以后就知道view大小了 之后可以通過getMeasuredWidth、getMeasuredHeight來獲取其寬高
* View MeasureSpec.getMode 獲取寬或高的模式(MeasureSpec.EXACTLY、MeasureSpec.AT_MOST、MeasureSpec.UNSPECIFIED)
* View MeasureSpec.getSize 獲取父布局能給我們的寬、高大小
* View setMeasuredDimension 設(shè)置測量結(jié)果
* View layout(left,top,right,bottom) 設(shè)置布局位置
*
* 幾個(gè)驗(yàn)證點(diǎn) getMeasuredHeight、getHeight何時(shí)有值 結(jié)論:分別在onMeasure 和 onLayout之后
* 子view是relativeLayout 并有子view時(shí)的情況 沒問題
* 通過addView方式添加 ok 已驗(yàn)證
*/
class ZSFlowLayout : ViewGroup {
//保存所有子view 按行保存 每行都可能有多個(gè)view 所有是一個(gè)list
var allViews: MutableList<MutableList<View>> = mutableListOf()
//每個(gè)子view之間的水平間距
val horizontalSpace: Int =
resources.getDimensionPixelOffset(R.dimen.zs_flowlayout_horizontal_space)
//每行之間的間距
val verticalSpace: Int = resources.getDimensionPixelOffset(R.dimen.zs_flowlayout_vertical_space)
//記錄每一行的行高 onLayout時(shí)會(huì)用到
var lineHeights: MutableList<Int> = mutableListOf()
constructor(context: Context) : super(context) {}
/**
* 必須的構(gòu)造函數(shù),系統(tǒng)會(huì)通過反射來調(diào)用此構(gòu)造方法完成view的創(chuàng)建
*/
constructor(context: Context, attr: AttributeSet) : super(context, attr) {}
constructor (context: Context, attr: AttributeSet, defZStyle: Int) : super(
context,
attr,
defZStyle
) {
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
//會(huì)測量次
allViews.clear()
lineHeights.clear()
//保存每一行的view
var everyLineViews: MutableList<View> = mutableListOf()
//記錄每一行當(dāng)前的寬度,用來判斷是否要換行
var curLineHasUsedWidth: Int = paddingLeft + paddingRight
//父布局能給的寬
val selfWidth: Int = MeasureSpec.getSize(widthMeasureSpec)
//父布局能給的高
val selfHeight: Int = MeasureSpec.getSize(heightMeasureSpec)
//我們自己通過測量需要的寬(如果用戶在布局里對(duì)ZSFlowLayout的寬設(shè)置了wrap_content 就會(huì)用到這個(gè))
var selfNeedWidth = 0
//我們自己通過測量需要的高(如果用戶在布局里對(duì)ZSFlowLayout的高設(shè)置了wrap_content 就會(huì)用到這個(gè))
var selfNeedHeight = paddingBottom + paddingTop
var curLineHeight = 0
//第一步 先測量子view 核心系統(tǒng)方法是 View measure方法
//(1)因?yàn)樽觱iew有很多,所以循環(huán)遍歷執(zhí)行
for (i in 0 until childCount) {
val childView = getChildAt(i)
if (childView.visibility == GONE) {
continue
}
//測量view之前 先把測量需要的參數(shù)準(zhǔn)備好 通過ViewGroup getChildMeasureSpec獲取子view的MeasureSpec信息
val childWidthMeasureSpec = getChildMeasureSpec(
widthMeasureSpec,
paddingLeft + paddingRight,
childView.layoutParams.width
)
val childHeightMeasureSpec = getChildMeasureSpec(
heightMeasureSpec,
paddingTop + paddingBottom,
childView.layoutParams.height
)
//調(diào)用子view的measure方法來對(duì)子view進(jìn)行測量
childView.measure(childWidthMeasureSpec, childHeightMeasureSpec)
//測量之后就能拿到子view的寬高了,保存起來用于判斷是否要換行 以及需要的總高度
val measuredHeight = childView.measuredHeight
val measuredWidth = childView.measuredWidth
//按行保存view 保存之前判斷是否需要換行,如果需要就保存在下一行的list里面
if (curLineHasUsedWidth + measuredWidth > selfWidth) {
//要換行了 先記錄換行之前的數(shù)據(jù)
lineHeights.add(curLineHeight)
selfNeedHeight += curLineHeight + verticalSpace
allViews.add(everyLineViews)
//再處理當(dāng)前要換行的view相關(guān)數(shù)據(jù)
curLineHeight = measuredHeight
everyLineViews = mutableListOf()
curLineHasUsedWidth = paddingLeft + paddingRight + measuredWidth + horizontalSpace
} else {
//每一行的高度是這一行view中最高的那個(gè)
curLineHeight = curLineHeight.coerceAtLeast(measuredHeight)
curLineHasUsedWidth += measuredWidth + horizontalSpace
}
everyLineViews.add(childView)
selfNeedWidth = selfNeedWidth.coerceAtLeast(curLineHasUsedWidth)
//處理最后一行
if (i == childCount - 1) {
curLineHeight = curLineHeight.coerceAtLeast(measuredHeight)
allViews.add(everyLineViews)
selfNeedHeight += curLineHeight
lineHeights.add(curLineHeight)
}
}
//第二步 測量自己
//根據(jù)父類傳入的尺寸規(guī)則 widthMeasureSpec、heightMeasureSpec 獲取當(dāng)前自身應(yīng)該遵守的布局模式
//以widthMeasureSpec為例說明下 這個(gè)是父類傳入的,那父類是如何按照什么規(guī)則生成的widthMeasureSpec呢?
//父類會(huì)結(jié)合自身的情況,并且結(jié)合子view的情況(子類的寬是match_parent、wrap_content、還是寫死的值)來生成
//生成的具體邏輯 請(qǐng)見:ViewGroup的getChildMeasureSpec方法
//(1)獲取父類傳過來的 我們自身應(yīng)該遵守的尺寸模式
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
//(2)根據(jù)模式來判斷最終的寬高
val widthResult = if (widthMode == MeasureSpec.EXACTLY) selfWidth else selfNeedWidth
val heightResult = if (heightMode == MeasureSpec.EXACTLY) selfHeight else selfNeedHeight
//第三步 設(shè)置自身的測量結(jié)果
setMeasuredDimension(widthResult, heightResult)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
//設(shè)置所有view的位置
var curT = paddingTop
for (i in allViews.indices) {
val mutableList = allViews[i]
//記錄每一行view的當(dāng)前距離父布局左側(cè)的位置 初始值就是父布局的paddingLeft
var curL = paddingLeft
if (i != 0) {
curT += lineHeights[i - 1] + verticalSpace
}
for (j in mutableList.indices) {
val view = mutableList[j]
val right = curL + view.measuredWidth
val bottom = curT + view.measuredHeight
view.layout(curL, curT, right, bottom)
//為下一個(gè)view做準(zhǔn)備
curL += view.measuredWidth + horizontalSpace
}
}
}
}在布局文件中使用:
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_marginTop="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/zs_flowlayout_title_marginL"
android:text="三國名將"
android:textColor="@android:color/black"
android:textSize="18sp" />
<com.zs.test.customview.ZSFlowLayout
android:id="@+id/activity_flow_flowlayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:padding="7dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="呂布呂奉先" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="趙云趙子龍" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:paddingLeft="10dp"
android:text="典韋" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="關(guān)羽關(guān)云長" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="馬超馬孟起" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="張飛張翼德" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="黃忠" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="徐褚徐仲康" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="孫策孫伯符" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="太史慈" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="夏侯惇" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="夏侯淵" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="張遼" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="張郃" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="徐晃徐功明" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="龐德" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="甘寧甘興霸" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="周泰" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="魏延" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="張繡" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="文丑" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="顏良" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="鄧艾" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="姜維" />
</com.zs.test.customview.ZSFlowLayout>
</LinearLayout>
</ScrollView>也可以在代碼中動(dòng)態(tài)添加view(更接近實(shí)戰(zhàn),實(shí)戰(zhàn)中數(shù)據(jù)多是后臺(tái)請(qǐng)求而來)
class FlowActivity : AppCompatActivity() {
@BindView(id = R.id.activity_flow_flowlayout)
var flowLayout : ZSFlowLayout ? = null;
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_customview_flow)
BindViewInject.inject(this)
for (i in 1 until 50) {
val tv:TextView = TextView(this)
tv.text = "TextView $i"
flowLayout!!.addView(tv)
}
}
}其中BindViewInject是用反射+注解實(shí)現(xiàn)的一個(gè)小工具類
object BindViewInject {
/**
* 注入
*
* @param activity
*/
@JvmStatic
fun inject(activity: Activity) {
inject(activity, false)
}
fun inject(activity: Activity, isSetOnClickListener: Boolean) {
//第一步 獲取class對(duì)象
val aClass: Class<out Activity> = activity.javaClass
//第二步 獲取類本身定義的所有成員變量
val declaredFields = aClass.declaredFields
//第三步 遍歷找出有注解的屬性
for (i in declaredFields.indices) {
val field = declaredFields[i]
//判斷是否用BindView進(jìn)行注解
if (field.isAnnotationPresent(BindView::class.java)) {
//得到注解對(duì)象
val bindView = field.getAnnotation(BindView::class.java)
//得到注解對(duì)象上的id值 這個(gè)就是view的id
val id = bindView.id
if (id <= 0) {
Toast.makeText(activity, "請(qǐng)?jiān)O(shè)置正確的id", Toast.LENGTH_LONG).show()
return
}
//建立映射關(guān)系,找出view
val view = activity.findViewById<View>(id)
//修改權(quán)限
field.isAccessible = true
//第四步 給屬性賦值
try {
field[activity] = view
} catch (e: IllegalAccessException) {
e.printStackTrace()
}
//第五步 設(shè)置點(diǎn)擊監(jiān)聽
if (isSetOnClickListener) {
//這里用反射實(shí)現(xiàn) 增加練習(xí)
//第一步 獲取這個(gè)屬性的值
val button = field.get(activity)
//第二步 獲取其class對(duì)象
val javaClass = button.javaClass
//第三步 獲取其 setOnClickListener 方法
val method =
javaClass.getMethod("setOnClickListener", View.OnClickListener::class.java)
//第四步 執(zhí)行此方法
method.invoke(button, activity)
}
}
}
}
}@Target(AnnotationTarget.FIELD)
@Retention(RetentionPolicy.RUNTIME)
annotation class BindView( //value是默認(rèn)的,如果只有一個(gè)參數(shù),并且名稱是value,外面?zhèn)鬟f時(shí)可以直接寫值,否則就要通過鍵值對(duì)來傳值(例如:value = 1)
// int value() default 0;
val id: Int = 0
)總結(jié)
到此這篇關(guān)于Android中FlowLayout組件實(shí)現(xiàn)瀑布流效果的文章就介紹到這了,更多相關(guān)Android FlowLayout瀑布流內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android?Studio?2022.1.1創(chuàng)建項(xiàng)目的Gradle配置問題
這篇文章主要介紹了Android?Studio?2022.1.1創(chuàng)建項(xiàng)目的Gradle配置問題,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-04-04
kotlin實(shí)戰(zhàn)教程之lambda編程
這篇文章主要給大家介紹了關(guān)于kotlin實(shí)戰(zhàn)教程之lambda編程的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用kotlin具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-09-09
安裝時(shí)加入外部數(shù)據(jù)庫示例(android外部數(shù)據(jù)庫)
這篇文章主要介紹了android打包安裝時(shí)加入外部數(shù)據(jù)庫的示例,需要的朋友可以參考下2014-03-03
Android學(xué)習(xí)教程之圓形Menu菜單制作方法(1)
這篇文章主要為大家詳細(xì)介紹了Android學(xué)習(xí)教程之圓形Menu菜單操作代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-11-11
Android使用相機(jī)實(shí)現(xiàn)拍照存儲(chǔ)及展示功能詳解
這篇文章主要介紹了Android使用相機(jī)實(shí)現(xiàn)拍照存儲(chǔ)及展示功能,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2023-01-01
Android?RecyclerLineChart實(shí)現(xiàn)圖表繪制教程
這篇文章主要為大家介紹了Android?RecyclerLineChart實(shí)現(xiàn)圖表繪制教程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12

