android日记(五)
上一篇:android日记(四)
1.java自旋锁
- 自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
2.java可以new一个接口对象吗?
- 先上结论,不可以new interface和abstract class。
- 接口只是定义了一个类的标准,供实体类去实现。
- 那下面的代码中,能对接口OnClickListener执行new呢?
view.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //todo } });
/** * Interface definition for a callback to be invoked when a view is clicked. */ public interface OnClickListener { /** * Called when a view has been clicked. * * @param v The view that was clicked. */ void onClick(View v); }
-
这个与new OnClickListener(); 根本的区别在于后面的大括号{},这个表示new了一个实现OnClickListener接口的匿名类。相当于,
class MyClickListener implements View.OnClickListener{ @Override public void onClick(View v) { } } view.setOnClickListener(new MyClickListener());
3.基于CAS操作的乐观锁机制
- Java多线程通信本质是内存共享,线程安全要保证的三个点:可见行、有序性、原子性。
- synchronized实现同步的机制在于,无论哪个线程持有共享资源的锁,其他线程就不能再访问该共享资源,是一种独占锁,独占锁是悲观锁的一种形式。
- volatile与锁相比,是一种更轻量的同步机制,它不需要线程调度和上下文的切换。但是volatile不能保证原子性,从而不能真正保证同步。
- CAS (compare and swap,比较和交换),可以在无锁的情况下实现同步。JDK java.util.concurrent.atomic包中提供的原子操作类,这些类的compareAndSet操作可以天然保证同步性。
AtomicObject.compareAndSet(AtomicObject expect, AtomicObject update)
方法的含义,原子对象与expect相比:1)如果相同,方法返回true,并将对象更新为update;2)如果不同,方法返回false。整个操作是原子的。
- 以AtomaicBoolean为例,比如某个sdk的初始化可能在多个线程中进行,但是又不允许重复初始化,那么就可以设置一个AtomicBoolean对象,作为共享资源,借助compareAndSet()的返回值,实现初始化操作的同步。
AtomicBoolean hasInited = new AtomicBoolean(false);
public void init() {
if (hasInited.compareAndSet(false, true)) {
Log.d(tag, "进行初始化");
//to do really init process
} else {
Log.d(tag, "已经初始化过");
}
} - 乐观锁的概念,先假设不会又线程冲突,只有当对共享资源进行更新操作的时候,才去检查是否存在线程冲突。如果存在冲突,就重新检查,直到无冲突后完成更新操作。
- 通过CAS操作,就可以实现乐观锁。对共享资源,不加锁,只是引入一个版本号的字段,通常用AtomicInteger来标记。当多个线程都执行读资源时,是没有任何问题的;当多个线程执行写资源时,会首先执行version.getAndInCrement(),当返回true时,说明没有冲突,正常更新资源,并把版本号加1,当返回false时,就循环检查,直到返回true。
- CAS的ABA问题,如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。但是通常情况下,在乐观锁应用场景中,变量每次更新时对应的版本号只会加1,不会减1,从而避免了ABA问题。另外通过对比时间戳的方式,也可以避免ABA的问题。
- 乐观锁适用于读操作多于写操作的场景,悲观锁适用于写操作多于读操作的场景。
4.实现一个拖动控件
- 实现拖动控件的核心在于拦截Touch事件,然后对MotionEvent.ACTION_MOVE事件做出响应,让view本身做出响应的位移变化。
- 实际应用场景中,为了简单,可以自定义一个处理MOVE事件的ViewGroup,以期实现添加到该ViewGroup中的view自动具有了拖动能力。
- 自定义一个ViewGroup,命名为DragLayout
public DragClickLayout(Context context, AttributeSet attrs) { super(context, attrs); touchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); }
- 拦截MOVE事件,并触发外部回调,getScaledTouchSlop是一个距离,表示滑动的时候,手的移动要大于这个距离才开始移动控件。
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { int action = ev.getAction(); if (action == MotionEvent.ACTION_DOWN) { downY = ev.getRawY(); downX = ev.getRawX(); if (onDragListener != null) onDragListener.onDragStarted(); return false; } else if (action == MotionEvent.ACTION_MOVE) { movedX = ev.getRawX() - downX; movedY = ev.getRawY() - downY; if (Math.abs(movedY) < touchSlop && Math.abs(movedX) < touchSlop) return false; if (onDragListener != null) onDragListener.onDrag(movedX, movedY); return true; } return super.onInterceptTouchEvent(ev); }
外部回调接口,
public interface OnDragListener { void onDragStarted(); void onDrag(float movedX, float movedY); } private OnDragListener onDragListener; public void setOnDragListener(OnDragListener listener) { this.onDragListener = listener; }
- 自定义的拖动控件布局写法,套在DragLayout中即可
<?xml version="1.0" encoding="utf-8"?> <DragLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical">
<!-- 需要拖动的控件 --> <ImageView android:id="@+id/btn_toggle" android:layout_width="45dp" android:layout_height="45dp" android:scaleType="centerCrop" android:src="@drawable/icon" /> </DragLayout> - 在回调中,设置控件的translationX和translationY,完成拖动。
private void initView() { rootView = View.inflate(mContext, R.layout.drag_view, null); ((DragLayout) rootView).setOnDragListener(new DragLayout.OnDragListener() { float paramX, paramY; @Override public void onDragStarted() { paramX = rootView.getTranslationX(); paramY = rootView.getTranslationY(); } @Override public void onDrag(float movedX, float movedY) { if (rootView.getLeft() + paramX + movedX >= 0 && rootView.getRight() + paramX + movedX <= decorView.getWidth()) rootView.setTranslationX(paramX + movedX); if (rootView.getTop() + paramY + movedY >= 0 && rootView.getBottom() + paramY + movedY <= decorView.getHeight()) rootView.setTranslationY(paramY + movedY); } }); }
5.关于Fragment的“秘书”mHost
- 一切还要从FragmentActivity中的mFragments变量说起,在FragmentActivity创建时,就会通过下面的代码创建一个FragmentController对象,
final FragmentController mFragments = FragmentController.createController(new HostCallbacks());
creatController()创建mFragments前,先创建了一个HostCallBack对象,并将其传入到FragmentController的构造函数中,
@NonNull public static FragmentController createController(@NonNull FragmentHostCallback<?> callbacks) { return new FragmentController(checkNotNull(callbacks, "callbacks == null")); }
并在构造函数中,对FragmentController.mHost变量进行了赋值。
private FragmentController(FragmentHostCallback<?> callbacks) { mHost = callbacks; }
- 可以看到HostCallbacks是抽象类FragmentHostCallbacks的实现,
public abstract class FragmentHostCallback<E> extends FragmentContainer
class HostCallbacks extends FragmentHostCallback<FragmentActivity>
HostCallbacks类是在FragmentActivity中定义的内部类。
- 明明要说的是Fragment.mHost,干嘛给我讲FragmentController.mHost。先不急,玄机马上揭晓。FragmentActivity通过fragmentManager添加fragment,
//FragmentActivity
getSupportFragmentManager() .beginTransaction() ...//FragmentActivity
public FragmentManager getSupportFragmentManager() { return mFragments.getSupportFragmentManager(); }fragmentManager从mFragments.getSupportFragmentManager()获得,mFragments是啥?不就是上面讲的FragmentActivity创建时一并创建的FragmentController对象么。继续往下看,FragmentController中又调用了自己的mHost.mFragmentManager()方法,至此,前面讲的FragmentController.mHost总算是派上用场了。
//FragmentController
public FragmentManager getSupportFragmentManager() { return mHost.mFragmentManager; } - 关键就是FragmentController.mHost.mFragmentManager,它在下面代码中被创建,
public abstract class FragmentHostCallback<E> extends FragmentContainer { @Nullable private final Activity mActivity; @NonNull private final Context mContext; @NonNull private final Handler mHandler; private final int mWindowAnimations; final FragmentManagerImpl mFragmentManager = new FragmentManagerImpl(); public FragmentHostCallback(@NonNull Context context, @NonNull Handler handler, int windowAnimations) { this(context instanceof Activity ? (Activity) context : null, context, handler, windowAnimations); } FragmentHostCallback(@NonNull FragmentActivity activity) { this(activity, activity /*context*/, new Handler(), 0 /*windowAnimations*/); } FragmentHostCallback(@Nullable Activity activity, @NonNull Context context, @NonNull Handler handler, int windowAnimations) { mActivity = activity; mContext = Preconditions.checkNotNull(context, "context == null"); mHandler = Preconditions.checkNotNull(handler, "handler == null"); mWindowAnimations = windowAnimations; } }
原来,mFragmentManager是FragmentHostCallbacks的内部变量,在FragmentHostCallbacks被创建的时候,它就一起被创建了。而mFragmentManager就是幕后大佬FragmentManagerImpl对象,它才是FragmentManager真正实现中枢,而mHost不过是给FragManagerImpl拉皮条的而已。
- 归纳上面的流程:FragmentActivity创建 -> new FragmentController()得到mFragments -> new HostCallbacks() -> new FragmentHostCallbacks() -> new FragmentManagerImpl()
- 你这不还是没有说到Fragment.mHost么,来了来了,现在正式进入整体。作为Fragment内中的变量,主要看它在什么地方赋值,
// Fragment // Host this fragment is attached to. FragmentHostCallback mHost;
一番追踪发现,在FragmentManagerImpl.onCreateView()中发现了赋值过程,也就是说Fragment中的mHost就是FragmentManagerImpl的mHost。
public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) { ... if (fragment == null) { fragment = getFragmentFactory().instantiate(context.getClassLoader(), fname); fragment.mFromLayout = true; fragment.mFragmentId = id != 0 ? id : containerId; fragment.mContainerId = containerId; fragment.mTag = tag; fragment.mInLayout = true; fragment.mFragmentManager = this; fragment.mHost = mHost; fragment.onInflate(mHost.getContext(), attrs, fragment.mSavedFragmentState); addFragment(fragment, true); } ... return fragment.mView; }
- 问题转为查看FragmentManagerImpl的mHost的赋值过程,追踪发现,在attchController()时会传入FragmentHostCallbacks初始化mHost,
//FragmentManagerImpl
public void attachController(@NonNull FragmentHostCallback host, @NonNull FragmentContainer container, @Nullable final Fragment parent) { if (mHost != null) throw new IllegalStateException("Already attached"); mHost = host; ... }而attchController()是在FragmentController.attchHost()方法触发,传入的mHost就是FragmentController自己内部的mHost,前面已经说到FragmentController的mHost是在FragmentActivity创建时一并创建的。
//FragmentController public void attachHost(@Nullable Fragment parent) { mHost.mFragmentManager.attachController( mHost, mHost /*container*/, parent); }
- 至此,可以明确Fragment中的mHost就是FragmentActivity在创建之出一并创建的内部类HostCallbacks对象。下面就剩一点疑问还没弄清楚,那就是FragmentController的attchHost()在什么时候执行。很容易找到,attchHost()方法是在FragmentActivity.onCreate()阶段执行的。
//FragmentActivity @Override protected void onCreate(@Nullable Bundle savedInstanceState) { mFragments.attachHost(null /*parent*/); }
- 总结:
- FragmentActivity在自己创建的同时,会一并创建FragmentController,并创建自己的内部HostCallbacks,给FragmentController的mHost进行初始化;
- 而HostCallbacks在创建的同时,又把自己内部的FragmentManagerImpl对象创建好了;
- 这时FragmentActivity就与FragmentManagerImpl建立了连接关系,这条连接就是getFragmentManager()的实现。
FragmentActivity.mFragments(FragmentController) -> FragmentController.mHost(FragmentHostCallbacks) -> FragmentHostCallbacks.mFragmentManager(FragmentManagerImpl)
- 在FragmentActity在onCreate()期间,会把FragmentManagerImpl的mHost进行赋值初始化;
- 等到FragmentActivity要添加Fragment时,执行getFragmentManager()获得FragmentManagerImpl对象,然后让FragmentManagerImpl完成实际操作;
- 当Fragment被添加后,在其onCreatView()期间,FragmentManaerImpl会用自己的mHost,对Fragment的mHost赋值初始化,从此Fragment就拥有了mHost。
- Fragment内部要执行什么操作时,都会找mHost,让mHost通知它的FragmentManagerImpl完成实际操作,所以把mHost叫做Fragment的“秘书”再合适不过了。
6.Fragment是如何收到onActivityResult()的
- onActivityResult常常用于activity之间的传值,在Activity1使用startActivityForResult(Intent startIntent, int requestCode)跳转到Activity2,在Activity2使用setResult(int resultCode, Intent backIntent),当Activity2返回到Activity1时,Activity1的onActivityResult()就会被调用,接收Activity2传回的数据。
- Activity中常常attch了fragment,fragment也有startActivityForResult()和onActivityResult()方法。在fragment中如果使用getActivity.startActivityForResult(),就与直接在onActivity中startActivityForResult()没有区别,activity返回时fragment的onActivityResult()方法不会被调用。如果直接使用fragment自己的startActivityForResult()方法跳转activity,这样fragment的onActivityResult()方法能直接收到回调,如此就避免了在fragment手写消息传递路径,收发消息不用绕道activity中,直接在fragment中进行。
- fragment能收到onActivityResult()的基础条件是,不能破坏它所在activity的onActivityResult()消息闭环路径。也就是如果Activity中有重写onActivityResult()方法,那么该方法中一定要有中super.onActivityResult()。例如下面的Activity重写了onActivityResult()代码,只有当设置了alwaysCallOnActivityResult=true,或者返回到activity时setResult(int resultCode, Intent data)中设置了resultCode=Activity.RESULT_OK= -1(注意,如果不设默认resultCode = Activity.CANCELED = 0),才会调用super.onActivityResult(),从而fragment才会收到onActivityResult()。
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (alwaysCallSuperOnActivityResult) { super.onActivityResult(requestCode, resultCode, data); } else { if (RESULT_OK == resultCode) { switch (requestCode) { default: super.onActivityResult(requestCode, resultCode, data); break; } } } if (mActivityShadow != null) { mActivityShadow.onActivityResult(this, requestCode, resultCode, data); } }
- 源码分析fragment是如何发起startActivityForResult()的,首先fragment发起startActivityForResult()时,会调用到FragmentActivity.startActivityForResult()
/** * Call {@link Activity#startActivityForResult(Intent, int, Bundle)} from the fragment's * containing Activity. */ public void startActivityForResult(@SuppressLint("UnknownNullness") Intent intent, int requestCode, @Nullable Bundle options) { if (mHost == null) { throw new IllegalStateException("Fragment " + this + " not attached to Activity"); } mHost.onStartActivityFromFragment(this /*fragment*/, intent, requestCode, options);//mHost就是秘书FragmentActivity.HostCallbacks }
/** * Called by Fragment.startActivityForResult() to implement its behavior. */ public void startActivityFromFragment(@NonNull Fragment fragment, @SuppressLint("UnknownNullness") Intent intent, int requestCode, @Nullable Bundle options) { mStartedActivityFromFragment = true; try { if (requestCode == -1) { ActivityCompat.startActivityForResult(this, intent, -1, options); return; } checkForValidRequestCode(requestCode);//检查requestCode是否越界,不能超过2^16 int requestIndex = allocateRequestIndex(fragment); ActivityCompat.startActivityForResult( this, intent, ((requestIndex + 1) << 16) + (requestCode & 0xffff), options); } finally { mStartedActivityFromFragment = false; } }
其中,allocateRequestIndex()方法主要是把requestIndex与mWho关联起来,mWho是fragment的唯一标识,也就是把requestIndex与fragmemt关联起来了,并返回了唯一的requestIndex。
// Allocates the next available startActivityForResult request index. private int allocateRequestIndex(@NonNull Fragment fragment) { // Sanity check that we havn't exhaused the request index space. if (mPendingFragmentActivityResults.size() >= MAX_NUM_PENDING_FRAGMENT_ACTIVITY_RESULTS) { throw new IllegalStateException("Too many pending Fragment activity results."); } // Find an unallocated request index in the mPendingFragmentActivityResults map. while (mPendingFragmentActivityResults.indexOfKey(mNextCandidateRequestIndex) >= 0) { mNextCandidateRequestIndex = (mNextCandidateRequestIndex + 1) % MAX_NUM_PENDING_FRAGMENT_ACTIVITY_RESULTS; } int requestIndex = mNextCandidateRequestIndex; mPendingFragmentActivityResults.put(requestIndex, fragment.mWho);//把requestIndex与mWho关联起来 mNextCandidateRequestIndex = (mNextCandidateRequestIndex + 1) % MAX_NUM_PENDING_FRAGMENT_ACTIVITY_RESULTS; return requestIndex; }
而CompatActivity.startActivityForResult()里面最终走到了activity.startActivityForResult(),后面的路径与直接在activity中发起startActivityForResult()一致了。
public static void startActivityForResult(@NonNull Activity activity, @NonNull Intent intent, int requestCode, @Nullable Bundle options) { if (Build.VERSION.SDK_INT >= 16) { activity.startActivityForResult(intent, requestCode, options); } else { activity.startActivityForResult(intent, requestCode); } }
-
源码分析fragment是如何收到onActivityResult()的,因为发起时,最终是通过activity.startActivityForResult()发起,因此肯定也是activity.onActivityResult()得到响应,而fragmengt能收到onActivityResult()的玄机就藏在super.onActivityResult()中。
#FragmentActivity
@Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { mFragments.noteStateNotSaved(); int requestIndex = requestCode>>16;//首先通过requestCode获取requestIndex if (requestIndex != 0) { requestIndex--; String who = mPendingFragmentActivityResults.get(requestIndex);//再根据requestIndex取出关联的who mPendingFragmentActivityResults.remove(requestIndex); if (who == null) { Log.w(TAG, "Activity result delivered for unknown Fragment."); return; } Fragment targetFragment = mFragments.findFragmentByWho(who);//再根据who,获取对应的fragment if (targetFragment == null) { Log.w(TAG, "Activity result no fragment exists for who: " + who); } else { targetFragment.onActivityResult(requestCode & 0xffff, resultCode, data);//最后调用fragment.onActivityResult() } return; } ... }查看FragmentActivity.onActivityResult()源码,发现首先根据requestCode解析requestIndex,与关联时的操作相逆,requestCode>>16 -1 = requestIndex,然后就根据requestIndex在取出关联表对应的mWho,再根据mWho得到对应的fragment,最后调用fragment.onActivityResult(),一气呵成。
7.Fragment是如何收到onRequestPermissionsResult()的
- Fragment中提供了,和Activity一样的,requestPermissions()和onRequestPermissionResult()方法,其工作原理基本上与Fragment的startActivityForResult()和onActivityResult()相同。必须依赖宿主Activity的requestPermissions()发起请求,并通过宿主Activity的onRequestPermissionResult()的接收响应,只是在对应的super.onRequestPermissionResult()方法中,会回调Fragment的onRequestPermissionResult()方法而已。
- 发起请求Fragment.requestPermissions(),Fragment只是通知它的专职秘书mHost,发起操作。
public final void requestPermissions(@NonNull String[] permissions, int requestCode) { if (mHost == null) { throw new IllegalStateException("Fragment " + this + " not attached to Activity"); } mHost.onRequestPermissionsFromFragment(this, permissions, requestCode); }
mHost就是那个在Activity诞生之际就一同创建的HostCallbacks对象,它会在Fragment.onCreatView()期间,对Fragment.mHost进行赋值初始化。查看mHost的onRequestPermissionFromFragment()方法源码可知,mHost没有自己处理请求,而是把请求传给了Activity,真正发起权限请求的是Activity。
class HostCallbacks extends FragmentHostCallback<FragmentActivity>{ ... @Override public void onRequestPermissionsFromFragment(@NonNull Fragment fragment, @NonNull String[] permissions, int requestCode) { FragmentActivity.this.requestPermissionsFromFragment(fragment, permissions, requestCode); } ... }
这就印证了,在Fragment中请求权限,最终还是由Activity实际发出请求的。
- 接收权限请求响应,很显然,系统会把响应返回给Activity的onRequestPermissionResult(),
//Activity
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); }至于Fragment.onRequestPermissionResult()回调机制就藏在,Activty接收响应的super方法中。
@Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { mFragments.noteStateNotSaved(); int index = (requestCode >> 16) & 0xffff; if (index != 0) { index--; String who = mPendingFragmentActivityResults.get(index); mPendingFragmentActivityResults.remove(index); if (who == null) { Log.w(TAG, "Activity result delivered for unknown Fragment."); return; } Fragment frag = mFragments.findFragmentByWho(who); if (frag == null) { Log.w(TAG, "Activity result no fragment exists for who: " + who); } else { frag.onRequestPermissionsResult(requestCode & 0xffff, permissions, grantResults); } } }
方法内部通过requestIndex找到fragment唯一标识mWho,然后根据mWho取出对应的Fragment,最后执行了Fragment.onRequestPermissionResult()方法。从而印证了,Fragment收到权限请求的响应,实际是宿主Activity在收到系统响应后,再转发给Fragment的。
8.基于代理Fragment打造优雅的Android权限请求工具
- 在android自6.0后,就免不了动态申请权限。如果按照传统方式,当某个业务中需要申请权限时,让其所在的Activity通过requestPermissions()发起权限,然后在Activity的onRequestpermissionsResult()中接收请求结果,然后activity把result传递给业务现场。如果业务现场在深层嵌套的fragment、adapter和自定义view中,尤其是跨组件间调用的,过长的数据输入输出路径,和大量重复的请求与接收代码,对大型商业项目而言,无疑是灾难性的。
- 即便是在基类BaseActivty和BaseFragment中提供一套专职于权限收发的接口,比如下面的BaseFragment,也只是稍稍减少了些冗余代码,对那些非Activity和Fragment的业务现场,还是无能为力。
public class BaseFragment extends Fragment { ... //检查定位权限 protected boolean checkLocationPermission() { return ActivityCompat.checkSelfPermission(mContext, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(mContext, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED; } //记录请求定位权限的bizType private String locateBizType = null; //请求定位权限 protected void requestLocationPermission() { requestLocationPermission(LocateBizType.TYPE_DEFAULT); } //请求定位权限 protected void requestLocationPermission(String bizType) { if (!TextUtils.isEmpty(locateBizType)) { //防止重复请求 return; } locateBizType = bizType; requestPermissions(new String[]{Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION}, Constants.LOCATION_REQUEST_CODE); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == Constants.LOCATION_REQUEST_CODE) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { if (checkLocationPermission()) { //检查gps权限 if (DeviceUtils.isGPSEnabled(getContext())) { requestLocationPermissionSucceed(locateBizType); } else { showOpenGPSSettingDialog(); requestLocationPermissionFailed(locateBizType, "定位失败,未开启位置信息"); } } else { CommonUtil.showToast("打开定位权限失败"); requestLocationPermissionFailed(locateBizType, "定位失败,打开定位权限失败"); } } else { CommonUtil.showToast("打开定位权限失败"); requestLocationPermissionFailed(locateBizType, "定位失败,打开定位权限失败"); } locateBizType = null; } } //请求定位权限成功 protected void requestLocationPermissionSucceed(String locateBizType) { //供子类回调 } //请求定位权限失败 protected void requestLocationPermissionFailed(String bizType, String message) { //供子类回调 } //开启gps定位服务设置弹窗 protected void showOpenGPSSettingDialog() { showAlert(getString(R.string.tips_location_closed), getString(R.string.tips_open_location), "去设置", getString(R.string.cancel), true, (dialog, i) -> { DeviceUtils.gotoLocationSettings(getContext()); dialog.dismiss(); }, (dialog, i) -> dialog.dismiss() ); } ... }
- 为了简化Activity/Fragment的onRequestPermissionResult()向业务方分发result(),自定义注解被引入。在基类中,onRequestPermissionResult()统一把result丢给处理中心PermisionGen。
public class BaseActivity { @Override public void onRequestPermissionsResult(int requestCode, @NotNull String[] permissions, @NotNull int[] grantResults) { PermissionGen.onRequestPermissionsResult(this, requestCode, permissions, grantResults); } }
处理中心,解析result,并把解析结果分为成功和失败,分别通过回调@PermissionSuccess,@PermissionFailed注解方法,传回给业务现场。
public class PermissionGen { @TargetApi(value = Build.VERSION_CODES.M) private static void requestPermissions(Object object, int requestCode, String[] permissions, PermissionRequestListener callback) { List<String> deniedPermissions = Utils.findDeniedPermissions(getActivity(object), permissions); if (deniedPermissions.size() > 0) { if (object instanceof Activity) { ActivityCompat.requestPermissions(((Activity) object), deniedPermissions.toArray(new String[0]), requestCode); } else if (object instanceof Fragment) { ((Fragment) object).requestPermissions(deniedPermissions.toArray(new String[deniedPermissions.size()]), requestCode); } } else { if (callback != null) { callback.permissionGranted(requestCode); } else { doExecuteSuccess(object, requestCode); } } } private static void doExecuteSuccess(Object activity, int requestCode) { Method executeMethod = Utils.findMethodWithRequestCode(activity.getClass(), PermissionSuccess.class, requestCode); executeMethod(activity, executeMethod); } private static void doExecuteFail(Object activity, int requestCode) { Method executeMethod = Utils.findMethodWithRequestCode(activity.getClass(), PermissionFail.class, requestCode); executeMethod(activity, executeMethod); } private static void executeMethod(Object activity, Method executeMethod) { try { if (!executeMethod.isAccessible()) executeMethod.setAccessible(true); executeMethod.invoke(activity); } catch (Exception e) { e.printStackTrace(); } } public static void onRequestPermissionsResult(Activity obj, int requestCode, String[] permissions, int[] grantResults) { List<String> deniedPermissions = new ArrayList<>(); for (int i = 0; i < grantResults.length; i++) { if (grantResults[i] != PackageManager.PERMISSION_GRANTED) { deniedPermissions.add(permissions[i]); } } if (deniedPermissions.size() > 0) { doExecuteFail(obj, requestCode); } else { doExecuteSuccess(obj, requestCode); } } }
业务现场,给出请求权限成功和失败的注解方法。
public class MainActivity extends BaseActivity { @PermissionSuccess(requestCode = 200) public void PermissionGranted() { //处理同意权限 } @PermissionFail(requestCode = 200) public void PermissionDeny() { //处理拒绝权限 } }
这种方式虽然避免了requester和宿主activity之间的层层传递,但是仍然不够,还是得依靠activity和Fragment的
onRequestPermissionsResult()
做中转,注解回调方法还是不能第在一业务现场定义,另外使用反射也带来了一定的开销问题。 - 于是开始了“偷懒”的做法,一种做法是采用代理Activity的方法,即弄一个PermissionActivity,专职于权限请求,权限的收发都在内部完成,业务方在任何地方、任何时候,只需要在向PermisionActivity发起请求的同时,开启一个回调监听,即完成了一个完整的收发请求收发过程,大大缩短了数据输入输出路径。首先,业务方直接在第一现场,向PermissioActivity发起请求,发起的同时开启回调监听。
public static void requestPermissions(Context context, IPermissionCallBack permissionCallBack, RationaleType[] types, String[] permissions) { if (context == null) { Log.w(TAG, "Can't check permissions for null context"); return; } if (checkPermissions(context, permissions)) { Log.w(TAG, "permissions has been granted"); permissionCallBack.onPermissionsGranted(true, null); return; } PermissionActivity.start(context, permissions, types, permissionCallBack); }
请求被传递到PermissionActivity内部,在其onCreate()后就在内部发出了权限请求,并在内部接收系统的响应结果,随后把结果回调给外部业务监听。
public class PermissionActivity extends FragmentActivity implements EasyPermissions.PermissionCallbacks { private IPermissionCallBack mCallBack; ... @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); EasyPermissions.requestPermissions(new PermissionRequest.Builder(this, REQUEST_CODE_PERMISSION, requestPermissions) .setRationale(rationale) .setTheme(AlertDialog.THEME_DEVICE_DEFAULT_DARK) .build()); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this); } @AfterPermissionGranted(REQUEST_CODE_PERMISSION) public void onPermissionsAllGranted() { Log.w(TAG, "onPermissionsAllGranted"); if (mCallBack != null) mCallBack.onPermissionsGranted(true, null); finish(); } @Override public void onPermissionsDenied(int requestCode, @NonNull List<String> perms) { Log.w(TAG, "onPermissionsDenied"); if (mCallBack != null) mCallBack.onPermissionsGranted(false, perms); finish(); } ... }
但这种方式的缺陷也是显而易见的,只要是申请权限,就得新开一个activity,虽然可以设置activity为透明无View的UI样式,但是一方面开销过大,一方面不可避免地要污染activity返回栈,使requester所在activity切到后台,同时也要污染requester的activity/fragment生命周期。而且一旦后台activity因为不保留活动等原因被意外销毁了,就无法收到PermissionActivity给予的回调了。 - 于是,代理Fragment就隆重登场了,与代理Activity类似,业务方在第一现场,向PermissionFragment发起权限请求,并开启回调监听。
private static void doRequest(final Activity requestHost, final String[] permissions, boolean mIsShowDialog, final PermissionHelper.PermissionCallback callback) { // 通过Fragment请求权限 PermissionHelper.PermissionInnerFragment innerFragment = new PermissionHelper.PermissionInnerFragment(); innerFragment.setPermissionCallback(callback); innerFragment.setFragmentActivity((FragmentActivity) requestHost); FragmentManager fragmentManager = ((FragmentActivity) requestHost).getSupportFragmentManager(); fragmentManager.beginTransaction() .add(innerFragment, PERMISSION_REQUEST_TAG) .commitAllowingStateLoss(); fragmentManager.executePendingTransactions(); innerFragment.requestPermissions(permissions, PERMISSION_REQUEST_CODE); for (String permission : permissions) { SharedPreferences settings = getSP(); if(settings != null){ settings.edit().putString(permission,"1").commit(); } } }
收到请求后,就会添加代理Fragment,这个Fragment是个无视图的,添加时不会指定承载fragment的container,但这丝毫不影响Fragment的生命周期。Fragment被创建好后,就发起了外部传入的请求,然后在内部接收系统返回result,并把结果回调给外部的监听。
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) isBusy.set(false) Log.w(logTag, "requestPermissions end") if (requests.indexOfKey(requestCode) < 0) { return } val request = requests[requestCode] var allGranted = false val length = grantResults.size for (i in 0 until length) { if (grantResults[i] != PackageManager.PERMISSION_GRANTED) { allGranted = false break } allGranted = true } when { allGranted -> { //点击了同意权限 if (isXiaoMiBrand()) { allGranted = doubleCheckPermissionGranted(permissions) } Log.w(logTag, "all permissions have been granted") notifyObserver(requestCode, request, allGranted, permissions.toList()) } shouldShowRationale(permissions) -> { //点击了禁止权限 notifyObserver(requestCode, request, allGranted, permissions.toList()) Log.w(logTag, "one or more permissions have been denied") } request.mShouldShowRationale -> { //点击了禁止后不再提示,本次操作后变为"禁止后不再提示"的状态,再次申请该权限时,就需要给予rationale弹窗引导 notifyObserver(requestCode, request, allGranted, permissions.toList()) Log.w(logTag, "one or more permissions have been denied") } TextUtils.isEmpty(request?.mRationale) -> { //没有设置引导文案,即便属于"禁止后不再提示"的状态,也没法弹窗引导 notifyObserver(requestCode, request, allGranted, permissions.toList()) } else -> { //申请的权限之前,已经属于"禁止后不再提示"的状态,这时候为了让用户有感知,给予弹窗rationale文案进行引导提示 Log.w(logTag, "one or more permissions have been denied with no longer prompt") try { val rationale = String.format(getString(R.string.setting_tip), request?.mRationale) val builder = AlertDialog.Builder(mContext).setTitle("").setMessage(rationale) builder.setPositiveButton(getString(R.string.ok)) { dialog, _ -> startSettingActivity() dialog.dismiss() } builder.setNegativeButton(getString(R.string.cancel)) { dialog, _ -> notifyObserver(requestCode, request, allGranted, permissions.toList()) dialog.dismiss() } builder.setCancelable(false) builder.show() } catch (e: Exception) { e.printStackTrace() } } } }
这种方式既保证了权限请求与回调都在业务第一现场发生,又避免了代理Activity对活动栈的影响。
- 面对“禁止后不再提示”带来的“无响应”现象,以及在重复和连续权限请求情况下丢失请求的问题,都在PermissionFragment内部进行处理好,大大解放了业务方。详细设计方案见Android权限框架,支持处理连续请求。
9.从当前app跳转到其他app
- android支持app间的跳转,从当前app跳转到其他app时,可以指定要打开的页面。打开app时,首先会执行打开app的MainApplication。不过,如果指定打开的页面不是MAIN页,是不会执行它的MainActivity的,而是直接跳转到指定的界面。
- coding一番,现有包名叫“com.example.kotlinrichtext”的app,后称TestApp,其中MainActivity为启动页,TestActivity是另一个页面。在TestActivity中有一个TextView,其显示的文案根据Shark.ch设置。Shark.ch的值,默认是“默认值”,当MainApplication执行后,会更改为“初始化过了”,当MainActivity执行后,会更改为“走了首页的”。
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.kotlinrichtext"> <application android:name=".MainApplication" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity" android:windowSoftInputMode="stateAlwaysHidden|adjustNothing"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".TestActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MY_ACTION" /> <category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.MY_CATEGORY" /> </intent-filter> </activity> </application> </manifest>
class TestActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Log.d("zouhecan", "TestActivity#onCreate()") setContentView(R.layout.activity_test) button.text = Shark.ch } }
public class Shark { public static String ch = "默认值"; public static void init() { ch = "初始化过了"; } }
class MainApplication extends Application { @Override public void onCreate() { super.onCreate(); Shark.init(); } }
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Shark.ch = "走了首页的" } }
在另一个app中,执行跳转到这个TestApp的逻辑,发现:
1)跳转时,如果TestApp未开启,TestActivity中显示文案为“初始化过了”;
2)跳转时,如果TestApp已经开启,TestActivity中显示文案为“走了首页的”。
- 那具体的跳转逻辑是咋样的呢?如果知道TestApp的包名和要跳转的页面的activity名,那很好办。通过如下方式,
private void jumpOtherApp() {try { ComponentName componentName = new ComponentName("com.example.kotlinrichtext", "com.example.kotlinrichtext.TestActivity"); Intent intent = new Intent(); intent.setComponent(componentName); startActivity(intent); } catch (Exception e) { e.printStackTrace(); } }
或者
private void jumpOtherApp() { try { Intent intent = new Intent(); intent.setClassName("com.example.kotlinrichtext", "com.example.kotlinrichtext.TestActivity"); startActivity(intent); } catch (Exception e) { e.printStackTrace(); } }
直接在intent中指定好包名和activity名即可。
- 那如果不知道activity包名和activity名呢?也可以通过隐式跳转的方式来匹配路径,上面代码中,指定了TestActivity的intent-fliter属性,其中action为android.intent.action.MY_ACTION,并添加了category,android.intent.category.DEFAULT和android.intent.category.MY_CATEGORY,其中android.intent.category.DEFAULT是每个需要被隐式跳转的activity都必须要有的,否则将匹配失败。
private void jumpOtherApp() { try { Intent intent = new Intent(); intent.setAction("android.intent.action.MY_ACTION"); // intent.addCategory("android.intent.category.MY_CATEGORY"); startActivity(intent); } catch (Exception e) { e.printStackTrace(); } }
跳转的Intent中可以不添加category,也可以添加任意多个category,但是添加的category需要全部在Activity的intent-filter中申明过,一旦有一个没匹配上,就会跳转失败,抛ActivityNotFoundException: No Activity Found to handle Intent{action = xxx, category = xxx}。
- 如果有多个App中的Activity注册了同一个action,这时候,系统咋知道该跳到哪里去,事实上,系统可聪明呢。这时候会出一个系统弹窗,让用户自己选择打开哪个app。
- 对目标app中的目标activity,intent-filter中注册data信息,申明外部跳入的scheme,host等信息。
<activity android:name=".TestActivity"> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <data android:host="android.study.diary" android:pathPattern=".*" android:pathPrefix="/" android:scheme="demo" /> </intent-filter> </activity>
然后外部可以通过特定的url信息,打开这个app中的activity。
private void jumpOtherApp() { String url = "demo://android.study.diary/"; try { Intent intent = new Intent(); intent.setData(Uri.parse(url)); startActivity(intent); } catch (Exception e) { e.printStackTrace(); } }
有时候,还需要通过data携带一些参数,比如下面的url中携带了一个message参数,
String url = "demo://android.study.diary/my_path?message=hello world!";
在TestActivity中可以解析这个参数,
private fun parseIntent() { intent.data?.let { val scheme = it.scheme //demo val host = it.host //android.study.diary val port = it.port //-1 val path = it.path ///mypath val query = it.query //message=hello world! val message = it.getQueryParameter("message") //hello world! } }
还有,虽然是通过uri匹配跳转,但是TestActivity在manifest中的action与默认category仍然不可少,少了就ActivityNotFoundException。
- 有时候我们需要在浏览器中直接打开我们的app,在目标activity的manifest中添加category “android.intent.category.BROWSABLE”,表示可以响应浏览器的intent。这时候在网页中放在设置对应的uri,比如这里设置了一个超链接打开demo app,设置超链接的uri为“demo://android.study.diary/my_path?message=hello world!”。在手机的浏览器中打开这篇文章,然后点击超链接,系统就会弹窗是否打开app的弹窗。
<activity android:name=".TestActivity"> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.APP_BROWSER" /> <data android:host="android.study.diary" android:pathPattern=".*" android:pathPrefix="/" android:scheme="demo" /> </intent-filter> </activity>
10.Android约束布局ConstraintLayout
- BaseLine,文本基线,layout_constraintBaseline_toBaselineOf可以约束两个文本的基线对齐。
<TextView android:id="@+id/tv1" android:layout_width="200dp" android:layout_height="100dp" android:background="@color/colorAccent" android:gravity="center" android:text="@string/app_name" android:textSize="22sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/tv2" android:layout_width="200dp" android:layout_height="200dp" android:background="@color/colorPrimary" android:text="@string/app_name" android:textSize="22sp" app:layout_constraintBaseline_toBaselineOf="@id/tv1" app:layout_constraintStart_toStartOf="parent" />
如果所以,两个文本框的文本基线对齐约束。
- Bias,偏移比例,layout_constraintHorizontal_bias指定水平方向的偏移比例,layout_constraintVertical_bias指定垂直方向的偏移比例,比例取值范围0~1,默认值是0.5(居中)。设置水平偏移时,需要同时指定start和end约束;设置垂直偏移时,需要同时指定top和bottom约束。
<TextView android:id="@+id/tv1" android:layout_width="200dp" android:layout_height="100dp" android:background="@color/colorAccent" android:gravity="center" android:text="@string/app_name" android:textSize="22sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.618" /> <TextView android:id="@+id/tv2" android:layout_width="200dp" android:layout_height="100dp" android:background="@color/colorPrimary" android:gravity="center" android:text="@string/app_name" android:textSize="22sp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.618" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" />
如图所示,分别给两个TextView在水平和垂直方向上,设置0.618黄金分割点的偏移比例后,显示的位置。
- ratio和percent,百分比约束,当layout_width和layout_height其一为0,其一不为0时,可以根据layout_constraintDimensionRatio="width:height"设置宽高比;另外当layout_width设置为0时,可以通过layout_constraintWidth_default设置宽的属性,属性“wrap”相当于layout_width="wrap_content";而指定属性为“percent”时,可以再通过layout_constraintWidth_percent设置宽度百分比,取值范围是0~1;layout_constraintHeight_default是类似的;
<TextView android:id="@+id/tv1" android:layout_width="0dp" android:layout_height="0dp" android:background="@color/colorAccent" android:gravity="center" android:text="@string/app_name" android:textSize="22sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHeight_default="wrap" app:layout_constraintTop_toTopOf="parent" app:layout_constraintWidth_default="percent" app:layout_constraintWidth_percent="0.6" /> <TextView android:id="@+id/tv2" android:layout_width="0dp" android:layout_height="0dp" android:background="@color/colorPrimary" android:gravity="center" android:text="@string/app_name" android:textSize="22sp" app:layout_constraintHeight_default="percent" app:layout_constraintHeight_percent="0.8" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintWidth_default="wrap" /> <TextView android:id="@+id/tv3" android:layout_width="200dp" android:layout_height="0dp" android:background="@color/colorPrimaryDark" android:gravity="center" android:text="@string/app_name" android:textSize="22sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintDimensionRatio="3:2" app:layout_constraintEnd_toEndOf="parent" />
如果所示,tv1设置高度自适应,宽占比60%;tv2设置宽度自适应,高占比80%;tv3设置宽高比为3:2。
- GuideLine,是开发者自己添加的辅助线,帮助约束控件位置,而且不会绘制在屏幕。guideline只需要设置orientation和layout_constraintGuide_percente属性,分别执行guideline的方向和偏移位置。例如,三条辅助线分别设置偏移百分比为0.25、0.5、1,他们将对屏幕进行1:1:2划分。
<TextView android:id="@+id/tv1" android:layout_width="0dp" android:layout_height="0dp" android:background="@color/colorAccent" android:gravity="center" android:text="@string/app_name" android:textSize="22sp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toTopOf="@+id/guideline1" /> <androidx.constraintlayout.widget.Guideline android:id="@+id/guideline1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" app:layout_constraintGuide_percent="0.25"/> <TextView android:id="@+id/tv2" android:layout_width="0dp" android:layout_height="0dp" android:background="@color/colorPrimary" android:gravity="center" android:text="@string/app_name" android:textSize="22sp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@+id/guideline1" app:layout_constraintBottom_toTopOf="@+id/guideline2" /> <androidx.constraintlayout.widget.Guideline android:id="@+id/guideline2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" app:layout_constraintGuide_percent="0.5"/> <TextView android:id="@+id/tv3" android:layout_width="0dp" android:layout_height="0dp" android:background="@color/colorPrimaryDark" android:gravity="center" android:text="@string/app_name" android:textSize="22sp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@+id/guideline2" app:layout_constraintBottom_toTopOf="@+id/guideline3" /> <androidx.constraintlayout.widget.Guideline android:id="@+id/guideline3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" app:layout_constraintGuide_percent="1"/>
如图所示,在垂直方向放置了三条guideline,分别在屏幕占比0.25、0.5、1的位置,然后三个TextView的高度分别由三个guideLine进行约束。
- Group,控件组,也是一个辅助控件,不会绘制到屏幕上,通过constraint_refrenced_ids指定多个view的id,将它们行成一个组,常常用于设置一组控件的visible属性。
<Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@color/colorAccent" android:gravity="center" android:text="button" android:textSize="22sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toEndOf="@+id/tv" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/tv" android:layout_width="200dp" android:layout_height="100dp" android:background="@color/colorPrimary" android:gravity="center" android:text="@string/app_name" android:textSize="22sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <androidx.constraintlayout.widget.Group android:id="@+id/group" android:layout_width="wrap_content" android:layout_height="wrap_content" app:constraint_referenced_ids="button,tv" />
如图所示,假设控件TextView和Button,在业务逻辑上表现出同时VISIBLE或者GONE,这时如果没有group,就需要在java代码中分别对tv和button进行操作,现在将他俩划到一个组后,java代码中就只需要对group进行setVisible()操作,相当于同一个组的控件同时被执行了setVisible()操作。
- Barriar,辅助开发控件,不会绘制到屏幕,通过constraint_refrence_ids指定一组id,将这些id对应的view包裹为一个屏障。使用时,需要通过orientation指定屏障方向,并通过设置barrierDirection属性,指定屏障在包裹view的哪一边。
<TextView android:id="@+id/tv1" android:layout_width="wrap_content" android:layout_height="100dp" android:background="@color/colorPrimary" android:gravity="center" android:text="@string/app_name" android:textSize="22sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/tv2" android:layout_width="wrap_content" android:layout_height="100dp" android:background="@color/colorAccent" android:gravity="center" android:text="文本长度不确定" android:textSize="22sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/tv1" /> <androidx.constraintlayout.widget.Barrier android:id="@+id/barrier" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" app:barrierDirection="end" app:constraint_referenced_ids="tv1,tv2" /> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="15dp" android:background="@color/colorAccent" android:gravity="center" android:text="button" android:textSize="22sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toEndOf="@+id/barrier" app:layout_constraintTop_toTopOf="parent" />
如图所示,tv1和tv2中的文本长度是不固定的,但是button要求一定出现在两个文本的右侧。如果不用barrier,就不得不给tv1和tv2外面包裹一层ViewGroup,然后让button就约束viewGroup。有了barrier后,可以将tv1和tv1包裹一个屏障,这时button只用和barrier形成约束即可,避免了嵌套viewGroup。
- Chain,链式约束,当布局中的控件,如果相互进行约束后,就产生了链,链默认属性值是spread,这时也可以通过layout_constraintHorizontal_chainStyle指定链属性为spread_inside或packed。不同的链属性决定链对空间平分的样式,layout_constraintVertical_chainStyle与layout_constraintHorizontal_chainStyle类似。
<TextView android:id="@+id/tv1" android:layout_width="wrap_content" android:layout_height="100dp" android:background="@color/colorPrimary" android:gravity="center" android:text="Text1" android:textSize="22sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/tv2" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintHorizontal_chainStyle="spread" /> <TextView android:id="@+id/tv2" android:layout_width="wrap_content" android:layout_height="85dp" android:background="@color/colorAccent" android:gravity="center" android:text="Text2" android:textSize="22sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/tv3" app:layout_constraintStart_toEndOf="@id/tv1" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/tv3" android:layout_width="wrap_content" android:layout_height="70dp" android:background="@color/colorPrimaryDark" android:gravity="center" android:text="Text3" android:textSize="22sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/tv2" app:layout_constraintTop_toTopOf="parent" />
如图所示,从左到右分别示layout_constraintHorizontal_chainStyle为spread、spread_inside、packed的效果。
下一篇:android日记(六)