1.需求描述,需要适配市面正常尺寸的手机,一般是5.5左右,当然新出的刘海屏,曲面屏,全面屏等等很多都大于5.5的;这中类型的目前还没有测试机,所以没看到过效果,除了5.5尺寸,还有7.5工业板常见尺寸屏幕,所谓的9.5超大屏商务板,可能你们没有啥概念,上图:
哭诉:理论上一个app遇到尺寸差异这么大的就要重新设计一套UI分别使用了,无奈时间不允许,只能这样将就。
解决方法:
- 鸿神 的 AndroidAutoLayout,这个现在已经没那么多人用了
- 今日头条的适配方案,正火。
- SmallestWidth 限定符适配方案,正火。
最后我采用的是[JessYan]的基于今日头条适配方案的AndroidAutoSize。这个代码侵入程度接近于0啊,惊叹,具体的使用与实现看作者的原著即可,地址是:https://www.jianshu.com/p/4aa23d69d481
用法上面的链接都有,但是他的框架是再AndroidManifest设置一个固定的比例来实现的,比如5.0尺寸左右的用这个比例375:667,这个比例就是苹果的750:1334,4.7英寸的苹果的6,6s,7,8的都是这个比例,这个比率放到android的5.0-5.5的也都合适,而且app的设计图只给了一份,就是基于这个尺寸的,所以一开始配置是这样的:
build.gradle的配置
dependencies {
...
// 基于今日头条适配方案的扩展框架:https://www.jianshu.com/p/4aa23d69d481
implementation 'me.jessyan:autosize:1.0.5'
...
}
AndroidManifest.xml的配置
这个设置在5.0-5.5还是比较能接受的
但是放到平板上一看,惨不忍睹,看看效果图:
SO,5.5,7.5和9.5由于尺寸差别太大是不能用一个比例来通配的,但是在AndroidManifest.xml里面并不能通过获取手机尺寸来设置,但是作者已经考虑到类似的需求了,可以通过接口来动态设置比例,实现CustomAdapt这个接口就可以在getSizeInDp() {}里面设置了, 所以我最后的做法是:尺寸差别太大的手机和平板动态获取尺寸,根据尺寸设置宽度适配或者高度适配的数值。
代码如下:
package com.dasudian.dsd.utils.app;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.os.Build;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.View;
import android.view.WindowManager;
import com.dasudian.dsd.DsdApplication;
import java.math.BigDecimal;
public class ScreenUtils {
private ScreenUtils()
{
/* cannot be instantiated */
throw new UnsupportedOperationException("cannot be instantiated");
}
/**
* 获得屏幕宽度
*
* @return
*/
public static int getScreenWidth()
{
WindowManager wm = (WindowManager) DsdApplication.getContext()
.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics outMetrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(outMetrics);
return outMetrics.widthPixels;
}
/**
* 获得屏幕高度
*
* @return
*/
public static int getScreenHeight()
{
WindowManager wm = (WindowManager) DsdApplication.getContext().getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics outMetrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(outMetrics);
return outMetrics.heightPixels;
}
/**
* 获得状态栏的高度
*
* @param context
* @return
*/
public static int getStatusHeight(Context context)
{
int statusHeight = -1;
try
{
Class> clazz = Class.forName("com.android.internal.R$dimen.xml");
Object object = clazz.newInstance();
int height = Integer.parseInt(clazz.getField("status_bar_height")
.get(object).toString());
statusHeight = context.getApplicationContext().getResources().getDimensionPixelSize(height);
} catch (Exception e)
{
e.printStackTrace();
}
return statusHeight;
}
/**
* 获取当前屏幕截图,包含状态栏
*
* @param activity
* @return
*/
public static Bitmap snapShotWithStatusBar(Activity activity)
{
View view = activity.getWindow().getDecorView();
view.setDrawingCacheEnabled(true);
view.buildDrawingCache();
Bitmap bmp = view.getDrawingCache();
int width = getScreenWidth();
int height = getScreenHeight();
Bitmap bp = null;
bp = Bitmap.createBitmap(bmp, 0, 0, width, height);
view.destroyDrawingCache();
return bp;
}
/**
* 获取当前屏幕截图,不包含状态栏
*
* @param activity
* @return
*/
public static Bitmap snapShotWithoutStatusBar(Activity activity)
{
View view = activity.getWindow().getDecorView();
view.setDrawingCacheEnabled(true);
view.buildDrawingCache();
Bitmap bmp = view.getDrawingCache();
Rect frame = new Rect();
activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(frame);
int statusBarHeight = frame.top;
int width = getScreenWidth();
int height = getScreenHeight();
Bitmap bp = null;
bp = Bitmap.createBitmap(bmp, 0, statusBarHeight, width, height
- statusBarHeight);
view.destroyDrawingCache();
return bp;
}
/**
* 获取当前屏幕的尺寸大小
* @return
*/
public static double getPingMuSize() {
try {
WindowManager wm = (WindowManager) DsdApplication.getContext().getSystemService(Context.WINDOW_SERVICE);
Display display = wm.getDefaultDisplay();
DisplayMetrics dm = new DisplayMetrics();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
display.getRealMetrics(dm);
}else {
display.getMetrics(dm);
}
double x = Math.pow(dm.widthPixels / dm.xdpi, 2);
double y = Math.pow(dm.heightPixels / dm.ydpi, 2);
// 屏幕尺寸
BigDecimal decimal = new BigDecimal(Math.sqrt(x + y));
decimal = decimal.setScale(1,BigDecimal.ROUND_UP);
double mScreenInches = decimal.doubleValue();
return mScreenInches;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 根据手机尺寸设置 适配框架 对应的宽,这里默认是拿宽,之前是在AndroidManifest的里面配置的,发现平板和手机因为尺寸差别太大无法只设置一个宽.
* Nexus 5x Api28--->当前手机尺寸为:5.3
* 其它信息:DisplayMetrics{density=2.625, width=1080, height=1794, scaledDensity=2.625, xdpi=420.0, ydpi=420.0}
* 375:667
*
* Raindi ITAB-01 Api22--->当前手机尺寸为:7.5
* 其它信息:DisplayMetrics{density=1.0, width=600, height=976, scaledDensity=1.0, xdpi=160.0, ydpi=160.0}
* 482 820
*
* KTE X20 Api26--->9.5
* 其它信息:DisplayMetrics{density=2.0, width=1600, height=2464, scaledDensity=2.0, xdpi=320.0, ydpi=320.0}
*
* 580 960
* @return 返回不同尺寸终端适应的宽,注意,你们自己需要匹配的平板的数值自己去尝试,这里只是参考。
*/
public static int getAutoSizeWidth() {
// 580是9.5寸的商务数据终端的适配值.
int width = 580;
try {
double size = getPingMuSize();
if(size < 7) {
width = 375; // height = 667
} else if (size >= 7 && size < 8){
width = 482; // height = 820
} else {
width = 580; // height = 960
}
} catch (Exception e) {
e.printStackTrace();
} finally {
return width;
}
}
/**
* 获取当前屏幕的尺寸大小
* @return
*/
public static DisplayMetrics getMetrics() {
DisplayMetrics metrics = new DisplayMetrics();
WindowManager manager = (WindowManager) DsdApplication.getContext().getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
manager.getDefaultDisplay().getMetrics(metrics);
return metrics;
}
}
一.上面的实现方法放在BaseActivity里面调用,建议你的的APP所有的类都要继承于BaseActivity/MvpBaseActivity,如果不继承于基类,则自己手动实现implements CustomAdapt接口也可以,然后重写他的方法,如果重写了方法设置了对应的宽或者高得数值就能顺利适配啦,太方便了8,代码如下:
package com.dasudian.dsd.mvp.base;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.dasudian.dsd.R;
import com.dasudian.dsd.utils.app.IntentUtil;
import com.dasudian.dsd.utils.app.ScreenUtils;
import com.dasudian.dsd.utils.stack.StackManager;
import com.dasudian.dsd.widget.NavigationBar;
import me.jessyan.autosize.internal.CustomAdapt;
public abstract class BaseActivity> extends AppCompatActivity implements CustomAdapt {
protected T mPresenter;
protected NavigationBar mNavigationBar;
private StackManager mStackManager;
private AlertDialog progressDialog;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);//竖屏
// 不是所有的界面都需要实现mvp , so允许为空
if (createPresenter() != null) {
mPresenter = createPresenter();
mPresenter.attachView((V) this);
}
}
// createPresenter
protected abstract T createPresenter();
//用于引入布局文件
abstract protected int provideContentViewId();
/**
* 规定按照宽度适配
* @return
*/
@Override
public boolean isBaseOnWidth() {
return true;
}
/**
* 设置适配的宽度或者高度
* @return
*/
@Override
public float getSizeInDp() {
return ScreenUtils.getAutoSizeWidth();
}
protected void openActivity(Class> cls) {
Intent intent = new Intent(this, cls);
startActivity(intent);
}
public void openActivityForResult(Class> cls, int requestCode) {
openActivity(cls, null, requestCode);
}
public void openActivity(Class> cls, Bundle bundle, int requestCode) {
Intent intent = new Intent(this, cls);
if (bundle != null) {
intent.putExtras(bundle);
}
if (requestCode == 0) {
IntentUtil.startPreviewActivity(this, intent, 0);
} else {
IntentUtil.startPreviewActivity(this, intent, requestCode);
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mPresenter != null) {
mPresenter.detachView();
}
}
public TextView showProgressDialog() {
return showProgressDialog(false);
}
public StackManager getStackManager() {
return mStackManager.getStackManager();
}
public void closeProgressDialog() {
try {
if (progressDialog != null) {
progressDialog.dismiss();
}
} catch (Exception e) {
e.printStackTrace();
}
}
public TextView showProgressDialog(boolean isCanCancel) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setCancelable(false);
View view = View.inflate(this, R.layout.dialog_loading, null);
builder.setView(view);
ProgressBar pb_loading = (ProgressBar) view.findViewById(R.id.pb_loading);
TextView tv_hint = (TextView) view.findViewById(R.id.tv_loading_hint);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
pb_loading.setIndeterminateTintList(ContextCompat.getColorStateList(this, R.color.colorPrimaryDark));
}
tv_hint.setText("正在加载中...");
progressDialog = builder.create();
progressDialog.show();
return tv_hint;
}
}
适配完之后的效果:
这个效果和上面虽然也不完美,但是也还可以接受是不是???,反正比没适配之前的好多啦,而且5.0尺寸和9.5尺寸差距了一倍,这个程度我是可以接受了哈哈O_O
二:第三方库里面的类也要适配,例如某些图片选择插件,打开相册选择那一块的Activity也不是我们写的,如果没适配会非常不协调,类似这种,要在Application里面提前适配,代码如下:
/**
* 适配手机平板,由于框架适配的取值有多种情况,一是早于Application就从AndroidManifest.xml的取值进行初始化,第二种是初始化Activity的时候通过回调接口重新赋值(本项目就是在基类进行的初始化);第三种是下面的提前声明第三方库的某个Activity需要适配的值
* 本项目使用第二中和第三种方法一起进行适配平板和手机.
* {@link ScreenUtils#getAutoSizeWidth()}
* 注意:如果用了这个框架的代码进行适配,必须在AndroidManifest.xml的中设置初始值.
*/
private void initAdaptivePhoneAndPad() {
try {
// 获取尺寸
double size = ScreenUtils.getPingMuSize();
LogUtil.e("手机尺寸为:" + size);
LogUtil.e("手机其它信息:" + ScreenUtils.getMetrics().toString());
// 适配第三方图片库的MatisseActivity类,不然这个会很小,其它的第三方类也需要在这里申明,具体参考:https://github.com/JessYanCoding/AndroidAutoSize/blob/master/demo/src/main/java/me/jessyan/autosize/demo/BaseApplication.java#L94
AutoSizeConfig.getInstance().getExternalAdaptManager().addExternalAdaptInfoOfActivity(MatisseActivity.class, new ExternalAdaptInfo(true, ScreenUtils.getAutoSizeWidth()));
AutoSizeConfig.getInstance().getExternalAdaptManager().addExternalAdaptInfoOfActivity(ImageCropActivity.class, new ExternalAdaptInfo(true, ScreenUtils.getAutoSizeWidth()));
AutoSizeConfig.getInstance().getExternalAdaptManager().addExternalAdaptInfoOfActivity(NotificationActivity.class, new ExternalAdaptInfo(true, ScreenUtils.getAutoSizeWidth()));
AutoSizeConfig.getInstance().getExternalAdaptManager().addExternalAdaptInfoOfActivity(MessageActivity.class, new ExternalAdaptInfo(true, ScreenUtils.getAutoSizeWidth()));
} catch (Exception e) {
e.printStackTrace();
}
}
到此处截至,AndroidAutoSize框架用来适配的部分就完成了。
三:由于尺寸相差这么大,还会有其它适配问题,例如设计师设计了一个图标在距顶部40%的地方,设计图上标记距离高度是100dp,如果我们设死marginTop = 100dp,那在9.5尺寸的平板上差距实际只是22%,**看如下数据:
5.3尺寸的手机:DisplayMetrics{density=2.625, width=1080, height=1794, scaledDensity=2.625, xdpi=420.0, ydpi=420.0}
1794px 2.625dpi 683.428571dp
设计图274dp / 683.428571 = 0.4011713
9.5尺寸的手机:DisplayMetrics{density=2.0, width=1600, height=2464, scaledDensity=2.0, xdpi=320.0, ydpi=320.0}
2464px 2.0dpi 1232dp
274dp / 1232 = 0.2224026
所以在视觉效果上差距会很大,这种情况有两种解决方法,一种是使用android.support.constraint.ConstraintLayout布局,动态拖拽,设置距离顶部的bias值,例如下面的代码就是让imageView_icon距离顶部0.1的间距。
例如这个布局:
但是如果是比较复杂的布局,拖拽也挺麻烦的,所以在复杂的布局我用的是在代码里动态获取总宽高,然后获取10%的高度,再转成对应的dp值,然后设置。
动态设置margin:
public static void setMargin(View view, int left, int top, int right, int bottom) {
int scaledLeft = scaleValue(view.getContext(), left);
int scaledTop = scaleValue(view.getContext(), top);
int scaledRight = scaleValue(view.getContext(), right);
int scaledBottom = scaleValue(view.getContext(), bottom);
if ((view.getLayoutParams() instanceof ViewGroup.MarginLayoutParams)) {
ViewGroup.MarginLayoutParams mMarginLayoutParams = (ViewGroup.MarginLayoutParams) view
.getLayoutParams();
if (mMarginLayoutParams != null) {
if (left != -2147483648) {
mMarginLayoutParams.leftMargin = scaledLeft;
}
if (right != -2147483648) {
mMarginLayoutParams.rightMargin = scaledRight;
}
if (top != -2147483648) {
mMarginLayoutParams.topMargin = scaledTop;
}
if (bottom != -2147483648) {
mMarginLayoutParams.bottomMargin = scaledBottom;
}
view.setLayoutParams(mMarginLayoutParams);
}
}
}
四,例如设计师设计了图标占屏幕宽度的一半,的地方,设计图上标记宽高是340dp,如果我们设死width=height= 340dp,那原本占宽度50%的图片宽高在9.5尺寸的平板上只是占了22.78%,看如下数据:
5.3尺寸的手机:DisplayMetrics{density=2.625, width=1080, height=1794, scaledDensity=2.625, xdpi=420.0, ydpi=420.0}
1794px 2.625dpi 683.428571dp
设计图340dp / 683.428571 ≈ 0.5,
9.5尺寸的手机:DisplayMetrics{density=2.0, width=1600, height=2464, scaledDensity=2.0, xdpi=320.0, ydpi=320.0}
2464px 2.0dpi 1232dp
340dp/ 1232 = 0.2759
,所注意这种情况下我们还是要按照百分百来设置这个图片的宽高。
PercentImageView.java
package com.dasudian.dsd.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import com.dasudian.dsd.R;
import com.dasudian.dsd.utils.app.ScreenUtils;
/**
* 可设置百分百高度的图片
*/
public class PercentImageView extends android.support.v7.widget.AppCompatImageView {
private float widthPer;
private float heightPer;
public PercentImageView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public PercentImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs);
}
private void init(AttributeSet attrs) {
TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.percent_imageview);
widthPer = ta.getFloat(R.styleable.percent_imageview_widthPer, 0);
heightPer = ta.getFloat(R.styleable.percent_imageview_heightPer, 0);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), getDefaultSize(0, heightMeasureSpec));
int widthSize = (int) (ScreenUtils.getScreenWidth() * widthPer);
int heightSize = (int) (ScreenUtils.getScreenHeight() * heightPer);
// 用户设置[0,1]区间以外的值都无效,都是采用ImageView默认的设置。
if(widthPer > 0 && widthPer < 1) {
widthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
}
if(heightPer > 0 && heightPer < 1) {
heightMeasureSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
attrs.xml中配置自定义参数:
xml中使用百分百高度的图片
项目用到的适配方面大体就这些了,还有LinearLayout等比,多套设计图那些就不说了,反正适配要见鸡行事,混着用才能完美,大家执生,撇。