通过修改density(density = dpi / 160)值,强行把所有不同尺寸分辨率的手机的宽度dp值改成一个统一的值,这样就解决了所有的适配问题这个方案侵入性很低,而且也没有涉及私有API,只是对老项目是不太友好;
如果我们想在所有设备上显示完全一致,其实是不现实的,因为屏幕高宽比不是固定的,16:9、4:3甚至其他宽高比层出不穷,宽高比不同,显示完全一致就不可能了。但是通常下,我们只需要以宽(支持上下滑动的页面)或高(不支持上下滑动的页面)一个维度去适配
通过阅读源码,我们可以得知,density 是 DisplayMetrics 中的成员变量,而 DisplayMetrics 实例通过 Resources#getDisplayMetrics 可以获得,而Resouces通过Activity或者Application的Context获得;DisplayMetrics 中和适配相关的几个变量:
布局文件中dp的转换,最终都是调用TypedValue#applyDimension(int unit, float value,DisplayMetrics metrics) (插图)来进行转换,方法中用到的DisplayMetrics正是从Resources中获得的;再看看图片的decode,BitmapFactory#decodeResourceStream方法(插图),也是通过 DisplayMetrics 中的值来计算的;因此,想要满足上述需求,我们只需要修改 DisplayMetrics 中和 dp 转换相关的变量即可;所以得到了下面适配方案:假设设计图宽度是360dp,以宽维度来适配,那么适配后的 density = 设备真实宽(单位px) / 360,接下来只需要把我们计算好的 density 在系统中修改下即可,同时在 Activity#onCreate 方法中调用下但是会有字体过小的现象,原因是在上面的适配中,我们忽略了DisplayMetrics#scaledDensity的特殊性,将DisplayMetrics#scaledDensity和DisplayMetrics#density设置为同样的值,从而某些用户在系统中修改了字体大小失效了,但是我们还不能直接用原始的scaledDensity,直接用的话可能导致某些文字超过显示区域,因此我们可以通过计算之前scaledDensity和density的比获得现在的scaledDensity;但是测试后发现另外一个问题,就是如果在系统设置中切换字体,再返回应用,字体并没有变化。于是还得监听下字体切换,调用 Application#registerComponentCallbacks 注册下onConfigurationChanged 监听即可;
可以参考https://github.com/Blankj/AndroidUtilCode,https://github.com/JessYanCoding/AndroidAutoSize这两个开源库;
1. 画笔:Skia 或者 OpenGL。我们可以用 Skia 画笔绘制 2D 图形,也可以用 OpenGL 来绘制 2D/3D 图形。正如前面所说,前者使用 CPU 绘制,后者使用 GPU 绘制。
2. 画纸:Surface。所有的元素都在 Surface 这张画纸上进行绘制和渲染。在 Android 中,Window 是 View 的容器,每个窗口都会关联一个 Surface。
而 WindowManager 则负责管理这些窗口,并且把它们的数据传递给 SurfaceFlinger。
3. 画板:Graphic Buffer。Graphic Buffer 缓冲用于应用程序图形的绘制,在 Android 4.1 之前使用的是双缓冲机制;在 Android 4.1 之后,使用的是三缓冲机制。
4. 显示:SurfaceFlinger。它将 WindowManager 提供的所有 Surface,通过硬件合成器 Hardware Composer 合成并输出到显示屏。
private long mStartFrameTime = 0;
private int mFrameCount = 0;
/**
* 单次计算FPS使用160毫秒
*/
private static final long MONITOR_INTERVAL = 160L;
private static final long MONITOR_INTERVAL_NANOS = MONITOR_INTERVAL * 1000L * 1000L;
/**
* 设置计算fps的单位时间间隔1000ms,即fps/s
*/
private static final long MAX_INTERVAL = 1000L;
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private void getFPS() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
return;
}
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
if (mStartFrameTime == 0) {
mStartFrameTime = frameTimeNanos;
}
long interval = frameTimeNanos - mStartFrameTime;
if (interval > MONITOR_INTERVAL_NANOS) {
double fps = (((double) (mFrameCount * 1000L * 1000L)) / interval) * MAX_INTERVAL;
// log输出fps
LogUtils.i("当前实时fps值为: " + fps);
mFrameCount = 0;
mStartFrameTime = 0;
} else {
++mFrameCount;
}
Choreographer.getInstance().postFrameCallback(this);
}
});
}
@Around("execution(* android.app.Activity.setContentView(..))")
public void getSetContentViewTime(ProceedingJoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String name = signature.toShortString();
long time = System.currentTimeMillis();
try {
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
LogHelper.i(name + " cost " + (System.currentTimeMillis() - time));
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
// 使用LayoutInflaterCompat.Factory2全局监控Activity界面每一个控件的加载耗时,
// 也可以做全局的自定义控件替换处理,比如:将TextView全局替换为自定义的TextView。
LayoutInflaterCompat.setFactory2(getLayoutInflater(), new LayoutInflater.Factory2() {
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
if (TextUtils.equals(name, "TextView")) {
// 生成自定义TextView
}
long time = System.currentTimeMillis();
// 1
View view = getDelegate().createView(parent, name, context, attrs);
LogHelper.i(name + " cost " + (System.currentTimeMillis() - time));
return view;
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
});
// 2、setFactory2方法需在super.onCreate方法前调用,否则无效
super.onCreate(savedInstanceState);
setContentView(getLayoutId());
unBinder = ButterKnife.bind(this);
mActivity = this;
ActivityCollector.getInstance().addActivity(this);
onViewCreated();
initToolbar();
initEventAndData();
}
Button button=new Button(this);
button.setBackgroundColor(Color.RED);
button.setText("Hello World");
ViewGroup viewGroup = (ViewGroup) LayoutInflater.from(this).inflate(R.layout.activity_main, null);
viewGroup.addView(button);
public static boolean prepareLooperWithMainThreadQueue(boolean reset) {
if (isMainThread()) {
return true;
} else {
ThreadLocal threadLocal = (ThreadLocal) ReflectionHelper.getStaticFieldValue(Looper.class, "sThreadLocal");
if (threadLocal == null) {
return false;
} else {
Looper looper = null;
if (!reset) {
Looper.prepare();
looper = Looper.myLooper();
Object queue = ReflectionHelper.invokeMethod(Looper.getMainLooper(), "getQUeue", new Class[0], new Object[0]);
if (!(queue instanceof MessageQueue)) {
return false;
}
}
ReflectionHelper.invokeMethod(threadLocal, "set", new Class[]{Object.class}, new Object[]{looper});
return true;
}
}
}
// 要注意的是,在创建完 View 后我们需要把线程的 Looper 恢复成原来的。
private static boolean isMainThread() {
return Looper.myLooper() == Looper.getMainLooper();
}
implementation 'com.android.support:asynclayoutinflater:28.0.0'
// 内部分别使用了IO和反射的方式去加载布局解析器和创建对应的View
// setContentView(R.layout.activity_main);
// 使用AsyncLayoutInflater进行布局的加载
new AsyncLayoutInflater(MainActivity.this).inflate(R.layout.activity_main, null, new AsyncLayoutInflater.OnInflateFinishedListener() {
@Override
public void onInflateFinished(@NonNull View view, int i, @Nullable ViewGroup viewGroup) {
setContentView(view);
// findViewById、视图操作等
}
});
super.onCreate(savedInstanceState);
/**
* 实现异步加载布局的功能,修改点:
*
* 1. super.onCreate之前调用没有了默认的Factory;
* 2. 排队过多的优化;
*/
public class AsyncLayoutInflaterPlus {
private static final String TAG = "AsyncLayoutInflaterPlus";
private Handler mHandler;
private LayoutInflater mInflater;
private InflateRunnable mInflateRunnable;
// 真正执行加载任务的线程池
private static ExecutorService sExecutor = Executors.newFixedThreadPool(Math.max(2,
Runtime.getRuntime().availableProcessors() - 2));
// InflateRequest pool
private static Pools.SynchronizedPool sRequestPool = new Pools.SynchronizedPool<>(10);
private Future> future;
public AsyncLayoutInflaterPlus(@NonNull Context context) {
mInflater = new AsyncLayoutInflaterPlus.BasicInflater(context);
mHandler = new Handler(mHandlerCallback);
}
@UiThread
public void inflate(@LayoutRes int resid, @Nullable ViewGroup parent, @NonNull CountDownLatch countDownLatch,
@NonNull AsyncLayoutInflaterPlus.OnInflateFinishedListener callback) {
if (callback == null) {
throw new NullPointerException("callback argument may not be null!");
}
AsyncLayoutInflaterPlus.InflateRequest request = obtainRequest();
request.inflater = this;
request.resid = resid;
request.parent = parent;
request.callback = callback;
request.countDownLatch = countDownLatch;
mInflateRunnable = new InflateRunnable(request);
future = sExecutor.submit(mInflateRunnable);
}
public void cancel() {
future.cancel(true);
}
/**
* 判断这个任务是否已经开始执行
*
* @return
*/
public boolean isRunning() {
return mInflateRunnable.isRunning();
}
private Handler.Callback mHandlerCallback = new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
AsyncLayoutInflaterPlus.InflateRequest request = (AsyncLayoutInflaterPlus.InflateRequest) msg.obj;
if (request.view == null) {
request.view = mInflater.inflate(
request.resid, request.parent, false);
}
request.callback.onInflateFinished(
request.view, request.resid, request.parent);
request.countDownLatch.countDown();
releaseRequest(request);
return true;
}
};
public interface OnInflateFinishedListener {
void onInflateFinished(View view, int resid, ViewGroup parent);
}
private class InflateRunnable implements Runnable {
private InflateRequest request;
private boolean isRunning;
public InflateRunnable(InflateRequest request) {
this.request = request;
}
@Override
public void run() {
isRunning = true;
try {
request.view = request.inflater.mInflater.inflate(
request.resid, request.parent, false);
} catch (RuntimeException ex) {
// Probably a Looper failure, retry on the UI thread
Log.w(TAG, "Failed to inflate resource in the background! Retrying on the UI"
+ " thread", ex);
}
Message.obtain(request.inflater.mHandler, 0, request)
.sendToTarget();
}
public boolean isRunning() {
return isRunning;
}
}
private static class InflateRequest {
AsyncLayoutInflaterPlus inflater;
ViewGroup parent;
int resid;
View view;
AsyncLayoutInflaterPlus.OnInflateFinishedListener callback;
CountDownLatch countDownLatch;
InflateRequest() {
}
}
private static class BasicInflater extends LayoutInflater {
private static final String[] sClassPrefixList = {
"android.widget.",
"android.webkit.",
"android.app."
};
BasicInflater(Context context) {
super(context);
if (context instanceof AppCompatActivity) {
// 加上这些可以保证AppCompatActivity的情况下,super.onCreate之前
// 使用AsyncLayoutInflater加载的布局也拥有默认的效果
AppCompatDelegate appCompatDelegate = ((AppCompatActivity) context).getDelegate();
if (appCompatDelegate instanceof LayoutInflater.Factory2) {
LayoutInflaterCompat.setFactory2(this, (LayoutInflater.Factory2) appCompatDelegate);
}
}
}
@Override
public LayoutInflater cloneInContext(Context newContext) {
return new AsyncLayoutInflaterPlus.BasicInflater(newContext);
}
@Override
protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
for (String prefix : sClassPrefixList) {
try {
View view = createView(name, prefix, attrs);
if (view != null) {
return view;
}
} catch (ClassNotFoundException e) {
// In this case we want to let the base class take a crack
// at it.
}
}
return super.onCreateView(name, attrs);
}
}
public AsyncLayoutInflaterPlus.InflateRequest obtainRequest() {
AsyncLayoutInflaterPlus.InflateRequest obj = sRequestPool.acquire();
if (obj == null) {
obj = new AsyncLayoutInflaterPlus.InflateRequest();
}
return obj;
}
public void releaseRequest(AsyncLayoutInflaterPlus.InflateRequest obj) {
obj.callback = null;
obj.inflater = null;
obj.parent = null;
obj.resid = 0;
obj.view = null;
sRequestPool.release(obj);
}
}
1. 配置Litho的相关依赖
// 项目下
repositories {
jcenter()
}
// module下
dependencies {
// ...
// Litho
implementation 'com.facebook.litho:litho-core:0.33.0'
implementation 'com.facebook.litho:litho-widget:0.33.0'
annotationProcessor 'com.facebook.litho:litho-processor:0.33.0'
// SoLoader
implementation 'com.facebook.soloader:soloader:0.5.1'
// For integration with Fresco
implementation 'com.facebook.litho:litho-fresco:0.33.0'
// For testing
testImplementation 'com.facebook.litho:litho-testing:0.33.0'
// Sections (options,用来声明去构建一个list)
implementation 'com.facebook.litho:litho-sections-core:0.33.0'
implementation 'com.facebook.litho:litho-sections-widget:0.33.0'
compileOnly 'com.facebook.litho:litho-sections-annotations:0.33.0'
annotationProcessor 'com.facebook.litho:litho-sections-processor:0.33.0'
}
2. Application下的onCreate方法中初始化SoLoader:
@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, false);
//Litho使用了Yoga进行布局,而Yoga包含有native依赖,在Soloader.init方法中对这些native依赖进行了加载。
}
3. 在Activity的onCreate方法中添加如下代码即可显示单个的文本视图:
// 1、将Activity的Context对象保存到ComponentContext中,并同时初始化
// 一个资源解析者实例ResourceResolver供其余组件使用。
ComponentContext componentContext = new ComponentContext(this);
// 2、Text内部使用建造者模式以实现组件属性的链式调用,下面设置的text、
// TextColor等属性在Litho中被称为Prop,此概念引申字React。
Text lithoText = Text.create(componentContext)
.text("Litho text")
.textSizeDip(64)
.textColor(ContextCompat.getColor(this, R.color.light_deep_red))
.build();
// 3、设置一个LithoView去展示Text组件:LithoView.create内部新建了一个
// LithoView实例,并用给定的Component(lithoText)进行初始化
setContentView(LithoView.create(componentContext, lithoText));
4. 使用自定义Component
Litho中的视图单元叫做Component,即组件,它的设计理念来源于React组件化的思想。
每个组件持有描述一个视图单元所必须的属性与状态,用于视图布局的计算工作。
视图最终的绘制工作是由组件指定的绘制单元(View或Drawable)来完成的。
@LayoutSpec
public class ListItemSpec {
@OnCreateLayout
static Component onCreateLayout(ComponentContext context) {
// Column的作用类似于HTML中的标签
return Column.create(context)
.paddingDip(YogaEdge.ALL, 16)
.backgroundColor(Color.WHITE)
.child(Text.create(context)
.text("Litho Study")
.textSizeSp(36)
.textColor(Color.BLUE)
.build())
.child(Text.create(context)
.text("JsonChao")
.textSizeSp(24)
.textColor(Color.MAGENTA)
.build())
.build();
}
}
// 2、构建ListItem组件
ListItem listItem = ListItem.create(componentContext).build();
- Flutter:自己的布局 + 渲染引擎
- RenderThread 与 RenderScript
- Android 5.0,系统增加了 RenderThread,对于 ViewPropertyAnimator 和 CircularReveal 动画,我们可以使用RenderThead 实现动画的异步渲染。
参考文章
- Android开发高手课-UI 优化(上):UI 渲染的几个关键概念
- Android开发高手课-UI 优化(下):如何优化 UI 渲染?
- Android 目前稳定高效的UI适配方案
- 一种极低成本的Android屏幕适配方式
- Android 屏幕适配终结者
- 骚年你的屏幕适配方式该升级了!-今日头条适配方案
- 深入探索Android布局优化
- Vulkan - 高性能渲染
- Android Project Butter分析
- Android 屏幕绘制机制及硬件加速
- Litho
- Yoga
- RenderThread:异步渲染动画
- RenderScript渲染利器
- RenderScript :简单而快速的图像处理
- Android AsyncLayoutInflater 限制及改进