丈夫志四海,万里犹比邻。这篇文章主要讲述Android6.0 WMS WMS动画管理相关的知识,希望能为你提供帮助。
android的应用启动时,
或者切换Activity时都会以动画的方式显示前后两屏的切换过程。动画的原理很简单,
把一帧一帧的图像按一定时间间隔显示出来就完成了。
动画的绘制需要定时驱动,
通常的做法是启动一个定时消息,
每隔一定时间发一个消息,
收到消息后输出一帧画面。Android支持VSync信号后,
动画的驱动就有VSync信号承担了。
窗口动画的基本元素是窗口Surface中保存的图像,
通过对窗口的Surface设置不同的变换矩阵和透明度,
然后强制Surface刷新,
就能在屏幕上显示出窗口的变化过程。
Choreographer对象初始化
我们先来看WMS中的mChoreographer 变量
final Choreographer mChoreographer =
Choreographer.getInstance();
该变量是一个线程局部存储变量, 在它的initialValue中创建了Choreographer对象并返回。这里使用线程局部存储的目录就是保证在线程中只有一个Choreographer对象。
public static Choreographer getInstance() {
return sThreadInstance.get();
}
private static final ThreadLocal<
Choreographer>
sThreadInstance =
new ThreadLocal<
Choreographer>
() {
@
Override
protected Choreographer initialValue() {
Looper looper =
Looper.myLooper();
if (looper =
=
null) {
throw new IllegalStateException("
The current thread must have a looper!"
);
}
return new Choreographer(looper);
}
};
再来看下Choreographer的构造函数, 这里主要是创建了FrameDisplayEventReceiver用来接受VSync信号的对象。
private Choreographer(Looper looper) {
mLooper =
looper;
mHandler =
new FrameHandler(looper);
mDisplayEventReceiver =
USE_VSYNC ? new FrameDisplayEventReceiver(looper) : null;
//接受VSync信号对象
mLastFrameTimeNanos =
Long.MIN_VALUE;
mFrameIntervalNanos =
(long)(1000000000 / getRefreshRate());
//计算刷新的时间间隔mCallbackQueues =
new CallbackQueue[CALLBACK_LAST +
1];
for (int i =
0;
i <
=
CALLBACK_LAST;
i+
+
) {
mCallbackQueues[i] =
new CallbackQueue();
}
}
FrameDisplayEventReceiver接受VSync信号
我们在http://blog.csdn.net/kc58236582/article/details/52892384( Android6.0 VSync信号如何到用户进程 ) 这篇博客已经分析过FrameDisplayEventReceiver的原理了, 当VSync信号过来时, 最后会调用到FrameDisplayEventReceiver类的onVsync函数:
@
Override
public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
if (builtInDisplayId !=
SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN) {
scheduleVsync();
return;
}long now =
System.nanoTime();
if (timestampNanos >
now) {
Log.w(TAG, "
Frame time is "
+
((timestampNanos - now) * 0.000001f)
+
"
ms in the future!Check that graphics HAL is generating vsync "
+
"
timestamps using the correct timebase."
);
timestampNanos =
now;
}if (mHavePendingVsync) {
Log.w(TAG, "
Already have a pending vsync event.There should only be "
+
"
one at a time."
);
} else {
mHavePendingVsync =
true;
}mTimestampNanos =
timestampNanos;
mFrame =
frame;
Message msg =
Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}
这主要是发送了一个信号, 而是是Runnable的那种消息。
因此我们主要看下这个类的run函数, 这里就是调用了Choreographer的doFrame函数。
@
Override
public void run() {
mHavePendingVsync =
false;
doFrame(mTimestampNanos, mFrame);
}
doFrame函数
doFrame函数主要有一些VSync时间逻辑处理如果抛弃该VSync信号的话会调用scheduleVsyncLocked函数让SurfaceFlinger发送一个VSync信号, 如果正常会调用4个doCallBacks函数。
void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
......long intendedFrameTimeNanos =
frameTimeNanos;
startNanos =
System.nanoTime();
final long jitterNanos =
startNanos - frameTimeNanos;
if (jitterNanos >
=
mFrameIntervalNanos) {
final long skippedFrames =
jitterNanos / mFrameIntervalNanos;
final long lastFrameOffset =
jitterNanos % mFrameIntervalNanos;
frameTimeNanos =
startNanos - lastFrameOffset;
}if (frameTimeNanos <
mLastFrameTimeNanos) {
if (DEBUG_JANK) {
Log.d(TAG, "
Frame time appears to be going backwards.May be due to a "
+
"
previously skipped frame.Waiting for next vsync."
);
}
scheduleVsyncLocked();
//让SurfaceFlinger立马发送一个VSync信号
return;
}mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
mFrameScheduled =
false;
mLastFrameTimeNanos =
frameTimeNanos;
}try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "
Choreographer#doFrame"
);
mFrameInfo.markInputHandlingStart();
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
//按键相关mFrameInfo.markAnimationsStart();
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
//动画相关mFrameInfo.markPerformTraversalsStart();
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
//power相关doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
doCallbacks函数, 我们首先会检查当前这个CallBackType是否有对应的CallBack回调, 如果没有直接return, 如果有的话会调用其回调的run函数。
void doCallbacks(int callbackType, long frameTimeNanos) {
CallbackRecord callbacks;
synchronized (mLock) {
final long now =
System.nanoTime();
callbacks =
mCallbackQueues[callbackType].extractDueCallbacksLocked(
now / TimeUtils.NANOS_PER_MS);
if (callbacks =
=
null) {//没有对应CallBack回调
return;
}
mCallbacksRunning =
true;
// safe by ensuring the commit time is always at least one frame behind.
if (callbackType =
=
Choreographer.CALLBACK_COMMIT) {
final long jitterNanos =
now - frameTimeNanos;
Trace.traceCounter(Trace.TRACE_TAG_VIEW, "
jitterNanos"
, (int) jitterNanos);
if (jitterNanos >
=
2 * mFrameIntervalNanos) {
final long lastFrameOffset =
jitterNanos % mFrameIntervalNanos
+
mFrameIntervalNanos;
if (DEBUG_JANK) {
mDebugPrintNextFrameTimeDelta =
true;
}
frameTimeNanos =
now - lastFrameOffset;
mLastFrameTimeNanos =
frameTimeNanos;
}
}
}
try {
for (CallbackRecord c =
callbacks;
c !=
null;
c =
c.next) {
c.run(frameTimeNanos);
//调用回调run函数
}
}
......
}
这也就意味着当你没有CallBackType对应的回调, 每次VSync信号过来到doFrame函数再到doCallBacks函数都是没有意义的。
WMS启动动画
那我们下面看在哪里把CallBackType对应的回调加入了, 这里我们只关注动画相关的。
上面我们说到VSync会不断的发送, 每秒60多次, 但是动画不会不停的播放, 就是这个CallBackType对应的回调没有。哪动画的启动和结束也就是受这个影响, 而就是在WMS中调用scheduleAnimationLocked函数发起的动画启动。
void scheduleAnimationLocked() {
if (!mAnimationScheduled) {
mAnimationScheduled =
true;
mChoreographer.postFrameCallback(mAnimator.mAnimationFrameCallback);
}
}
这里就是调用Choreographer设置CallBackType, 相关的回调。这里我们的callbackType是CALLBACK_ANIMATION
public void postFrameCallback(FrameCallback callback) {
postFrameCallbackDelayed(callback, 0);
}public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {
if (callback =
=
null) {
throw new IllegalArgumentException("
callback must not be null"
);
}postCallbackDelayedInternal(CALLBACK_ANIMATION,
callback, FRAME_CALLBACK_TOKEN, delayMillis);
}
我们最后看postCallbackDelayedInternal函数, 就是在mCallBackQueues对应的CallBackType中增加相应的回调。这里也就是前面在WMS的scheduleAnimationLocked的参数mAnimator.mAnimationFrameCallback就是回调。
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
synchronized (mLock) {
final long now =
SystemClock.uptimeMillis();
final long dueTime =
now +
delayMillis;
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
if (dueTime <
=
now) {
scheduleFrameLocked(now);
} else {
Message msg =
mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 =
callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}
我们来看下scheduleFrameLocked函数, 我们注意mFrameScheduled这个变量, 这个时候赋值为true, 后面就是用这个变量来控制每次VSync信号过来调用doFrame函数的时候是否要播放动画
private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled =
true;
//注意这个变量
if (USE_VSYNC) {
if (isRunningOnLooperThreadLocked()) {
scheduleVsyncLocked();
//尽快让SurfaceFlinger中的EventThread发送一个VSync信号
} else {
Message msg =
mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);
mHandler.sendMessageAtFrontOfQueue(msg);
}
} else {
final long nextFrameTime =
Math.max(
mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS +
sFrameDelay, now);
if (DEBUG_FRAMES) {
Log.d(TAG, "
Scheduling next frame in "
+
(nextFrameTime - now) +
"
ms."
);
}
Message msg =
mHandler.obtainMessage(MSG_DO_FRAME);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, nextFrameTime);
}
}
}
我们再回过头来看doFrame函数, 当mFrameScheduled为false时, VSync信号过来该函数直接return不会播放动画。
void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
if (!mFrameScheduled) {
return;
// no work to do
}
继续看postCallbackDelayedInternal函数中增加的回调, 这个回调在WindowAnimator的构造函数中就新建了Choreographer.FrameCallback回调
WindowAnimator(final WindowManagerService service) {
mService =
service;
mContext =
service.mContext;
mPolicy =
service.mPolicy;
mAnimationFrameCallback =
new Choreographer.FrameCallback() {
public void doFrame(long frameTimeNs) {
synchronized (mService.mWindowMap) {
mService.mAnimationScheduled =
false;
animateLocked(frameTimeNs);
}
}
};
}
我们最后看回调的run函数, 如果是FRAME_CALLBACK_TOKEN, 就是调用回调的doFrame函数。
private static final class CallbackRecord {
public CallbackRecord next;
public long dueTime;
public Object action;
// Runnable or FrameCallback
public Object token;
public void run(long frameTimeNanos) {
if (token =
=
FRAME_CALLBACK_TOKEN) {
((FrameCallback)action).doFrame(frameTimeNanos);
} else {
((Runnable)action).run();
}
}
}
播放动画
在上面doFrame函数启动动画, 而动画的播放主要在WindowAnimator的animateLocked函数。
private void animateLocked(long frameTimeNs) {
......
boolean wasAnimating =
mAnimating;
mAnimating =
false;
//设置mAnimating为false
mAppWindowAnimating =
false;
SurfaceControl.openTransaction();
SurfaceControl.setAnimationTransaction();
try {
final int numDisplays =
mDisplayContentsAnimators.size();
for (int i =
0;
i <
numDisplays;
i+
+
) {
final int displayId =
mDisplayContentsAnimators.keyAt(i);
updateAppWindowsLocked(displayId);
......// Update animations of all applications, including those
// associated with exiting/removed apps
updateWindowsLocked(displayId);
updateWallpaperLocked(displayId);
final WindowList windows =
mService.getWindowListLocked(displayId);
final int N =
windows.size();
for (int j =
0;
j <
N;
j+
+
) {
windows.get(j).mWinAnimator.prepareSurfaceLocked(true);
//输出动画帧
}
}for (int i =
0;
i <
numDisplays;
i+
+
) {
final int displayId =
mDisplayContentsAnimators.keyAt(i);
testTokenMayBeDrawnLocked(displayId);
final ScreenRotationAnimation screenRotationAnimation =
mDisplayContentsAnimators.valueAt(i).mScreenRotationAnimation;
if (screenRotationAnimation !=
null) {
screenRotationAnimation.updateSurfacesInTransaction();
}mAnimating |=
mService.getDisplayContentLocked(displayId).animateDimLayers();
if (mService.mAccessibilityController !=
null
&
&
displayId =
=
Display.DEFAULT_DISPLAY) {
mService.mAccessibilityController.drawMagnifiedRegionBorderIfNeededLocked();
}
}if (mAnimating) {//为true,
继续调用WMS的scheduleAnimationLocked播放下一帧
mService.scheduleAnimationLocked();
}......
finally {
SurfaceControl.closeTransaction();
}......
boolean doRequest =
false;
if (mBulkUpdateParams !=
0) {
doRequest =
mService.copyAnimToLayoutParamsLocked();
}if (hasPendingLayoutChanges || doRequest) {
mService.requestTraversalLocked();
//重新刷新UI
}if (!mAnimating &
&
wasAnimating) {
mService.requestTraversalLocked();
}
}
animateLocked方法先将mAnimating 设置为false, 然后调用updateWindowsLocked函数和updateWallpaperLocked函数, updateWindowsLocked这个函数会调用WindowStateAnimator类的stepAnimationLocker方法, 如果动画已经显示完最后一帧, stepAnimationLocker方法将会WindowStateAnimator类的mAnimating设置为false, 表示该窗口的动画已经结束。而在updateWallpaperLocked函数中会判断所有窗口的动画是否已经结束, 只要有一个动画没结束, 就会将winAnimator的mAnimating设置为true。
for (int i =
windows.size() - 1;
i >
=
0;
i--) {
final WindowState win =
windows.get(i);
WindowStateAnimator winAnimator =
win.mWinAnimator;
if (winAnimator.mSurfaceControl =
=
null) {
continue;
}final int flags =
win.mAttrs.flags;
if (winAnimator.mAnimating) {
......
mAnimating =
true;
}
......
再回到animatelocked方法, 当mAnimating为true是会调用WMS的scheduleAnimationLocked方法继续显示动画, 否则动画显示就结束了。
下面我们总结下动画的播放过程: 需要播放动画时, 先会调用WMS的scheduleAnimationLocked方法。调用这个方法后, 才会接受并处理一次VSync信号, 对VSync信号的处理, 就是所有需要绘制的窗口根据各自动画的谁知重新调整窗口Surface的变化矩阵和透明度; 如果还有窗口动画需要显示, 继续调用scheduleAnimationLocked方法准备下一帧。
【Android6.0 WMS WMS动画管理】准备一帧动画的时间可以跨越多个VSync信号周期, 但是只有收到VSync信号才能更新窗口的Surface的属性和内容, 对应用而言收到VSync信号意味着SurfaceFlinger中已经把上次设置的动画数据取走了, 可以安全地设置下一帧动画的属性和内容了。
窗口动画对象WindowStateAnimator
窗口对象WindowState中定义了一个类型为WindowStateAnimator的成员变量mWinAnimator, 用来表示窗口的动画对象。
下面是一些成员变量
boolean mAnimating;
//表示是否正在显示动画
boolean mLocalAnimating;
//表示窗口动画是否已经初始化
Animation mAnimation;
//表示窗口动画对象
boolean mAnimationIsEntrance;
//
boolean mHasTransformation;
//表示当前动画的mTransformation是否可用
boolean mHasLocalTransformation;
//表示当前动画时一个窗口动画还是Activity动画
final Transformation mTransformation =
new Transformation();
//变换矩阵对象
当前正在显示的动画有两种类型, 一种的窗口切换动画, 一种是Activity切换动画, 这里使用了mLocalAnimating和mHasLocalTransformation分别表示窗口动画的状态。
stepAnimationLocked是WindowStateAnimator类中显示动画首先调用的方法, 它会初始化WindowStateAnimator对象的一些成员变量
boolean stepAnimationLocked(long currentTime) {
final DisplayContent displayContent =
mWin.getDisplayContent();
if (displayContent !=
null &
&
mService.okToDisplay()) {if (mWin.isDrawnLw() &
&
mAnimation !=
null) {//窗口准备好绘制了,
窗口动画对象不为空
mHasTransformation =
true;
mHasLocalTransformation =
true;
if (!mLocalAnimating) {//还没有初始化窗口对象
final DisplayInfo displayInfo =
displayContent.getDisplayInfo();
if (mAnimateMove) {
mAnimateMove =
false;
mAnimation.initialize(mWin.mFrame.width(), mWin.mFrame.height(),//初始化窗口对象
mAnimDw, mAnimDh);
} else {
mAnimation.initialize(mWin.mFrame.width(), mWin.mFrame.height(),
displayInfo.appWidth, displayInfo.appHeight);
}
mAnimDw =
displayInfo.appWidth;
mAnimDh =
displayInfo.appHeight;
mAnimation.setStartTime(mAnimationStartTime !=
-1
? mAnimationStartTime
: currentTime);
mLocalAnimating =
true;
// 设置为true代表已经初始化窗口对象
mAnimating =
true;
}
if ((mAnimation !=
null) &
&
mLocalAnimating) {
mLastAnimationTime =
currentTime;
if (stepAnimation(currentTime)) {//通过时间判断动画是否显示完毕
return true;
}
}
}
mHasLocalTransformation =
false;
if ((!mLocalAnimating || mAnimationIsEntrance) &
&
mAppAnimator !=
null//没有设置窗口动画或者窗口动画结束了
&
&
mAppAnimator.animation !=
null) {
// 如果有Activity动画,
将mAnimating设为true
mAnimating =
true;
mHasTransformation =
true;
mTransformation.clear();
return false;
} else if (mHasTransformation) {
// Little trick to get through the path below to act like
// we have finished an animation.
mAnimating =
true;
} else if (isAnimating()) {
mAnimating =
true;
}
} else if (mAnimation !=
null) {
mAnimating =
true;
}if (!mAnimating &
&
!mLocalAnimating) {
return false;
}mAnimating =
false;
mKeyguardGoingAwayAnimation =
false;
mAnimatingMove =
false;
mLocalAnimating =
false;
......
mHasLocalTransformation =
false;
......
mTransformation.clear();
......return false;
}
该方法的工作就是设置WindowStateAnimator对象的几个成员变量, 首先调用WindowState对象的isDrawnLw来判断窗口系统的状态, 只有准备好了才能显示, 接着判断mAnimation是否为空, 不为空代表已经设置好了动画对象。
接下来判断mLocalAnimating变量的值, 为false则调用mAnimation的intialize方法来完成动画对象的初始化( 主要设置动画的高度和宽度) , 然后将mLocalAnimating和mAnimating设为true。完成初始化后, 接着调用stepAnimation方法来判断动画是否已经显示完成, 没有完成返回true。
如果没有设置动画或者动画已经结束了, 则还有判断窗口所在的Activity是否还存在动画, 如果有, 将mAnimating设置true( 表示还要继续播放动画) , 如果同时mHasTransformation的值仍然为true, 或者isAnimating方法返回true, 也将mAnimating设置为true。
isAnimating会根据当前动画对象mAnimation是否为空, 它的附加窗口的动画对象是否为空, 以及窗口所在的Activity的动画对象是否为空等条件来判断, 这表示只要有可能mAnimating就会设置为true。这样的目的尽量让动画完成显示, 即使没有可显示的动画, 多刷新几次不会有副作用, 但如果少画了一次, 屏幕上就可能留下不正确画面了。
boolean isAnimating() {
return mAnimation !=
null
|| (mAttachedWinAnimator !=
null &
&
mAttachedWinAnimator.mAnimation !=
null)
|| (mAppAnimator !=
null &
&
mAppAnimator.isAnimating());
}
动画生成及显示
我们再看看动画的生成过程, WindowStateAnimator的prepareSurfaceLocked方法来完成计算一帧动画并显示工作:
public void prepareSurfaceLocked(final boolean recoveringMemory) {
......
computeShownFrameLocked();
//计算要显示的动画帧setSurfaceBoundariesLocked(recoveringMemory);
if (mIsWallpaper &
&
!mWin.mWallpaperVisible) {
hide();
//如果是壁纸窗口,
隐藏
} else if (w.mAttachedHidden || !w.isOnScreen()) {
hide();
//如果窗口不可见,
隐藏
......
} else if (mLastLayer !=
mAnimLayer
|| mLastAlpha !=
mShownAlpha
|| mLastDsDx !=
mDsDx
|| mLastDtDx !=
mDtDx
|| mLastDsDy !=
mDsDy
|| mLastDtDy !=
mDtDy
|| w.mLastHScale !=
w.mHScale
|| w.mLastVScale !=
w.mVScale
|| mLastHidden) {//每个值是否有变化
displayed =
true;
mLastAlpha =
mShownAlpha;
mLastLayer =
mAnimLayer;
mLastDsDx =
mDsDx;
mLastDtDx =
mDtDx;
mLastDsDy =
mDsDy;
mLastDtDy =
mDtDy;
w.mLastHScale =
w.mHScale;
w.mLastVScale =
w.mVScale;
if (mSurfaceControl !=
null) {
try {
mSurfaceAlpha =
mShownAlpha;
mSurfaceControl.setAlpha(mShownAlpha);
mSurfaceLayer =
mAnimLayer;
mSurfaceControl.setLayer(mAnimLayer);
mSurfaceControl.setMatrix(
mDsDx * w.mHScale, mDtDx * w.mVScale,
mDsDy * w.mHScale, mDtDy * w.mVScale);
if (mLastHidden &
&
mDrawState =
=
HAS_DRAWN) {
if (showSurfaceRobustlyLocked()) {//输出动画帧
mLastHidden =
false;
if (mIsWallpaper) {
mService.dispatchWallpaperVisibility(w, true);
}
mAnimator.setPendingLayoutChanges(w.getDisplayId(),
WindowManagerPolicy.FINISH_LAYOUT_REDO_ANIM);
} else {
w.mOrientationChanging =
false;
}
}
if (mSurfaceControl !=
null) {
w.mToken.hasVisible =
true;
}
} catch (RuntimeException e) {
Slog.w(TAG, "
Error updating surface in "
+
w, e);
if (!recoveringMemory) {
mService.reclaimSomeSurfaceMemoryLocked(this, "
update"
, true);
}
}
}
}
......
}
该函数先调用了computeShownFrameLocked函数计算当前需要显示的动画帧数据, mAnimLayer表示窗口的Z轴、mShownAlpha窗口透明度; mDsDx、mDtDx、mDsDy和mDtDy表示二维变换矩阵; w.mHScale w.mVScale表示窗口的缩放比例
只有计算出的数据和上一次数据不一样才会调用showSurfaceRobustlyLocked输出动画帧。
我们再来看WindowStateAnimator的prepareSurfaceLocked函数中下面一段代码, 会调用showSurfaceRobustlyLocked函数显示window, 当返回true代表成功, 会把mLastHidden为false, 代表上次没有隐藏。
if (showSurfaceRobustlyLocked()) {
mLastHidden =
false;
if (mIsWallpaper) {
mService.dispatchWallpaperVisibility(w, true);
}
// This draw means the difference between unique content and mirroring.
// Run another pass through performLayout to set mHasContent in the
// LogicalDisplay.
mAnimator.setPendingLayoutChanges(w.getDisplayId(),
WindowManagerPolicy.FINISH_LAYOUT_REDO_ANIM);
} else {
w.mOrientationChanging =
false;
}
推荐阅读
- Android编程(UDP客户端和TCP客户端)
- android获取位置location为null的问题
- 转 Android RadioButton设置选中时文字和背景颜色同时改变
- Android Studio第二十六期 - 自定义Activity中Fragment之间的传值
- unity, reduce android size
- Android中Activity处理返回结果的实现方式
- eclipse JNI项目迁移android studio相关问题
- Android Studio第二十五期 - 自定义键盘+支付输入框
- Android6.0权限组申请