Flutter學(xué)習(xí)之構(gòu)建、布局及繪制三部曲
前言
學(xué)習(xí)Fullter也有些時(shí)間了,寫(xiě)過(guò)不少demo,對(duì)一些常用的widget使用也比較熟練,但是總覺(jué)得對(duì)Flutter的框架沒(méi)有一個(gè)大致的了解,碰到有些細(xì)節(jié)的地方又沒(méi)有文檔可以查詢,例如在寫(xiě)UI時(shí)總不知道為什么container添加了child就變小了;widget中key的作用,雖然官方有解釋但是憑空來(lái)講的話有點(diǎn)難理解。所以覺(jué)得深入一點(diǎn)的了解Flutter框架還是很有必要的。
構(gòu)建
初次構(gòu)建
flutter的入口main方法直接調(diào)用了runApp(Widget app)方法,app參數(shù)就是我們的根視圖的Widget,我們直接跟進(jìn)runApp方法
void runApp(Widget app) {
WidgetsFlutterBinding.ensureInitialized()//此方法是對(duì)flutter的框架做一些必要的初始化
..attachRootWidget(app)
..scheduleWarmUpFrame();
}
runApp方法先調(diào)用了WidgetsFlutterBinding.ensureInitialized()方法,這個(gè)方法是做一些必要的初始化
class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {
static WidgetsBinding ensureInitialized() {
if (WidgetsBinding.instance == null)
WidgetsFlutterBinding();
return WidgetsBinding.instance;
}
}
WidgetsFlutterBinding混入了不少的其他的Binding
- BindingBase 那些單一服務(wù)的混入類的基類
- GestureBinding framework手勢(shì)子系統(tǒng)的綁定,處理用戶輸入事件
- ServicesBinding 接受平臺(tái)的消息將他們轉(zhuǎn)換成二進(jìn)制消息,用于平臺(tái)與flutter的通信
- SchedulerBinding 調(diào)度系統(tǒng),用于調(diào)用Transient callbacks(
Window.onBeginFrame的回調(diào))、Persistent callbacks(Window.onDrawFrame的回調(diào))、Post-frame callbacks(在Frame結(jié)束時(shí)只會(huì)被調(diào)用一次,調(diào)用后會(huì)被系統(tǒng)移除,在Persistent callbacks后Window.onDrawFrame回調(diào)返回之前執(zhí)行) - PaintingBinding 繪制庫(kù)的綁定,主要處理圖片緩存
- SemanticsBinding 語(yǔ)義化層與Flutter engine的橋梁,主要是輔助功能的底層支持
- RendererBinding 渲染樹(shù)與Flutter engine的橋梁
- WidgetsBinding Widget層與Flutter engine的橋梁
以上是這些Binding的主要作用,在此不做過(guò)多贅述,WidgetsFlutterBinding.ensureInitialized()返回的是WidgetsBinding對(duì)象,然后馬上調(diào)用了WidgetsBinding的attachRootWidget(app)方法,將我們的根視圖的Widget對(duì)象穿進(jìn)去,我們繼續(xù)看attachRootWidget方法
void attachRootWidget(Widget rootWidget) {
_renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
container: renderView,
debugShortDescription: '[root]',
child: rootWidget
).attachToRenderTree(buildOwner, renderViewElement);
}
創(chuàng)建了一個(gè)RenderObjectToWidgetAdapter,讓后直接調(diào)用它的attachToRenderTree方法,BuildOwner是Widget framework的管理類
RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [RenderObjectToWidgetElement<T> element]) {
if (element == null) {
owner.lockState(() {
element = createElement();
assert(element != null);
element.assignOwner(owner);
});
owner.buildScope(element, () {
element.mount(null, null);
});
} else {
element._newWidget = this;
element.markNeedsBuild();
}
return element;
}
element為空,owner先鎖定狀態(tài),然后調(diào)用了RenderObjectToWidgetAdapter的createElement()返回了RenderObjectToWidgetElement對(duì)象,讓后將owner賦值給element(assignOwner方法),讓后就是owner調(diào)用buildScope方法
void buildScope(Element context, [VoidCallback callback]) {
if (callback == null && _dirtyElements.isEmpty)
return;
Timeline.startSync('Build', arguments: timelineWhitelistArguments);
try {
_scheduledFlushDirtyElements = true;
if (callback != null) {
_dirtyElementsNeedsResorting = false;
try {
callback();
} finally {}
}
...
}
省略了部分以及后續(xù)代碼,可以看到buildScope方法首先就調(diào)用了callback(就是element.mount(null, null)方法),回到RenderObjectToWidgetElement的mount方法
@override
void mount(Element parent, dynamic newSlot) {
assert(parent == null);
super.mount(parent, newSlot);
_rebuild();
}
首先super.mount(parent, newSlot)調(diào)用了RootRenderObjectElement的mount方法(只是判定parent和newSlot都為null),讓后又繼續(xù)向上調(diào)用了RenderObjectElement中的mount方法
@override
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_renderObject = widget.createRenderObject(this);
attachRenderObject(newSlot);
_dirty = false;
}
RenderObjectElement中的mount方法又調(diào)用了Element的mount方法
@mustCallSuper
void mount(Element parent, dynamic newSlot) {
_parent = parent;
_slot = newSlot;
_depth = _parent != null ? _parent.depth + 1 : 1;
_active = true;
if (parent != null) // Only assign ownership if the parent is non-null
_owner = parent.owner;
if (widget.key is GlobalKey) {
final GlobalKey key = widget.key;
key._register(this);
}
_updateInheritance();
}
Element的mount方法其實(shí)就是進(jìn)行了一些賦值,以確認(rèn)當(dāng)前Element在整個(gè)樹(shù)種的位置,讓后回到RenderObjectElement中的mount方法,調(diào)用了widget.createRenderObject(this)方法,widget是RenderObjectToWidgetAdapter的實(shí)例,它返回的是RenderObjectWithChildMixin對(duì)象,讓后調(diào)用attachRenderObject方法
@override
void attachRenderObject(dynamic newSlot) {
assert(_ancestorRenderObjectElement == null);
_slot = newSlot;
_ancestorRenderObjectElement = _findAncestorRenderObjectElement();//獲取此RenderObjectElement最近的RenderObjectElement對(duì)象
_ancestorRenderObjectElement?.insertChildRenderObject(renderObject, newSlot);//將renderObject插入RenderObjectElement中
final ParentDataElement<RenderObjectWidget> parentDataElement = _findAncestorParentDataElement();
if (parentDataElement != null)
_updateParentData(parentDataElement.widget);
}
///RenderObjectToWidgetElement中的insertChildRenderObject方法,簡(jiǎn)單將子RenderObject賦值給父RenderObject的child字段
@override
void insertChildRenderObject(RenderObject child, dynamic slot) {
assert(slot == _rootChildSlot);
assert(renderObject.debugValidateChild(child));
renderObject.child = child;
}
Element的mount方法確定當(dāng)前Element在整個(gè)樹(shù)種的位置并插入,RenderObjectElement中的mount方法來(lái)創(chuàng)建RenderObject對(duì)象并將其插入到渲染樹(shù)中,讓后再回到RenderObjectToWidgetElement方法,mount之后調(diào)用_rebuild()方法, _rebuild()方法中主要是調(diào)用了Element的updateChild方法
@protected
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
if (newWidget == null) {//當(dāng)子Widget沒(méi)有的時(shí)候,直接將child deactivate掉
if (child != null)
deactivateChild(child);
return null;
}
if (child != null) {//有子Element的時(shí)候
if (child.widget == newWidget) {//Widget沒(méi)有改變
if (child.slot != newSlot)//再判斷slot有沒(méi)有改變,沒(méi)有則不更新slot
updateSlotForChild(child, newSlot);//更新child的slot
return child;//返回child
}
if (Widget.canUpdate(child.widget, newWidget)) {//Widget沒(méi)有改變,再判斷Widget能否update,如果能還是重復(fù)上面的步驟
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
child.update(newWidget);
return child;
}
deactivateChild(child);//如果不能更新的話,直接將child deactivate掉,然后在inflateWidget(newWidget, newSlot)創(chuàng)建新的Element
}
return inflateWidget(newWidget, newSlot);//根據(jù)Widget對(duì)象以及slot創(chuàng)建新的Element
}
由于我們是第一次構(gòu)建,child是null,所以就直接走到inflateWidget方法創(chuàng)建新的Element對(duì)象,跟進(jìn)inflateWidget方法
@protected
Element inflatinflateWidgeteWidget(Widget newWidget, dynamic newSlot) {
final Key key = newWidget.key;
if (key is GlobalKey) {//newWidget的key是GlobalKey
final Element newChild = _retakeInactiveElement(key, newWidget);//復(fù)用Inactive狀態(tài)的Element
if (newChild != null) {
newChild._activateWithParent(this, newSlot);//activate 此Element(將newChild出入到Element樹(shù))
final Element updatedChild = updateChild(newChild, newWidget, newSlot);//直接將newChild更新
return updatedChild;//返回更新后的Element
}
}
final Element newChild = newWidget.createElement();//調(diào)用createElement()進(jìn)行創(chuàng)建
newChild.mount(this, newSlot);//繼續(xù)調(diào)用newChild Element的mount方法(如此就行一直遞歸下去,當(dāng)遞歸完成,整個(gè)構(gòu)建過(guò)程也就結(jié)束了)
return newChild;//返回子Element
}
inflateWidget中其實(shí)就是通過(guò)Widget得到Element對(duì)象,讓后繼續(xù)調(diào)用子Element的mount的方將進(jìn)行遞歸。
不同的Element,mount的實(shí)現(xiàn)會(huì)有所不同,我們看一下比較常用的StatelessElement、StatefulElement,他們的mount方法實(shí)現(xiàn)在ComponentElement中
@override
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_firstBuild();
}
void _firstBuild() {
rebuild();//調(diào)用了Element的rebuild()方法
}
//Element的rebuild方法,通常被三處地方調(diào)用
//1.當(dāng)BuildOwner.scheduleBuildFor被調(diào)用標(biāo)記此Element為dirty時(shí)
//2.當(dāng)Element第一次構(gòu)建由mount方法去調(diào)用
//3.當(dāng)Widget改變時(shí),被update方法調(diào)用
void rebuild() {
if (!_active || !_dirty)
return;
performRebuild();//調(diào)用performRebuild方法(抽象方法)
}
//ComponentElement的performRebuild實(shí)現(xiàn)
@override
void performRebuild() {
Widget built;
try {
built = build();//構(gòu)建Widget(StatelessElement直接調(diào)用build方法,StatefulElement直接調(diào)用state.build方法)
} catch (e, stack) {
built = ErrorWidget.builder(_debugReportException('building $this', e, stack));//有錯(cuò)誤的化就創(chuàng)建一個(gè)ErrorWidget
} finally {
_dirty = false;
}
try {
_child = updateChild(_child, built, slot);//讓后還是根據(jù)Wdiget來(lái)更新子Element
} catch (e, stack) {
built = ErrorWidget.builder(_debugReportException('building $this', e, stack));
_child = updateChild(null, built, slot);
}
}
再看一看MultiChildRenderObjectElement的mount方法
@override
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_children = List<Element>(widget.children.length);
Element previousChild;
for (int i = 0; i < _children.length; i += 1) {
final Element newChild = inflateWidget(widget.children[i], previousChild);//遍歷children直接inflate根據(jù)Widget創(chuàng)建新的Element
_children[i] = newChild;
previousChild = newChild;
}
}
可以看到不同的Element構(gòu)建方式會(huì)有些不同,Element(第一層Element)的mount方法主要是確定當(dāng)前Element在整個(gè)樹(shù)種的位置并插入;ComponentElement(第二層)的mount方法先構(gòu)建Widget樹(shù),讓后再遞歸更新(包括重用,更新,直接創(chuàng)建inflate)其Element樹(shù);RenderObjectElement(第二層)中的mount方法來(lái)創(chuàng)建RenderObject對(duì)象并將其插入到渲染樹(shù)中。MultiChildRenderObjectElement(RenderObjectElement的子類)在RenderObjectElement還要繼續(xù)創(chuàng)建children Element。
總結(jié):首先是由WidgetBinding創(chuàng)建RenderObjectToWidgetAdapter然后調(diào)用它的attachToRenderTree方法,創(chuàng)建了RenderObjectToWidgetElement對(duì)象,讓后將它mount(調(diào)用mount方法),mount方法中調(diào)用的_rebuild,繼而調(diào)用updateChild方法,updateChild會(huì)進(jìn)行遞歸的更新Element樹(shù),若child沒(méi)有則需要重新創(chuàng)建新的Element,讓后將其mount進(jìn)Element樹(shù)中(如果是RenderobjectElement的化,mount的過(guò)程中會(huì)去創(chuàng)建RenderObject對(duì)象,并插入到RenderTree)。
通過(guò)setState觸發(fā)構(gòu)建
通常我們?cè)趹?yīng)用中要更新?tīng)顟B(tài)都是通過(guò)State中的setState方法來(lái)觸發(fā)界面重繪,setState方法就是先調(diào)用了callback讓后調(diào)用該State的Element對(duì)象的markNeedsBuild方法,markNeedsBuild中將Element標(biāo)記為dirty并通過(guò)BuildOwner將其添加到dirty列表中并調(diào)用onBuildScheduled回調(diào)(在WidgetsBinding初始化時(shí)設(shè)置的,它回去調(diào)用window.scheduleFrame方法),讓后window的onBeginFrame,onDrawFrame回調(diào)(在SchedulerBinding初始化時(shí)設(shè)置的,這兩個(gè)回調(diào)會(huì)執(zhí)行一些callback)會(huì)被調(diào)用,SchedulerBinding通過(guò)persisterCallbacks來(lái)調(diào)用到BuildOwner中buildScope方法。上面我們只看了buildScope的一部分,當(dāng)通過(guò)setState方法來(lái)觸發(fā)界面重繪時(shí),buildScope的callBack為null
void buildScope(Element context, [VoidCallback callback]) {
if (callback == null && _dirtyElements.isEmpty)
return;
Timeline.startSync('Build', arguments: timelineWhitelistArguments);
try {
_scheduledFlushDirtyElements = true;
if (callback != null) {
Element debugPreviousBuildTarget;
_dirtyElementsNeedsResorting = false;
try {
callback();//調(diào)用callback
} finally {}
}
_dirtyElements.sort(Element._sort);
_dirtyElementsNeedsResorting = false;
int dirtyCount = _dirtyElements.length;
int index = 0;
while (index < dirtyCount) {
try {
_dirtyElements[index].rebuild();//遍歷dirtyElements并執(zhí)行他們的rebuild方法來(lái)使這些Element進(jìn)行rebuild
} catch (e, stack) {}
index += 1;
if (dirtyCount < _dirtyElements.length || _dirtyElementsNeedsResorting) {
_dirtyElements.sort(Element._sort);
_dirtyElementsNeedsResorting = false;
dirtyCount = _dirtyElements.length;
while (index > 0 && _dirtyElements[index - 1].dirty) {
index -= 1;
}
}
}
} finally {
for (Element element in _dirtyElements) {//最后解除Element的dirty標(biāo)記,以及清空dirtyElements
assert(element._inDirtyList);
element._inDirtyList = false;
}
_dirtyElements.clear();
_scheduledFlushDirtyElements = false;
_dirtyElementsNeedsResorting = null;
Timeline.finishSync();
}
}
很明顯就是對(duì)dirtyElements中的元素進(jìn)行遍歷并且對(duì)他們進(jìn)行rebuild。
布局
window通過(guò)scheduleFrame方法會(huì)讓SchedulerBinding來(lái)執(zhí)行handleBeginFrame方法(執(zhí)行transientCallbacks)和handleDrawFrame方法(執(zhí)行persistentCallbacks,postFrameCallbacks),在RendererBinding初始化時(shí)添加了_handlePersistentFrameCallback,它調(diào)用了核心的繪制方法drawFrame。
@protected
void drawFrame() {
assert(renderView != null);
pipelineOwner.flushLayout();//布局
pipelineOwner.flushCompositingBits();//刷新dirty的renderobject的數(shù)據(jù)
pipelineOwner.flushPaint();//繪制
renderView.compositeFrame(); // 將二進(jìn)制數(shù)據(jù)發(fā)送給GPU
pipelineOwner.flushSemantics(); // 將語(yǔ)義發(fā)送給系統(tǒng)
}
flushLayout觸發(fā)布局,將RenderObject樹(shù)的dirty節(jié)點(diǎn)通過(guò)調(diào)用performLayout方法進(jìn)行逐一布局,我們先看一下RenderPadding中的實(shí)現(xiàn)
@override
void performLayout() {
_resolve();//解析padding參數(shù)
if (child == null) {//如果沒(méi)有child,直接將constraints與padding綜合計(jì)算得出自己的size
size = constraints.constrain(Size(
_resolvedPadding.left + _resolvedPadding.right,
_resolvedPadding.top + _resolvedPadding.bottom
));
return;
}
final BoxConstraints innerConstraints = constraints.deflate(_resolvedPadding);//將padding減去,生成新的約束innerConstraints
child.layout(innerConstraints, parentUsesSize: true);//用新的約束去布局child
final BoxParentData childParentData = child.parentData;
childParentData.offset = Offset(_resolvedPadding.left, _resolvedPadding.top);//設(shè)置childParentData的offset值(這個(gè)值是相對(duì)于parent的繪制偏移值,在paint的時(shí)候傳入這個(gè)偏移值)
size = constraints.constrain(Size(//將constraints與padding以及child的sieze綜合計(jì)算得出自己的size
_resolvedPadding.left + child.size.width + _resolvedPadding.right,
_resolvedPadding.top + child.size.height + _resolvedPadding.bottom
));
}
可以看到RenderPadding中的布局分兩種情況。如果沒(méi)有child,那么就直接拿parent傳過(guò)來(lái)的約束以及padding來(lái)確定自己的大?。环駝t就先去布局child,讓后再拿parent傳過(guò)來(lái)的約束和padding以及child的size來(lái)確定自己的大小。
RenderPadding是典型的單child的RenderBox,我們看一下多個(gè)child的RenderBox。例如RenderFlow
@override
void performLayout() {
size = _getSize(constraints);//直接先確定自己的size
int i = 0;
_randomAccessChildren.clear();
RenderBox child = firstChild;
while (child != null) {//遍歷孩子
_randomAccessChildren.add(child);
final BoxConstraints innerConstraints = _delegate.getConstraintsForChild(i, constraints);//獲取child的約束,此方法為抽象
child.layout(innerConstraints, parentUsesSize: true);//布局孩子
final FlowParentData childParentData = child.parentData;
childParentData.offset = Offset.zero;
child = childParentData.nextSibling;
i += 1;
}
}
可以看到RenderFlow的size直接就根據(jù)約束來(lái)確定了,并沒(méi)去有先布局孩子,所以RenderFlow的size不依賴與孩子,后面依舊是對(duì)每一個(gè)child依次進(jìn)行布局。
還有一種比較典型的樹(shù)尖類型的RenderBox,LeafRenderObjectWidget子類創(chuàng)建的RenderObject對(duì)象都是,他們沒(méi)有孩子,他們才是最終需要渲染的對(duì)象,例如
@override
void performLayout() {
size = _sizeForConstraints(constraints);
}
非常簡(jiǎn)單就通過(guò)約束確定自己的大小就結(jié)束了。所以performLayout過(guò)程就是兩點(diǎn),確定自己的大小以及布局孩子。我們上面提到的都是RenderBox的子類,這些RenderObject約束都是通過(guò)BoxConstraints來(lái)完成,但是RenderSliver的子類的約束是通過(guò)SliverConstraints來(lái)完成,雖然他們對(duì)child的約束方式不同,但他們?cè)诓季诌^(guò)程需要執(zhí)行的操作都是一致的。
繪制
布局完成了,PipelineOwner就通過(guò)flushPaint來(lái)進(jìn)行繪制
void flushPaint() {
try {
final List<RenderObject> dirtyNodes = _nodesNeedingPaint;
_nodesNeedingPaint = <RenderObject>[];
// 對(duì)dirty nodes列表進(jìn)行排序,最深的在第一位
for (RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => b.depth - a.depth)) {
assert(node._layer != null);
if (node._needsPaint && node.owner == this) {
if (node._layer.attached) {
PaintingContext.repaintCompositedChild(node);
} else {
node._skippedPaintingOnLayer();
}
}
}
} finally {}
}
PaintingContext.repaintCompositedChild(node)會(huì)調(diào)用到child._paintWithContext(childContext, Offset.zero)方法,進(jìn)而調(diào)用到child的paint方法,我們來(lái)看一下第一次繪制的情況,dirty的node就應(yīng)該是RenderView,跟進(jìn)RenderView的paint方法
@override
void paint(PaintingContext context, Offset offset) {
if (child != null)
context.paintChild(child, offset);//直接繪制child
}
自己沒(méi)有什么繪制的內(nèi)容,直接繪制child,再看一下RenderShiftedBox
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
final BoxParentData childParentData = child.parentData;
context.paintChild(child, childParentData.offset + offset);//直接繪制child
}
}
好像沒(méi)有繪制內(nèi)容就直接遞歸的進(jìn)行繪制child,那找一個(gè)有繪制內(nèi)容的吧,我們看看RenderDecoratedBox
@override
void paint(PaintingContext context, Offset offset) {//Offset由parent去paintChild的時(shí)候傳入,該值存放在child的parentdata字段中,該字段是BoxParentData或以下實(shí)例
_painter ??= _decoration.createBoxPainter(markNeedsPaint);//獲取painter畫(huà)筆
final ImageConfiguration filledConfiguration = configuration.copyWith(size: size);
if (position == DecorationPosition.background) {//畫(huà)背景
_painter.paint(context.canvas, offset, filledConfiguration);//繪制過(guò)程,具體細(xì)節(jié)再painter中
if (decoration.isComplex)
context.setIsComplexHint();
}
super.paint(context, offset);//畫(huà)child,里面直接調(diào)用了paintChild
if (position == DecorationPosition.foreground) {//畫(huà)前景
_painter.paint(context.canvas, offset, filledConfiguration);
if (decoration.isComplex)
context.setIsComplexHint();
}
}
如果自己有繪制內(nèi)容,paint方法中的實(shí)現(xiàn)就應(yīng)該包括繪制自己以及繪制child,如果沒(méi)有孩子就只繪制自己的內(nèi)容,看一下RenderImage
@override
void paint(PaintingContext context, Offset offset) {
if (_image == null)
return;
_resolve();
paintImage(//直接繪制Image,具體細(xì)節(jié)再此方法中
canvas: context.canvas,
rect: offset & size,
image: _image,
scale: _scale,
colorFilter: _colorFilter,
fit: _fit,
alignment: _resolvedAlignment,
centerSlice: _centerSlice,
repeat: _repeat,
flipHorizontally: _flipHorizontally,
invertColors: invertColors,
filterQuality: _filterQuality
);
}
所以基本上繪制需要完成的流程就是,如果自己有繪制內(nèi)容,paint方法中的實(shí)現(xiàn)就應(yīng)該包括繪制自己以及繪制child,如果沒(méi)有孩子就只繪制自己的內(nèi)容,流程比較簡(jiǎn)單。
以上是自己學(xué)習(xí)的一些總結(jié),如有錯(cuò)誤之處請(qǐng)指出,大家共同探討,覺(jué)得不錯(cuò)的話,點(diǎn)個(gè)贊唄!
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
Android 中TextView中跑馬燈效果的實(shí)現(xiàn)方法
這篇文章主要介紹了Android 中TextView中跑馬燈效果的實(shí)現(xiàn)方法,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-02-02
ASM的tree?api對(duì)匿名線程的hook操作詳解
這篇文章主要為大家介紹了ASM的tree?api對(duì)匿名線程的hook操作詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09
Android自定義控件之繼承ViewGroup創(chuàng)建新容器
這篇文章主要介紹了Android自定義控件之繼承ViewGroup創(chuàng)建新容器,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-12-12
Android將camera獲取到的YuvData在jni中轉(zhuǎn)化為Mat方法
今天小編就為大家分享一篇Android將camera獲取到的YuvData在jni中轉(zhuǎn)化為Mat方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-08-08
Android簡(jiǎn)單實(shí)現(xiàn)動(dòng)態(tài)權(quán)限獲取相機(jī)權(quán)限及存儲(chǔ)空間等多權(quán)限
這篇文章主要介紹了Android簡(jiǎn)單實(shí)現(xiàn)動(dòng)態(tài)權(quán)限獲取相機(jī)權(quán)限及存儲(chǔ)空間等多權(quán)限,文章圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的朋友可以參考一下2022-07-07
RecyclerView實(shí)現(xiàn)流式標(biāo)簽單選多選功能
RecyclerView是Android一個(gè)更強(qiáng)大的控件,其不僅可以實(shí)現(xiàn)和ListView同樣的效果,還有優(yōu)化了ListView中的各種不足。這篇文章主要介紹了RecyclerView實(shí)現(xiàn)的流式標(biāo)簽單選多選功能,需要的朋友可以參考下2019-11-11

