Android实用小技巧(持续更新)

文章目录

TextView设置缩进

参考

给drawableRight设置点击事件

例如EditText右侧有一个删除按钮是通过drawableRight属性设置的,此时如果想让其响应点击事件,这可以通过判断点击的坐标位置与删除按钮的位置对比,下面是通过处理点击删除按钮删除EditText框的内容

/**
 * 给EditText的右侧drawableRight属性的图片设置点击事件
 *
 * @param editText
 */
public static void registerEditRightDrawableClickListener(final EditText editText) {
    editText.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            // et.getCompoundDrawables()得到一个长度为4的数组,分别表示左上右下四张图片
            Drawable drawable = editText.getCompoundDrawables()[2];
            //如果右边没有图片,不再处理
            if (drawable == null)
                return false;
            //如果不是按下事件,不再处理
            if (event.getAction() != MotionEvent.ACTION_UP)
                return false;
            if (event.getX() > editText.getWidth() - editText.getPaddingRight() - drawable.getIntrinsicWidth()) {
                editText.setText("");
                return true;
            }
            return false;
        }
    });
}

限制EditText可输入的字数,超出后提示文案

 /**
  * 限制EditText可输入的字数,超出后提示文案
  *
  * @param editText 目标view
  * @param maxLength 最大字数
  * @param msg 提示文案
  * @param callback 回调接口
  */
 public static void registerEditMaxTextShow(final EditText editText, final int maxLength, final String msg, final Callback callback) {
     editText.addTextChangedListener(new TextWatcher() {
         @Override
         public void beforeTextChanged(CharSequence s, int start, int count, int after) {
         }
         @Override
         public void onTextChanged(CharSequence s, int start, int before, int count) {
         }
         @Override
         public void afterTextChanged(Editable s) {
             String currMsg = editText.getText().toString().trim();
             if (null != callback) callback.onEditTextChange(currMsg);
             if (currMsg.length() > maxLength) {
                 ToastUtils.showShort(msg);
                 int editStart = editText.getSelectionStart();
                 int editEnd = editText.getSelectionEnd();
                 s.delete(editStart - 1, editEnd);
                 String finalMsg = s.toString();
                 editText.removeTextChangedListener(this);
                 editText.setText(finalMsg);
                 editText.setSelection(finalMsg.length());
                 if (null != callback) callback.onEditTextChange(finalMsg);
                 editText.addTextChangedListener(this);
             }
         }
     });
 }
 //回调接口
 public interface Callback {
     void onEditTextChange(String msg);
 }

解决RecycleView布局中GridLayoutManager和StaggeredGridLayoutManager添加头部和底部不占用一行的问题

重写RecyclerView.Adapter的2个方法

/**
 * 解决GridLayoutManager添加头部和底部不占用一行的问题
 *
 * @param recyclerView
 */
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
    super.onAttachedToRecyclerView(recyclerView);
    RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
    if (manager instanceof GridLayoutManager) {
        final GridLayoutManager gridManager = ((GridLayoutManager) manager);
        gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
            @Override
            public int getSpanSize(int position) {
                return (isHeaderViewPosition(position) || isFooterViewPosition(position))
                        ? gridManager.getSpanCount() : 1;
            }
        });
    }
    mAdapter.onAttachedToRecyclerView(recyclerView);
}
/**
 * 解决StaggeredGridLayoutManager添加头部和底部不占用一行的问题
 *
 * @param holder
 */
@Override
public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
    super.onViewAttachedToWindow(holder);
    ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
    int position = holder.getLayoutPosition();
    if (lp != null
            && lp instanceof StaggeredGridLayoutManager.LayoutParams
            && (isHeaderViewPosition(position) || isFooterViewPosition(position))) {
        StaggeredGridLayoutManager.LayoutParams p = (StaggeredGridLayoutManager.LayoutParams) lp;
        p.setFullSpan(true);
    }
    mAdapter.onViewAttachedToWindow(holder);
}

重写RecycleView的setLayoutManager方法

/**
 * 解决GridLayoutManager添加头部和底部不占用一行的问题
 *
 * @param layout
 */
@Override
public void setLayoutManager(LayoutManager layout) {
    super.setLayoutManager(layout);
    if (mWrapAdapter != null) {
        if (layout instanceof GridLayoutManager) {
            final GridLayoutManager gridManager = ((GridLayoutManager) layout);
            gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
                @Override
                public int getSpanSize(int position) {
                    return (mWrapAdapter.isHeaderViewPosition(position) || mWrapAdapter.isFooterViewPosition(position))
                            ? gridManager.getSpanCount() : 1;
                }
            });
        }
    }
}

前提是得确定头部和尾部的位置,即isHeaderViewPosition和isFooterViewPosition的逻辑了.通过adapter就可以计算出是否是头部还是尾部的位置了.


解决由于RecyclerView有刷新头存在,导致canScrollVertically(-1)时始终返回true的bug


 @Override
 public boolean canScrollVertically(int direction) {
      if (direction < 1) {
          boolean original = super.canScrollVertically(direction);
          int firstVisiblePosition = getFirstVisiblePosition();
          if (!original || getChildAt(0) != null && getChildAt(0).getTop() >= 0) {
              LayoutManager layoutManager = getLayoutManager();
              if (layoutManager instanceof LinearLayoutManager) {
                  return firstVisiblePosition > 0;
              } else if (layoutManager instanceof StaggeredGridLayoutManager) {
                  int spanCount = ((StaggeredGridLayoutManager) layoutManager).getSpanCount();
                  return firstVisiblePosition > spanCount;
              } else if (layoutManager instanceof GridLayoutManager) {
                  int spanCount = ((GridLayoutManager) layoutManager).getSpanCount();
                  return firstVisiblePosition > spanCount;
              }
          } else {
              return true;
          }
      }
      return super.canScrollVertically(direction);
  }

获取RecycleView第一个和最后一个可见Item的位置

 /**
  * 获取第一个可见的item位置
  * @return
  */
 public int getFirstVisiablePosition() {
     LayoutManager layoutManager = getLayoutManager();
     int firstVisibleItemPosition;
     if (layoutManager instanceof GridLayoutManager) {
         firstVisibleItemPosition = ((GridLayoutManager) layoutManager).findFirstVisibleItemPosition();
     } else if (layoutManager instanceof StaggeredGridLayoutManager) {
         int[] into = new int[((StaggeredGridLayoutManager) layoutManager).getSpanCount()];
         ((StaggeredGridLayoutManager) layoutManager).findFirstVisibleItemPositions(into);
         firstVisibleItemPosition = findMin(into);
     } else {
         firstVisibleItemPosition = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
     }
     return firstVisibleItemPosition;
 }
 

/**
 * 获取可见列表内最后一个item的位置
 *
 * @return
 */
public int getLastVisibleItemPosition() {
    int lastVisibleItemPosition;
    LayoutManager layoutManager = getLayoutManager();
    if (layoutManager instanceof GridLayoutManager) {
        lastVisibleItemPosition = ((GridLayoutManager) layoutManager).findLastVisibleItemPosition();
    } else if (layoutManager instanceof StaggeredGridLayoutManager) {
        int[] into = new int[((StaggeredGridLayoutManager) layoutManager).getSpanCount()];
        ((StaggeredGridLayoutManager) layoutManager).findLastVisibleItemPositions(into);
        lastVisibleItemPosition = findMax(into);
    } else {
        lastVisibleItemPosition = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();
    }
    return lastVisibleItemPosition;
}

private int findMin(int[] firstPositions) {
     int min = firstPositions[0];
     for (int value : firstPositions) {
         if (value < min) {
             min = value;
         }
     }
     return min;
 }
 private int findMax(int[] lastPositions) {
     int max = lastPositions[0];
     for (int value : lastPositions) {
         if (value > max) {
             max = value;
         }
     }
     return max;
 }

修改RadioButton的drawableRight图片与文字相隔的距离太大,导致drawablePadding设置无效

重写AppCompatRadioButton的onDraw方法

@Override
protected void onDraw(Canvas canvas) {
    //得到Drawable集合  分别对应 左上右下
    Drawable[] drawables = getCompoundDrawables();
    if (drawables != null) {
        //获取右边图片,修改drawableRight的图片紧贴着文字
        Drawable drawableRight = drawables[2];
        if (drawableRight != null) {
            //获取文字占用长宽
            int textWidth = (int) getPaint().measureText(getText().toString());
            int textHeight = (int) getPaint().getTextSize();
            //获取图片实际长宽
            int drawableWidth = drawableRight.getIntrinsicWidth();
            int drawableHeight = drawableRight.getIntrinsicHeight();
            //setBounds修改Drawable在View所占的位置和大小,对应参数同样的 左上右下()
            int bodyWidth = textWidth + drawableWidth + getCompoundDrawablePadding();
            int left = (bodyWidth - getWidth()) / 2;
            int right = left + drawableWidth;
            drawableRight.setBounds(left, 0, right, drawableHeight);
        }
    }
    super.onDraw(canvas);
}

解决RadioButton无法重复点击取消选中的状态

重写AppCompatRadioButton的toggle方法

@Override
public void toggle() {
    setChecked(!isChecked());
    if (!isChecked()) {
        if (null != getParent() && getParent() instanceof RadioGroup)
            ((RadioGroup) getParent()).clearCheck();
    }
}

解决手动设置Switch的setChecked方法导致setOnCheckedChangeListener触发响应

mPushSwt.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
    @Override
    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
        if (buttonView.isPressed()) { //避免代码设置setChecked状态导致回调监听
            //do sth...
        }
    }
});

避免SwipeRefreshLayout重复下拉导致刷新按钮异常显示

重写SwipeRefreshLayout的onStartNestedScroll方法

 @Override
 public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
     //避免重复下拉刷新导致动画异常
     return !isRefreshing() && super.onStartNestedScroll(child, target, nestedScrollAxes);
 }

更新于2019-01-03


TextView的drawableLeft与文本无法一起居中显示

TextView设置的文本默认是存在一个上下间距的,也就是上下空白,当我们在使用drawableLeft的时候,这个默认的空白会使TextView中的文本向下偏移,当你的drawableLeft使用的icon很小,文字的size也很小的时候,即使你设置了android:gravity=“center”,也能很明显的看到你的TextView中的文本基本上是与icon处于底边对其,而不是居中对其

只要TextView中加上android:includeFontPadding=“false” 这个属性属性就可以了!


控制子View的状态跟随父View状态变化

代码设置 :
setDuplicateParentStateEnabled(true)

布局设置:
android:duplicateParentState=“true”


更新于2019-01-08


重力传感监听

public class MySensorHelper {
    private static final String TAG = MySensorHelper.class.getSimpleName();
    private OrientationEventListener mLandOrientationListener;
    private OrientationEventListener mPortOrientationListener;

    public interface Callback {
        void onOrientationChange(int orientation);
    }

    public MySensorHelper(final Activity activity, final Callback callback) {
        this.mLandOrientationListener = new OrientationEventListener(activity, 3) {
            public void onOrientationChanged(int orientation) {
                Log.d(MySensorHelper.TAG, "mLandOrientationListener");
                if (orientation < 100 && orientation > 80 || orientation < 280 && orientation > 260) {
                    Log.e(MySensorHelper.TAG, "转到了横屏");
                    if (callback != null) {
                        Log.e(MySensorHelper.TAG, "转到了横屏##################");
                        callback.onOrientationChange(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
                    }
                }

            }
        };
        this.mPortOrientationListener = new OrientationEventListener(activity, 3) {
            public void onOrientationChanged(int orientation) {
                Log.w(MySensorHelper.TAG, "mPortOrientationListener");
                if (orientation < 10 || orientation > 350 || orientation < 190 && orientation > 170) {
                    Log.e(MySensorHelper.TAG, "转到了竖屏");
                    if (callback != null) {
                        Log.e(MySensorHelper.TAG, "转到了竖屏!!!!!!!!!!!!!!!!!!!!!!");
                        callback.onOrientationChange(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
                    }
                }

            }
        };
        this.enable();
    }

    //禁用切换屏幕的开关
    public void disable() {
        Log.e(TAG, "disable");
        this.mPortOrientationListener.disable();
        this.mLandOrientationListener.disable();
    }

    //开启横竖屏切换的开关
    public void enable() {
        this.mPortOrientationListener.enable();
        this.mLandOrientationListener.enable();
    }
}

网络状态监听

public class NetworkConnectChangedReceiver extends BroadcastReceiver {

    private static final String TAG = "Network";
    private Callback mCallback;

    public abstract static class Callback {
        /**
         * wifi是否打开
         *
         * @param isEnable
         */
        public void isWifiEnable(boolean isEnable) {
        }

        /**
         * 各种状态监听
         *
         * @param wifi
         * @param mobile
         * @param all
         */
        public void isAvailable(boolean wifi, boolean mobile, boolean all) {
        }

    }

    public NetworkConnectChangedReceiver(Callback callback) {
        this.mCallback = callback;
    }

    public IntentFilter getIntentFilter() {
        IntentFilter filter = new IntentFilter();
        filter.addAction("android.net.conn.CONNECTIVITY_CHANGE");
        filter.addAction("android.net.wifi.WIFI_STATE_CHANGED");
        filter.addAction("android.net.wifi.STATE_CHANGE");
        return filter;
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        // 这个监听wifi的打开与关闭,与wifi的连接无关
        if (WifiManager.WIFI_STATE_CHANGED_ACTION.equals(intent.getAction())) {
            int wifiState = intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, 0);
            Log.e(TAG, "wifiState" + wifiState);
            switch (wifiState) {
                case WifiManager.WIFI_STATE_DISABLED:
                    mCallback.isWifiEnable(false);
                    break;
                case WifiManager.WIFI_STATE_DISABLING:
                    break;
                case WifiManager.WIFI_STATE_ENABLING:
                    break;
                case WifiManager.WIFI_STATE_ENABLED:
                    mCallback.isWifiEnable(true);
                    break;
                case WifiManager.WIFI_STATE_UNKNOWN:
                    break;
                default:
                    break;


            }
        }
        if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) {
            ConnectivityManager manager = (ConnectivityManager) context
                    .getSystemService(Context.CONNECTIVITY_SERVICE);
            Log.i(TAG, "CONNECTIVITY_ACTION");

            NetworkInfo activeNetwork = manager.getActiveNetworkInfo();
            if (activeNetwork != null) { // connected to the internet
                if (activeNetwork.isConnected()) {
                    if (activeNetwork.getType() == ConnectivityManager.TYPE_WIFI) {
                        // connected to wifi
                        Log.e(TAG, "当前WiFi连接可用 ");
                        mCallback.isAvailable(true, false, true);
                    } else if (activeNetwork.getType() == ConnectivityManager.TYPE_MOBILE) {
                        // connected to the mobile provider's data plan
                        Log.e(TAG, "当前移动网络连接可用 ");
                        mCallback.isAvailable(false, true, true);
                    }
                } else {
                    Log.e(TAG, "当前没有网络连接,请确保你已经打开网络 ");
                    mCallback.isAvailable(false, false, false);
                }

            } else {   // not connected to the internet
                Log.e(TAG, "当前没有网络连接,请确保你已经打开网络 ");
                mCallback.isAvailable(false, false, false);

            }


        }
    }


}

使用方式

private NetworkConnectChangedReceiver mConnectChangedReceiver;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

    mConnectChangedReceiver = new NetworkConnectChangedReceiver(new NetworkConnectChangedReceiver.Callback() {
        @Override
        public void isAvailable(boolean wifi, boolean mobile, boolean all) {
            if (wifi) {
                //dosth..
            }else if(mobile){
            	//dosth...
			}
        }
    });
    registerReceiver(mConnectChangedReceiver, mConnectChangedReceiver.getIntentFilter());
}


@Override
protected void onDestroy() {
    super.onDestroy();
    unregisterReceiver(mConnectChangedReceiver);
}

更新于2019-01-10


RecycleView#LinearLayoutManager滚动到指定位置并距离顶部xxx偏移量

 LinearLayoutManager layoutManager = (LinearLayoutManager) mRv.getLayoutManager();
 int itemWidth = DisplayUtils.dip2px(mContext, 51);//item的宽度
 //item位于中心显示的偏移量
 int pinkPx = (int) ((Env.screenWidth - 2 * mRv.getPaddingLeft() - itemWidth) * 0.5f);
 //定位到指定的position并且该position对应的item距离RecycleView顶部的距离是pinkPx 
 layoutManager.scrollToPositionWithOffset(currIndex, pinkPx);

#更新于2019-01-17


RecycleView水平向左滑动弹性收回

重写RecycleView的onTouchEvent方法

private float downTouch;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downTouch = event.getX();
                break;
            case MotionEvent.ACTION_MOVE:
                float moveTouch = event.getX();
                if (!canScrollHorizontally(1)) {
                    int deltaX = (int) (downTouch - moveTouch);
                    if (deltaX > 10) {
                    	//向左滑动到底后,修改其右内边距不断递增
                        setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight() + deltaX / 3, getPaddingBottom());
                    }
                } else {
                	//恢复原位
                    setPadding(getPaddingLeft(), getPaddingTop(), 0, getPaddingBottom());
                }
                downTouch = moveTouch;
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
            	//恢复原位
                setPadding(getPaddingLeft(), getPaddingTop(), 0, getPaddingBottom());
                break;
        }
        return super.onTouchEvent(event);
    }

如果还需要实现回滚完毕后跳去新页面,则可以监听其滚动,当滚动停止的时候就可以跳去新页面了.
例如:


OverScrollRecycleView osRv = findViewById(R.id.rv_over_scroll);
osRv.addOnScrollListener(new RecyclerView.OnScrollListener() {
    @Override
    public void onScrollStateChanged(RecyclerView rv, int newState) {
     
        if (newState == RecyclerView.SCROLL_STATE_IDLE && !rv.canScrollHorizontally(1)) {
            Intent intent = new Intent(mContext, NextActivity.class);
            intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);//避免打开同一个Activity多次
            startActivity(intent);
        }
    }
});

#更新于2019-01-24


限制EditText的最大字数输入

例如限制EditText最大只能输入150个字,超出后提示文案

private class LengthInputFilter extends InputFilter.LengthFilter {
       
        public LengthInputFilter(int max) {
            super(max);
        }

        @Override
        public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
            CharSequence s = super.filter(source, start, end, dest, dstart, dend);
            if (s != null) {
            	//getMax是LengthFilter的内部方法,返回的是构造方法传入的max
                ToastUtils.show(mContext, "不能超过" + getMax()+ "字", Toast.LENGTH_SHORT);
            }
            return s;
        }
    }

使用方式如下:

 mInputEdt.setFilters(new InputFilter[]{new LengthInputFilter(150)});

更新于2019-01-26


临时申请root权限

前提是设备已经root过了.

public void testRoot() {
        try {
            Process su = Runtime.getRuntime().exec("su");
            OutputStream outputStream = su.getOutputStream();
            DataOutputStream dataOutputStream = new DataOutputStream(outputStream);
            dataOutputStream.writeBytes("\n");
            dataOutputStream.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

更新于2019-01-30


WebView内容截屏

/**
     * Android 5.0以下版本
     * 此方法用在5.0以上时需在创建webView之前调用enableSlowWholeDocumentDraw()方法关闭优化才能截取到这个WebView内容
     * 对WebView进行截屏
     *
     * @param webView
     * @return
     */
    public static Bitmap getWebViewLongBitmpKitKat(WebView webView) {
        Picture picture = webView.capturePicture();
        int width = picture.getWidth();
        int height = picture.getHeight();
        if (width > 0 && height > 0) {
            Bitmap bitmap = Bitmap.createBitmap(webView.getWidth(), (int) (webView.getContentHeight() * webView.getScale()), Bitmap.Config.RGB_565);
            Canvas canvas = new Canvas(bitmap);
            picture.draw(canvas);
            return bitmap;
        }
        return null;
    }

修改状态栏为透明

/**
     * 修改状态栏为透明效果
     */
    public static void setStatusBarTrans(Activity activity) {
        //5.0及以上,修改完后,布局会延伸到状态栏
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            Window window = activity.getWindow();
            window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                    | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
            window.setStatusBarColor(Color.TRANSPARENT);

        }
        //4.4到5.0
        else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            WindowManager.LayoutParams localLayoutParams = activity.getWindow().getAttributes();
            localLayoutParams.flags = WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS |
                    localLayoutParams.flags;
        }
    }

修改状态栏背景颜色

/**
     * 修改状态栏颜色,支持4.4以上版本
     *
     * @param activity
     * @param colorId
     */
    public static void setStatusBarColor(Activity activity, int colorId) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            Window window = activity.getWindow();
            window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
            window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
            window.setStatusBarColor(activity.getResources().getColor(colorId));
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            //使用SystemBarTint库使4.4版本状态栏变色,需要先将状态栏设置为透明
            WindowManager.LayoutParams localLayoutParams = activity.getWindow().getAttributes();
            localLayoutParams.flags = WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS |
                    localLayoutParams.flags;
			
			//需要引入依赖包 compile 'com.readystatesoftware.systembartint:systembartint:1.0.3'
            SystemBarTintManager tintManager = new SystemBarTintManager(activity);
            tintManager.setStatusBarTintEnabled(true);
            tintManager.setStatusBarTintResource(colorId);
        }
    }


修改状态栏文字颜色(白/黑切换)

 /**
     * android 4.4及以上修改状态栏文字的颜色
     * 修改状态栏文字颜色,这里小米,魅族区别对待。
     */
    public static void setLightStatusBar(final Activity activity, final boolean dark) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            switch (RomUtils.getLightStatusBarAvailableRomType()) {
                case RomUtils.AvailableRomType.MIUI:
                    setMIUIStatusBarLightMode(activity, dark);
                    break;

                case RomUtils.AvailableRomType.FLYME:
                    setFlymeLightStatusBar(activity, dark);
                    break;

                default:
                    setAndroidNativeLightStatusBar(activity, dark);
                    break;

            }
        }
    }

    /**
     * 小米系统下状态栏文字颜色的修改
     *
     * @param activity
     * @param dark true:黑色  false: 白色
     * @return
     */
    public static boolean setMIUIStatusBarLightMode(Activity activity, boolean dark) {
        boolean result = false;
        Window window = activity.getWindow();
        if (window != null) {
            Class clazz = window.getClass();
            try {
                int darkModeFlag = 0;
                Class layoutParams = Class.forName("android.view.MiuiWindowManager$LayoutParams");
                Field field = layoutParams.getField("EXTRA_FLAG_STATUS_BAR_DARK_MODE");
                darkModeFlag = field.getInt(layoutParams);
                Method extraFlagField = clazz.getMethod("setExtraFlags", int.class, int.class);
                if (dark) {
                    extraFlagField.invoke(window, darkModeFlag, darkModeFlag);//状态栏透明且黑色字体
                } else {
                    extraFlagField.invoke(window, 0, darkModeFlag);//清除黑色字体
                }
                result = true;

                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && RomUtils.isMiUIV7OrAbove()) {
                    //开发版 7.7.13 及以后版本采用了系统API,旧方法无效但不会报错,所以两个方式都要加上
                    if (dark) {
                        activity.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
                    } else {
                        activity.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
                    }
                }
            } catch (Exception e) {

            }
        }
        return result;
    }

    /**
     * 魅族系统状态栏文字颜色修改
     *
     * @param activity
     * @param dark true:黑色  false: 白色
     * @return
     */
    private static boolean setFlymeLightStatusBar(Activity activity, boolean dark) {
        boolean result = false;
        if (activity != null) {
            try {
                WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
                Field darkFlag = WindowManager.LayoutParams.class
                        .getDeclaredField("MEIZU_FLAG_DARK_STATUS_BAR_ICON");
                Field meizuFlags = WindowManager.LayoutParams.class
                        .getDeclaredField("meizuFlags");
                darkFlag.setAccessible(true);
                meizuFlags.setAccessible(true);
                int bit = darkFlag.getInt(null);
                int value = meizuFlags.getInt(lp);
                if (dark) {
                    value |= bit;
                } else {
                    value &= ~bit;
                }
                meizuFlags.setInt(lp, value);
                activity.getWindow().setAttributes(lp);
                result = true;
            } catch (Exception e) {
            }
        }
        return result;
    }

   /**
     * 修改状态栏文字颜色
     * @param activity
     * @param dark true:深色,false:白色
     */
    private static void setAndroidNativeLightStatusBar(Activity activity, boolean dark) {
        View decor = activity.getWindow().getDecorView();
        if (dark) {
            //设置字体为深色
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { //6.0+
                decor.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
            }else{ //6.0以下无法修改状态栏文字的颜色,只能通过修改状态栏颜色为暗色来区分白色的字体
                setStatusBarColor(activity, R.color.color_10Black); //调用前面例子的方法
            }

        } else {
            //恢复字体为白色
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { //6.0+
                decor.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
            }else{ //6.0以下无法修改状态栏文字的颜色,默认就是白色,将之前设置状态栏的颜色去掉
                setStatusBarColor(activity, R.color.full_transparent);
            }
        }
    }

更新于2019-02-28


代码设置选择器

1.文字颜色选择器

	//选择器状态是二维数组,负数表示false,正数表示true
	int[][] states = new int[][]{
	         new int[]{-android.R.attr.state_selected}, // unselected
	         new int[]{android.R.attr.state_selected}  // selected
	 };
	 //对应状态的颜色值
	 int[] colors = new int[]{
	         getResources().getColor(R.color.color_121C35),
	         getResources().getColor(R.color.color_FF3B3B)
	 };
	 //设置文字颜色选择器ColorStateList
	 TextView textView = new TextView(getContext());
	 textView.setTextColor(new ColorStateList(states, colors));

2.图片背景选择器

	//创建图片选择器
	StateListDrawable stateListDrawable = new StateListDrawable();
    //添加状态和对应的图片,同样状态是负数表示false,反之表示true
    stateListDrawable.addState(new int[]{-android.R.attr.state_checked}, getDrawable(android.R.drawable.ic_item_normal));
    stateListDrawable.addState(new int[]{android.R.attr.state_checked}, getDrawable(android.R.drawable.ic_item_checked));

    //直接设置背景为图片选择器
    ImageView imageView = new ImageView(getContext());
    imageView.setImageDrawable(stateListDrawable);

更新于2019-03-05


限制EditText小数点后可以输入几位数

/**
     * 保留多少位小数
     *
     * @param expectPriceEdt
     * @param remainCount    保留几位小数,例如保留2位小数,那么小数点后只能输入2位数
     */
    public static void formatNumInput(EditText expectPriceEdt, final int remainCount) {
        expectPriceEdt.addTextChangedListener(new TextWatcher() {
            public void afterTextChanged(Editable edt) {
                String temp = edt.toString();
                int posDot = temp.indexOf(".");
                if (posDot <= 0) return;
                if (temp.length() - posDot - 1 > remainCount) {
                    edt.delete(posDot + 3, posDot + 4);
                }
            }

            public void beforeTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) {
            }

            public void onTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) {
            }
        });
    }

点击非EditText区域隐藏软键盘

		
	//点击其他区域隐藏软键盘后还是否需要响应当次的点击事件
    private boolean clickShouldRspAfterSoftInputHide;
	
	//重写Activity的事件分发方法,点击输入框以外的区域隐藏软键盘
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            View v = getCurrentFocus();
            if (isShouldHideInput(v, ev)) {
                if (hideInputMethod(v) && !clickShouldRspAfterSoftInputHide) {
               		/**直接返回true表示不会向下继续分发事件了
               		   如果点击的view还需要响应事件,那么clickShouldRspAfterSoftInputHide设置为true
               		**/
                    return true; 
                }
            }
        }
        return super.dispatchTouchEvent(ev);
    }

	//判断是否要隐藏软键盘
    public boolean isShouldHideInput(View v, MotionEvent event) {
        if (v != null && (v instanceof EditText)) {
            int[] leftTop = {0, 0};
            v.getLocationInWindow(leftTop);//获取控件在屏幕的坐标
            int left = leftTop[0], top = leftTop[1], bottom = top + v.getHeight(), right = left
                    + v.getWidth();
            if (event.getRawX() > left && event.getRawX() < right
                    && event.getRawY() > top && event.getRawY() < bottom) {
                // 点击的范围是当前EditText,不用隐藏软键盘
                return false;
            } else {
                return true;//需要隐藏软键盘
            }
        }
        return false;
    }

    public Boolean hideInputMethod(View v) {
        InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
        if (imm != null) {
            return imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
        }
        return false;
    }

更新于2019-03-07


WebView首页重定向后,canGoBack在首页永远返回true,怎么解决?

	@Override
    public void onBackPressed() {
        //后退
        if (mWebView.canGoBack()) {
           
            WebBackForwardList list = mWebView.copyBackForwardList();
            if (list.getCurrentIndex() ==0) {
                //如果首页重定向后,初始地址会变成data:text/html;charset=utf-8;base64,
                String firstUrl = list.getItemAtIndex(0).getUrl();
                if (!URLUtil.isNetworkUrl(firstUrl)) {
                    //避免首次进入重定向的时候退不出webview
                    super.onBackPressed();
                    return;
                }
            }
            mWebView.goBack();
        } else {
            super.onBackPressed();
        }
    }

更新于2019-03-08


动态设置shape的圆角

假设布局里面已经给View设置了一个shape作为背景图片了,如果我想在代码中修改它的圆角的话,可以通过这种方式修改:

	//圆角为8dp
 	final int dp8 = DisplayUtils.dip2px(mContext, 8);
 	//先找到设置了shape的view
	ConstraintLayout item= findViewById(R.id.cl_content);
	//获取shape图形
	GradientDrawable drawable = (GradientDrawable) item.getBackground();
	//左上,右上,右下,左下 必须传8个值,每2个为一个方向
	drawable.setCornerRadii(new float[]{dp8, dp8, dp8, dp8, dp8, dp8, dp8, dp8});

更新于2019-03-13


禁用多点触控

两种方式:

1、禁用全局多点触控:

在application引用的Theme中添加以下代码:

  <item name="android:windowEnableSplitTouch">false</item>
  <item name="android:splitMotionEvents">false</item>

2、如果要单独对某个界面禁用,则需要的在相应的xml或代码中添加:

android:splitMotionEvents="false"
rootLayout.setMotionEventSplittingEnabled(false);

注意:必须是需要禁止多点触控的控件的上层父类中添加! 父类的父类加了无效。 另外,该属性实在Android API level 11 之后才有的。

更新于2019-03-25


屏蔽dialog的焦点

window.setFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);

View设置缩放中心点

用过ScaleAnimation的人都知道,可以直接在构造方法中设置,但是如果是使用ObjectAnimator或者是直接setScaleX和setScaleY的方式那要怎么设置缩放中心点呢??

答案是使用View的setPivotX和setPivotY方法单独设置,源码如下:

/**
     * Sets the x location of the point around which the view is
     * {@link #setRotation(float) rotated} and {@link #setScaleX(float) scaled}.
     * By default, the pivot point is centered on the object.
     * Setting this property disables this behavior and causes the view to use only the
     * explicitly set pivotX and pivotY values.
     *
     * @param pivotX The x location of the pivot point.
     * @see #getRotation()
     * @see #getScaleX()
     * @see #getScaleY()
     * @see #getPivotY()
     *
     * @attr ref android.R.styleable#View_transformPivotX
     */
    public void setPivotX(float pivotX) {
        if (!mRenderNode.isPivotExplicitlySet() || pivotX != getPivotX()) {
            invalidateViewProperty(true, false);
            mRenderNode.setPivotX(pivotX);
            invalidateViewProperty(false, true);

            invalidateParentIfNeededAndWasQuickRejected();
        }
    }
 /**
     * Sets the y location of the point around which the view is {@link #setRotation(float) rotated}
     * and {@link #setScaleY(float) scaled}. By default, the pivot point is centered on the object.
     * Setting this property disables this behavior and causes the view to use only the
     * explicitly set pivotX and pivotY values.
     *
     * @param pivotY The y location of the pivot point.
     * @see #getRotation()
     * @see #getScaleX()
     * @see #getScaleY()
     * @see #getPivotY()
     *
     * @attr ref android.R.styleable#View_transformPivotY
     */
    public void setPivotY(float pivotY) {
        if (!mRenderNode.isPivotExplicitlySet() || pivotY != getPivotY()) {
            invalidateViewProperty(true, false);
            mRenderNode.setPivotY(pivotY);
            invalidateViewProperty(false, true);

            invalidateParentIfNeededAndWasQuickRejected();
        }
    }

例如设置右下角为缩放中心点可以这样弄target.setPivotX(target.getWidth());target.setPivotY(target.getHeight());

更新于2019-04-15


android如何监听应用进入后台,回到前台时做相应逻辑

详情看这里

更新于2019-04-17

Android 软键盘弹出情况下监听返回键直接退出界面

详情看这里

更新于2019-04-23


解决App启动时黑屏或者白屏的问题

将启动图通过样式的方式设置

<style name="AppTheme.Splash" parent="@style/Theme.AppCompat.NoActionBar">
        <item name="android:windowFullscreen">true</item>
        <item name="android:windowNoTitle">true</item>
        <!--开屏图,解决应用启动的时候,会白屏一段时间的问题-->
        <item name="android:windowBackground">@drawable/app_launcher_360</item>
        <!--就是这个控件的背景,注意这个属性会影响到dialog的背景-->
        <item name="android:background">@null</item>

</style>

然后在启动Activity使用该样式

<activity
   android:name=".module.main.LaunchActivity"
    android:alwaysRetainTaskState="true"
    android:screenOrientation="portrait"
    android:theme="@style/AppTheme.Splash">
   
    <intent-filter>
        <action android:name="android.intent.action.MAIN"/>

        <category android:name="android.intent.category.LAUNCHER"/>
    </intent-filter>
</activity>

然后LaunchActivity就不需要做任何操作了,如果启动图还有广告图的话,那只需要在布局中添加广告图的布局,代码中书写广告的

加载逻辑就可以了,无需关心默认的启动图,因为广告图显示的时候会自动覆盖默认的的启动图.

例如:

protected void initView() {
   mTimerTv = findViewById(R.id.tv_timer);//计时器
   mLaunchAdIv = findViewById(R.id.iv_launch_ad);//启动图广告
   if (null != mLaunchAdv && URLUtil.isNetworkUrl(mLaunchAdv.image)) {
       ImageLoadUtils.disPlay(mLaunchAdv.image, mLaunchAdIv);//加载启动图
       showLaunchAd(); //显示启动图布局
   } else {
       dealLaunchNormal();//处理正常跳转
   }
}

//正常跳转
private void dealLaunchNormal() {
   if (isFirstInstall) { //首次安装
       jump2Guide(); //进入新手引导
   } else {
       jump2Main(); //进入首页
   }
}

更新于2019-04-23


android启动页图片icon拉伸问题完美解决方案

查看详情

更新于2019-04-26


解决RecycleView#StaggeredGridLayoutManager布局,列表滚动时会出现切换动画的问题

由于瀑布流布局,每个item的高度都是不定的,所以当滑动了很多页的时候,滚出屏幕的item已经回收掉了,而当再次滑动回到列表顶部的时候,由于item需要重新布局,所以会看到切换动画, 解决办法如下:

步骤一: 去掉切换动画

StaggeredGridLayoutManager layoutManager = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
layoutManager.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_NONE); //禁止瀑布流切换动画,问题是顶部会留白
mRecyclerView.setLayoutManager(layoutManager);
mRecyclerView.setAdapter(new MyAdapter(mContext, mData));

步骤二:解决顶部留白的问题

mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {

   boolean hasNotify = true;
    int topLastVisibleItemPosition = -1;//上次刷新时,最后一个可见item的位置

    @Override
    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
        super.onScrollStateChanged(recyclerView, newState);
        //获取实时滚动的列表top和bottom可见位置
        int firstVisiblePosition = mRecyclerView.getFirstVisiblePosition();
        int lastVisibleItemPosition = mRecyclerView.getLastVisibleItemPosition();

        //解决顶部留白的问题
        if (mRecyclerView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE
                && !mRecyclerView.canScrollVertically(-1)) {
            if (mRecyclerView.getAdapter() != null && !hasNotify) {
                hasNotify = true;
                topLastVisibleItemPosition = lastVisibleItemPosition;
                //刷新列表,留白就看不到了.
                mRecyclerView.getAdapter().notifyDataSetChanged();
                LogUtils.e("cys", "置顶刷新");
            }
        }
        // 当前列表最后一个可见item的位置如果大于上次刷新时的位置,所以列表已经向上拖拽过了,这个时候才需要重置变量hasNotify 
        if (firstVisiblePosition > topLastVisibleItemPosition) {
            hasNotify = false; //避免重复刷新列表
            LogUtils.e("cys", "重置hasNotify = false");
        }

    }
});

更新于2019/05/16


gradle.properties常用的几个配置说明

org.gradle.jvmargs=-Xmx1536m : 用来加快gradle的编译!

android.enableAapt2=false

Error:java.util.concurrent.ExecutionException: com.android.tools.aapt2.Aapt2Exception:

android.useDeprecatedNdk=true

Error:(12, 0) Error: NDK integration is deprecated in the current plugin.
Consider trying the new experimental plugin.
For details, see http://tools.android.com/tech-docs/new-build-system/gradle-experimental.
Set “android.useDeprecatedNdk=true” in gradle.properties to continue using the current NDK integration.

android.injected.testOnly=false
让debug包也可以下载安装.

Java 得到泛型中得到T.class

Class <T> entityClass = (Class <T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0]; 

更新于2019/06/4


Android textview字体不随系统字体改变

方式1:把textview的字体大小设置为PX为单位,原因是px不随系统而改变。

方式2:在BaseActivity重写 getResources()方法,具体代码如下

//字体不随系统字体改变
@Override
 public Resources getResources() {
     Resources resources = super.getResources();
     Configuration config = new Configuration();
     config.setToDefaults();
     resources.updateConfiguration(config, resources.getDisplayMetrics());
     return resources;
 }

更新于2019/06/12


如何设置Dialog的状态栏和虚拟导航栏为透明

方式一:
在style文件中添加自定义样式,并继承@android:style/Theme.Dialog

<!--状态栏开启半透明-->
<item name="android:windowTranslucentStatus">true</item>
<!--状态栏颜色设置成透明,这个属性是v21可以用,单独在values-v21下创建style-->
<item name="android:statusBarColor">@android:color/transparent</item>
<!--导航透明化-->
<item name="android:windowTranslucentNavigation">true</item>

然后,自定义的Dialog的构造方法中使用该样式
super(context, R.style.myDialog);

方式二:
代码设置,在自定义的Dialog中加入这段代码

Window window = getWindow();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    //导航栏透明
    window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
    //状态栏透明,这个其实也可以不用设置,上面那条flag也有这个效果
    window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
}

需要强调一点的是,导航栏是在App页面的最上面的,所以透明后,是可以看到dialog的背景的.

更新于2019/06/27


RecycleView的GridLayoutManager布局自适应高度

package com.juchaozhi.classification;

import android.content.Context;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.view.ViewGroup;

/**
 * 自适应高度的GridLayoutManager
 * Created by mChenys on 2019/7/24.
 */
public class WrapHeightGridLayoutManager extends GridLayoutManager {

    private int mChildPerLines;//每一行的子View 个数
    private int[] mMeasuredDimension = new int[2];

    public WrapHeightGridLayoutManager(Context context, int spanCount) {
        super(context, spanCount);
        this.mChildPerLines = spanCount;
    }

    @Override
    public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, int widthSpec, int heightSpec) {

        final int heightMode = View.MeasureSpec.getMode(heightSpec);
        final int widthSize = View.MeasureSpec.getSize(widthSpec);
        final int heightSize = View.MeasureSpec.getSize(heightSpec);
        int height = 0;
        for (int i = 0; i < getItemCount(); ) {
            measureScrapChild(recycler, i,
                    View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED),
                    View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED),
                    mMeasuredDimension);
            height = height + mMeasuredDimension[1];//累计每一行的高度
            i = i + mChildPerLines;
        }

        if (height < heightSize) {
            switch (heightMode) {
                case View.MeasureSpec.EXACTLY:
                    height = heightSize;
                case View.MeasureSpec.AT_MOST:
                case View.MeasureSpec.UNSPECIFIED:
            }
            setMeasuredDimension(widthSize, height + getPaddingTop() + getPaddingBottom());
        } else {
            // If child view is more than screen size, there is no need to make it wrap content. We can use original onMeasure() so we can scroll view.
            super.onMeasure(recycler, state, widthSpec, heightSpec);
        }
    }

    private void measureScrapChild(RecyclerView.Recycler recycler, int position, int widthSpec,
                                   int heightSpec, int[] measuredDimension) {

        View view = recycler.getViewForPosition(position);

        // For adding Item Decor Insets to view
        super.measureChildWithMargins(view, 0, 0);
        if (view != null) {
            RecyclerView.LayoutParams p = (RecyclerView.LayoutParams) view.getLayoutParams();
            int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec,
                    getPaddingLeft() + getPaddingRight() + getDecoratedLeft(view) + getDecoratedRight(view), p.width);
            int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec,
                    getPaddingTop() + getPaddingBottom() + getPaddingBottom() + getDecoratedBottom(view), p.height);
            view.measure(childWidthSpec, childHeightSpec);

            // Get decorated measurements
            measuredDimension[0] = getDecoratedMeasuredWidth(view) + p.leftMargin + p.rightMargin;
            measuredDimension[1] = getDecoratedMeasuredHeight(view) + p.bottomMargin + p.topMargin;
            recycler.recycleView(view);
        }
    }
}

更新于2019/07/27


清空ViewPager显示Fragment的时的缓存

if (mContentVp.getAdapter() != null) {
     Class<? extends FragmentManager> aClass = getChildFragmentManager().getClass();
     try {
         Field f = aClass.getDeclaredField("mAdded");
         f.setAccessible(true);
         ArrayList<Fragment> list = (ArrayList) f.get(getChildFragmentManager());
         list.clear();

         f = aClass.getDeclaredField("mActive");
         f.setAccessible(true);
         SparseArray<Fragment> array = (SparseArray) f.get(getChildFragmentManager());
         array.clear();
     } catch (Exception e) {
         e.printStackTrace();
     }
 }

这个得从FragmentPagerAdapter的instantiateItem方法查看

public Object instantiateItem(ViewGroup container, int position) {
    if (mCurTransaction == null) {
        mCurTransaction = mFragmentManager.beginTransaction();
    }

    final long itemId = getItemId(position);

    // Do we already have this fragment?
    String name = makeFragmentName(container.getId(), itemId);
    Fragment fragment = mFragmentManager.findFragmentByTag(name);
    if (fragment != null) {
        if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
        mCurTransaction.attach(fragment);
    } else {
        fragment = getItem(position);
        if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
        mCurTransaction.add(container.getId(), fragment,
                makeFragmentName(container.getId(), itemId));
    }
    if (fragment != mCurrentPrimaryItem) {
        fragment.setMenuVisibility(false);
        fragment.setUserVisibleHint(false);
    }

    return fragment;
}

定位到这一句mFragmentManager.findFragmentByTag(name);,继续查找FragmentManager的实现类FragmentManagerImpl
中的findFragmentByTag方法

public Fragment findFragmentByTag(String tag) {
    if (tag != null) {
        // First look through added fragments.
        for (int i=mAdded.size()-1; i>=0; i--) {
            Fragment f = mAdded.get(i);
            if (f != null && tag.equals(f.mTag)) {
                return f;
            }
        }
    }
    if (mActive != null && tag != null) {
        // Now for any known fragment.
        for (int i=mActive.size()-1; i>=0; i--) {
            Fragment f = mActive.valueAt(i);
            if (f != null && tag.equals(f.mTag)) {
                return f;
            }
        }
    }
    return null;
}

观察mAdded和mActive 变量,他们就是用来存储Fragment的容器

final ArrayList<Fragment> mAdded = new ArrayList<>();
SparseArray<Fragment> mActive;

所以如果想要ViewPager重新初始化已加载的Fragment的话,就得通过反射清空这2个容器.

更新于2019/08/7


shape只有上下边框(shape只有任意一边边框)

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">

    <!--    只有上下边框的shape    -->

    <item>
        <shape>
            <!--    边框颜色    -->
            <solid android:color="@color/color_d6d6d6"/>
        </shape>
    </item>
    <!--    想保留哪一边就设置哪一边边框就好了,列如只有上边框:top="1dp"    -->
    <item android:top="1dp" android:bottom="1dp">
        <shape>
            <solid android:color="@color/color_ffffff"/>
        </shape>
    </item>
</layer-list>

android View 截屏

详情


ViewPager获取当前显示页面的View

在ViewPager的PagerAdapter里面有一个回调方法,源码如下:

/**
 * Called to inform the adapter of which item is currently considered to
 * be the "primary", that is the one show to the user as the current page.
 *
 * @param container The containing View from which the page will be removed.
 * @param position The page position that is now the primary.
 * @param object The same object that was returned by
 * {@link #instantiateItem(View, int)}.
 */
public void setPrimaryItem(ViewGroup container, int position, Object object) {
    setPrimaryItem((View) container, position, object);
}

我们只需要重写该方法,然后记录当前的View

@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
    this.mCurrentView = (View) object;
}

之后再对外提供一个方法获取该View即可

public View getPrimaryItem() {
	return mCurrentView;
}

保存相片到手机相册

public void saveImageToGallery(Context context, Bitmap bmp) {
        // 首先保存图片
        File picDir;
        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            picDir = new File(context.getExternalCacheDir(), "image");
        } else {
            picDir = new File(context.getCacheDir(), "image");
        }
        if (!picDir.exists()) {
            picDir.mkdir();
        }
        String fileName = System.currentTimeMillis() + ".jpg";
        File file = new File(picDir, fileName);
        try {
            FileOutputStream fos = new FileOutputStream(file);
            bmp.compress(Bitmap.CompressFormat.JPEG, 100, fos);
            fos.flush();
            fos.close();
            Toast.makeText(context, "图片已生成", Toast.LENGTH_SHORT).show();

            // 其次把文件插入到系统图库
            MediaStore.Images.Media.insertImage(context.getContentResolver(),
                    file.getAbsolutePath(), fileName, null);
            // 最后通知图库更新
            Uri uri;
            /*if (Build.VERSION.SDK_INT >= 24) { //7.0获取uri的方式变了
                String authority = getPackageName() + ".fileprovider"; //授权只要保证是唯一字符串即可,需保证和清单文件注册时的provider的name一致
                uri= FileProvider.getUriForFile(this, authority, file);
            } else {
                uri= Uri.fromFile(file);//7.0之前的方式
            }*/
           
           /* uri = Uri.parse("file://" + file.getAbsolutePath());
            context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri));//据说部分手机不行
            */
            final MediaScannerConnection[] mScanner = {null};
            MediaScannerConnection mediaConnection = new MediaScannerConnection(context, new MediaScannerConnection.MediaScannerConnectionClient() {
                @Override
                public void onMediaScannerConnected() {
                    mScanner[0].scanFile(file.getAbsolutePath(), file.getName());
                }

                @Override
                public void onScanCompleted(String path, Uri uri) {
                    mScanner[0].disconnect();
                    mScanner[0] = null;
                    ToastUtils.showShort("图片已保存至相册");
                }
            });
            mediaConnection.connect();
            mScanner[0] = mediaConnection;
        } catch (Exception e) {
            e.printStackTrace();
            Toast.makeText(context, "图片生成失败", Toast.LENGTH_SHORT).show();
        }
    }

递归删除目录及其所有子目录内容

public static boolean deleteAllFile(File sourceFile) {
	if (!sourceFile.exists()) {
		return false;
	}
	File[] files = sourceFile.listFiles();
	if (files != null && files.length > 0) {
		for (int i = 0; i < files.length; i++) {
			File file = files[i];
			if (file.isFile()) {
				file.delete();
			} else if (file.isDirectory()) {
				deleteAllFile(file);
			}
			if (i == (files.length - 1)) {
				sourceFile.delete();
			}
		}
	} else {
		sourceFile.delete();
	}
	return true;
}

getLocationInWindow 和 getLocationOnScreen区别

在这里插入图片描述
以控件C为例:
getLocationInWindow是以B为原点的C的左上角坐标
getLocationOnScreen是以A为原点的C的左上角坐标

使用方式:

int[] location = new int[2];
v.getLocationOnScreen(location);
int x = location [0];//获取x坐标
int y = location [1];//获取y坐标

getWindowVisibleDisplayFrame的用法

getWindowVisibleDisplayFrame()是View类下的一个方法,用来获取当前窗口可视区域的大小。该方法原型为:

public void getWindowVisibleDisplayFrame(Rect outRect);

outRect中保存了可视区域的范围,如left, top, right, bottom。

该方法使用注意事项:

  1. 调用该方法的view对象必须在有效的window中,比如activity,fragment或者dialog的layout中。类似new TextView(context).getWindowVisibleDisplayFrame(rect)无法得到正确的结果。

  2. 该方法必须在view已经attach到window时调用才能得到期望的正确结果。比如我们可以在Activity、Fragment和Dialog的onWindowFocusChanged()方法中执行,在view的onAttachedToWindow()中可能无法获得正确结果

  3. outRect所表示的只是窗体可见范围,其会受到系统状态栏,虚拟键盘和导航栏的影响,状态栏主要影响outRect的top值,虚拟键盘和导航栏会影响outRect的bottom值


DecorView的介绍

详情


保留2位小数

public class Test {
    public static void main(String[] args) {
        double d = 756.2345566;

        //方法一:最简便的方法,调用DecimalFormat类
        DecimalFormat df = new DecimalFormat("0.00");//0和#都是占位符,不同点是0在小数为不够的时候会用0补,#的话不会, 正数位无影响.
        System.out.println(df.format(d));

        //方法二:直接通过String类的format函数实现
        System.out.println(String.format("%.2f", d));

        //方法三:通过BigDecimal类实现
        BigDecimal bg = new BigDecimal(d);
        double d3 = bg.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue();
        System.out.println(d3);

        //方法四:通过NumberFormat类实现
        NumberFormat nf = NumberFormat.getNumberInstance();
        nf.setMaximumFractionDigits(2);
        System.out.println(nf.format(d));

    }
}

软键盘弹出自动时滑动到指定View的下方显示

/**
 * 处理软键盘弹出界面滚动
 * Created by mChenys on 2019/12/20.
 */

public class SoftInputUtil {


    // 控制是否移动布局。比如只有密码输入框获取到焦点时才执行。
    public static boolean flag = true;

    public interface Callback {
        //软件盘状态变化回调
        void onKeyboardStateChange(boolean isOpen);
        //页面滚动前回调
        int beforeScroll();
    }

    /**
     * @param act          activiry用于获取底部导航栏高度。
     * @param root         最外层布局,需要调整的布局
     * @param scrollToView 被键盘遮挡的scrollToView,滚动root,使scrollToView在root可视区域的底部
     */
    public static void controlKeyboardLayout(Context act, final View root, final View scrollToView, Callback callback) {
        final int navigationBarHeight = getNavigationBarHeight(act);

        root.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                Rect rect = new Rect();
                //获取root在窗体的可视区域
                root.getWindowVisibleDisplayFrame(rect);
                //根View的高度
                int decorViewHeight = root.getRootView().getHeight();
                //获取root在窗体的不可视区域高度(被其他View遮挡的区域高度)
                int rootInvisibleHeight = decorViewHeight - rect.bottom;
                //若不可视区域高度大于100,则键盘显示
                if (rootInvisibleHeight > navigationBarHeight && flag) {
                    int[] location = new int[2];
                    //获取scrollToView在窗体的坐标
                    scrollToView.getLocationInWindow(location);
                    //计算root滚动高度,使scrollToView在可见区域
                    int srollHeight = (location[1] + scrollToView.getHeight()) - rect.bottom;
                    if (root.getScrollY() != 0) {// 如果已经滚动,要根据上次滚动,重新计算位置。
                        srollHeight += root.getScrollY();
                    }
                    int dy = 0; //偏移量,可以调节滚动距离
                    if (null != callback) dy = callback.beforeScroll();
                    root.scrollTo(0, srollHeight + dy);
                    if (null != callback) callback.onKeyboardStateChange(true);
                } else {
                    //键盘隐藏
                    root.scrollTo(0, 0);
                    if (null != callback) callback.onKeyboardStateChange(false);
                }
            }
        });
    }

    /**
     * 获取底部导航栏高度
     *
     * @param act
     * @return
     */
    private static int getNavigationBarHeight(Context act) {
        Resources resources = act.getResources();
        int resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android");
        int height = resources.getDimensionPixelSize(resourceId);
        Log.v("dbw", "Navi height:" + height);
        return height;
    }
}

exoplayer获取当前帧画面

这个播放器支持surfaceView和TextureView 两种视图模式的展示,
对于surfaceView好像做不到转换成bitmap 但是TextureView 就很简单了.

PlayerView playerView = (PlayerView) LayoutInflater.from(application)
                .inflate(R.layout.layout_exo_player_view, null, false);
TextureView textureView = (TextureView)playerView.getVideoSurfaceView();
Bitmap bitmap = textureView.getBitmap();

通过adb shell命令查看当前与用户交互的activity

方法一:

adb shell dumpsys activity activities | sed -En -e '/Running activities/,/Run #0/p'

在这里插入图片描述
其中TaskRecord即为查询到的记录。其中 com.android.calendar为包名,.AllInOneActivity为对应的Activity名称。

方法二:

adb shell dumpsys activity | grep -i run

查询结果为:
在这里插入图片描述
如果在Windows下使用时,则需先通过adb shell进入到shell里,然后再执行命令, 例如方法二可以这样操作:
在这里插入图片描述


Android SpannableString 自定义圆角背景

参考


RecyclerView获取滚动距离


import android.content.Context;
import android.view.View;

import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import java.util.HashMap;
import java.util.Map;

/**
 * 自定义LayoutManager 
 */
public class OffsetLinearLayoutManager extends LinearLayoutManager {
    public OffsetLinearLayoutManager(Context context) {
        super(context);
    }

    private Map<Integer, Integer> heightMap = new HashMap<>();

    /**
     * 统计列表已展示过的item的高度,在每次布局完成的时候,用一个map记录positon位置item对应的view的高度
     */
    @Override
    public void onLayoutCompleted(RecyclerView.State state) {
        super.onLayoutCompleted(state);
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View view = getChildAt(i);
            heightMap.put(i, view.getHeight());
        }
    }

    /**
     * 找到当前屏幕第一个可见item的position,通过heightMap循环累加0到positon的item高度,
     * 再加上第一个可见item不可见部分高度。最终得到整个列表的滑动偏移
     *
     * @param state
     * @return
     */
    @Override
    public int computeVerticalScrollOffset(RecyclerView.State state) {
        if (getChildCount() == 0) {
            return 0;
        }
        try {
            int firstVisiablePosition = findFirstVisibleItemPosition();
            View firstVisiableView = findViewByPosition(firstVisiablePosition);
            int offsetY = -(int) (firstVisiableView.getY());
            for (int i = 0; i < firstVisiablePosition; i++) {
                offsetY += heightMap.get(i) == null ? 0 : heightMap.get(i);
            }
            return offsetY;
        } catch (Exception e) {
            return 0;
        }
    }
}


解析动态style的属性

public static void applyStyle(int styleId){
	int[] ints = {
                R.attr.cust_color1,
                R.attr.cust_color2,
                R.attr.cust_color3,
                R.attr.cust_drawable4
	};
	// 创建theme
	Resources.Theme theme = getResources().newTheme();
	// 使用指定的styleId
	theme.applyStyle(styleId, true);
	
	// 动态解析属性
	TypedArray typedArray = theme.obtainStyledAttributes(ints);
	int color1= typedArray.getColor(0, 0);
	int color2= typedArray.getColor(1, 0);
	int color3= typedArray.getColor(2, 0);
	Drawableb drawable4 = typedArray.getDrawable(3, 0);

}

然后使用时在res/values/styles.xml中定义要解析的style即可

<?xml version="1.0" encoding="utf-8"?>
 <style name="my_style1" >
	 <item name="cust_color1">@color/color_1</item>
	 <item name="cust_color2">@color/color_2</item>
	 <item name="cust_color3">@color/color_3</item>
	 <item name="cust_drawable4">@drawable/ic_background</item>
 </style>

并且在res/values/attrs.xml中自定义属性名

<?xml version="1.0" encoding="utf-8"?>
<resources>
	<attr name="cust_color1" format="reference"></attr>
	<attr name="cust_color2" format="color"></attr>
	<attr name="cust_color3" format="color"></attr>
	<attr name="cust_drawable4" format="reference"></attr>
<resources>

跳应用市场和搜索页

	//跳转应用商店
    private fun intentAppStore(packageName: String, markPackageName: String) {
        val uri = Uri.parse("market://details?id=$packageName")
        val intent = Intent(Intent.ACTION_VIEW, uri)
        intent.setPackage(markPackageName)
        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
        mContext.startActivity(intent)
    }

    // 跳去应用市场搜索app
    private fun intentSearchApp(appName: String) {
        val intent = Intent(Intent.ACTION_VIEW)
        intent.data = Uri.parse("market://search?q=pub:+" + appName)
        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
        mContext.startActivity(intent)
    }

将含有其他aar包的module打包成新的aar

如果直接将其它的aar包放到module的libs就开始打包,虽然也可以打成功,但是生成aar包放到项目中使用的时候如果访问该aar所依赖的其它aa的类就会报找不到目标类的异常,解决办法就是还得在项目中依赖这个aar所依赖的其他aar包,显然这并不是我们所期望的.

这里介绍正确的解决方案是使用fat-aar-android插件.

fat-aar可以解决aar嵌套aar打包的需求场景.

使用步骤如下:
1.编辑主工程的build.gradle文件

// Top-level build file where you can add configuration options common to all sub-projects/modules.
apply from: this.rootProject.file('project.gradle')
buildscript {
    ext.kotlin_version = '1.3.72'
    repositories {
        google()
        jcenter()
        maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
        maven { url "https://jitpack.io" }

        //1.添加fat插件仓库地址
        maven { url 'http://plugins.gradle.org/m2/' }
    }
    dependencies {
        classpath "com.android.tools.build:gradle:3.6.0"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        //2.添加插件
        classpath "com.kezong:fat-aar:1.3.3"
      
    }
}

allprojects {
    repositories {
        //3.配置flatDir 
        flatDir { dirs 'libs' }
        google()
        jcenter()
        maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
        maven { url "https://jitpack.io" }
        
        //4.添加fat插件仓库地址
        maven { url 'http://plugins.gradle.org/m2/' }
    }

}

task clean(type: Delete) {
    delete rootProject.buildDir
}

2.编辑需要打包生成aar的module工程的build.gradle文件

apply plugin: 'com.android.library'
apply plugin: 'com.kezong.fat-aar'  //1.引用fat插件

android {
   ...
}


dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    // 通过embed来嵌套其他aar包,这里以阿里百川的aar为例,aar包是放在module的libs目录的
    embed(name: 'alibabauth_core-2.0.0.11', ext: 'aar')
    embed(name: 'alibabauth_ext-2.0.0.11', ext: 'aar')
    embed(name: 'alibabauth_ui-2.0.0.11', ext: 'aar')
    embed(name: 'alibc_link_partner-4.1.15', ext: 'aar')
    embed(name: 'AlibcTradeBiz-4.0.0.16', ext: 'aar')
    embed(name: 'AlibcTradeCommon-4.0.0.16', ext: 'aar')
    embed(name: 'avmpaar3-5.4.36', ext: 'aar')
    embed(name: 'nb_trade-4.0.0.16', ext: 'aar')
    embed(name: 'securitybodyaar3-5.4.99', ext: 'aar')
    embed(name: 'securityguardaar3-5.4.171', ext: 'aar')
    embed(name: 'sgmiddletieraar3-5.4.9', ext: 'aar')
}


确保module的libs存在这些aar包
在这里插入图片描述
ps: 如果遇到依赖的外部sdk(aar)相互之间出现资源重复冲突,例如app_name冲突可以在主工程的gradle.properties中加入:

android.disableResourceValidation=true

通过RecyclerView和item获取对应的ViewHolder

  //先获取item
  View item= mRecyclerView.getChildAt(0);
  //或者
  LinearLayoutManager layoutManager = (LinearLayoutManager) mRecyclerView.getLayoutManager();
  item= layoutManager.findViewByPosition(0);
  //再通过child获取ViewHolder 
  RecyclerView.ViewHolder viewHolder = mRecyclerView.findContainingViewHolder(item);

通过RecyclerView的findChildViewUnder(x,y)返回指定位置的childView

方法参数是 (float x, float y),作用是查找指定坐标点 (x, y) 落于 RecyclerView 的哪个子 View 上面,这里的坐标点是以 RecyclerView 控件作为坐标轴,并不是以屏幕左上角作为坐标原点。方法的源码如下:

public View findChildViewUnder(float x, float y) {
    final int count = mChildHelper.getChildCount();
    for (int i = count - 1; i >= 0; i--) {
        final View child = mChildHelper.getChildAt(i);
        final float translationX = child.getTranslationX();
        final float translationY = child.getTranslationY();
        // 其实就是判断传入的x,y是否落在某个子View的范围内,是的话就返回
        if (x >= child.getLeft() + translationX
                && x <= child.getRight() + translationX
                && y >= child.getTop() + translationY
                && y <= child.getBottom() + translationY) {
            return child;
        }
    }
    return null;
}

这个ChildHelper类,它会协助获取RecyclerView中的childVIew,并提供忽略隐藏Children的功能,也就是说,调它的getChildAt只会在当前显示的Children中去查找,如果想查HiddenChildren,那么可以调getUnfilteredChildAt。


关闭RecyclerView 的测量、布局操作

调用setLayoutFrozen()该方法传入true,其实等效于关闭了 ReyclerView 的刷新,不管数据源发生了何种变化,不管用户滑动了多长距离,都不会去刷新界面,看起来就像是不响应一样,但等到再次调用该方法参数传入 false 后,就会立马去根据变化后的数据源来刷新界面了。

使用场景还是有的,假如有些场景暂时不想让 RecyclerView 去刷新,比如此时有其他动画效果正在执行中,RecyclerView 刷新多少会有些耗时,万一导致了当前动画的卡顿,那么体验就不好了。所以,这个时候可以暂时将 ReyclerView 的刷新关闭掉,但后面记得要重新开启。
ps:新版本的RecyclerView该方法已标注为过时方法,可以使用suppressLayout代替.

通过RecyclerView的findContainingItemView(view)得到包含指定View的ItemView

该方法参数是 (View view),作用正如命名上的理解,查找含有指定 View 的 ItemView,而 ItemView 是指 RecyclerView 的直接子 View
通常,RecyclerView 的 Item 布局都不会简单到直接就是一个具体的 TextView,往往都挺复杂的。


全屏并让视图延伸到刘海区域

通常启动页都会设置为全屏展示,但是遇到刘海屏的时候,会发现刘海区域是黑的,如下图所示
在这里插入图片描述
解决办法如下:

<style name="AppThemeFull" parent="Theme.AppCompat.Light.NoActionBar">
   <item name="android:statusBarColor">@android:color/transparent</item>
    <item name="android:navigationBarColor">@android:color/transparent</item>
    <item name="windowActionBar">false</item>
    <item name="android:windowFullscreen">true</item>
    <item name="windowNoTitle">true</item>
    <!--允许页面延伸到刘海区域,需要在values-v28中定义-->
    <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>

本人华为荣耀20上试过有效,小米和vivo没试过,如果不行的话,可以试试如下方式:

 <application
 
  <!--vivo刘海屏适配-->
  <meta-data
       android:name="android.max_aspect"
       android:value="2.4" />

   <!--小米刘海屏适配-->
   <meta-data
       android:name="notch.config"
       android:value="portrait|landscape" />

</application>

SmartRefreshLayout自定义刷新头

只需要继承SimpleComponent和实现RefreshHeader接口即可,然后根据需求实现对应的文案或者动画效果


import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.annotation.NonNull;

import com.scwang.smart.refresh.layout.api.RefreshHeader;
import com.scwang.smart.refresh.layout.api.RefreshLayout;
import com.scwang.smart.refresh.layout.constant.RefreshState;
import com.scwang.smart.refresh.layout.constant.SpinnerStyle;
import com.scwang.smart.refresh.layout.simple.SimpleComponent;

/**
 * @Author: mChenys
 * @Date: 2021/2/20
 * @Description:自定义刷新头
 */
public class DemoRefreshHeader extends SimpleComponent implements RefreshHeader {
    private TextView mTitleText;
    private ImageView mLoadingView;
    private ObjectAnimator mObjectAnimator;

    public DemoRefreshHeader(Context context) {
        super(context, null, 0);
        View view = View.inflate(getContext(), R.layout.layout_header_demo, this);
        mTitleText = view.findViewById(R.id.tv_title);
        mLoadingView = view.findViewById(R.id.iv_loading);
        mObjectAnimator = ObjectAnimator.ofFloat(mLoadingView, "rotation", 0f, 360f);
        mObjectAnimator.setDuration(100L);
        mObjectAnimator.setRepeatMode(ValueAnimator.RESTART);
        mObjectAnimator.setRepeatCount(ValueAnimator.INFINITE);

    }

    /**
     * 头部在下拉时的变化方式
     *
     * @return Translate, //平行移动        特点: HeaderView高度不会改变,
     * Scale, //拉伸形变            特点:在下拉和上弹(HeaderView高度改变)时候,会自动触发OnDraw事件
     * FixedBehind, //固定在背后    特点:HeaderView高度不会改变,
     * FixedFront, //固定在前面     特点:HeaderView高度不会改变,
     * MatchLayout//填满布局        特点:HeaderView高度不会改变,尺寸充满 RefreshLayout
     */
    @NonNull
    @Override
    public SpinnerStyle getSpinnerStyle() {
        return SpinnerStyle.Translate;
    }

    /**
     * 松手且达到刷新条件时回调
     *
     * @param refreshLayout
     * @param height
     * @param maxDragHeight
     */
    @Override
    public void onReleased(@NonNull RefreshLayout refreshLayout, int height, int maxDragHeight) {
        onStartAnimator(refreshLayout, height, maxDragHeight);
    }

    /**
     * 动画执行回调
     *
     * @param refreshLayout
     * @param height
     * @param maxDragHeight
     */
    @Override
    public void onStartAnimator(@NonNull RefreshLayout refreshLayout, int height, int maxDragHeight) {
        if (mLoadingView.getVisibility() != View.VISIBLE) {
            mLoadingView.setVisibility(View.VISIBLE);
            mObjectAnimator.start();
        }
    }

    /**
     * 刷新完成时回调
     *
     * @param refreshLayout
     * @param success
     * @return
     */
    @Override
    public int onFinish(@NonNull RefreshLayout refreshLayout, boolean success) {
        mObjectAnimator.cancel();
        mLoadingView.setVisibility(View.GONE);
        if (success) {
            mTitleText.setText("刷新完成");
        } else {
            mTitleText.setText("刷新失败");
        }
        return 500;//延迟500毫秒之后再弹回
    }

    /**
     * 持续下拉持续回调
     *
     * @param isDragging
     * @param percent
     * @param offset
     * @param height
     * @param maxDragHeight
     */
    @Override
    public void onMoving(boolean isDragging, float percent, int offset, int height, int maxDragHeight) {
        // 按下拉百分比旋转
        mLoadingView.setRotation(percent * 360);
    }

    /**
     * 状态改变时回调
     *
     * @param refreshLayout
     * @param oldState
     * @param newState
     */
    @Override
    public void onStateChanged(@NonNull RefreshLayout refreshLayout, @NonNull RefreshState oldState, @NonNull RefreshState newState) {
        switch (newState) {
            case None:
            case PullDownToRefresh:
                mTitleText.setText("下拉刷新");
                mLoadingView.setVisibility(View.VISIBLE);
                break;
            case Refreshing:
            case RefreshReleased:
                mTitleText.setText("正在刷新...");
                mLoadingView.setVisibility(View.GONE);
                break;
            case ReleaseToRefresh:
                mTitleText.setText("释放立即刷新");
                break;
        }
    }
}

上面的效果就是下拉过程中文案会由"下拉刷新"->“释放立即刷新”->“释放立即刷新”->“刷新完成”, 而动画效果就是有一个加载圈在持续下拉的过程会根据下拉百分比来旋转,当变成"正在刷新…"的时候会播放旋转动画,当刷新完成后会取消动画

使用方式也很简单,全局使用可以在初始的时候配置如下:

 //设置全局的Header构建器
 SmartRefreshLayout.setDefaultRefreshHeaderCreator(new DefaultRefreshHeaderCreator() {

     @NonNull
     @Override
     public RefreshHeader createRefreshHeader(@NonNull Context context, @NonNull RefreshLayout layout) {
         // 返回上面自定义的刷新头
         return new DemoRefreshHeader(context);
     }

 });

将图片绘制到指定大小的View上面

// 重写View的onSizeChanged获取View的宽高
 override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        mWidth = w
        mHeight = h
 }

 override fun onDraw(canvas: Canvas?) {
     super.onDraw(canvas)
     drawBg(canvas)
 }

 // 绘制背景
 private fun drawBg(canvas: Canvas?) {
     // 1.获取资源图片
     val srcBitmap = BitmapFactory.decodeResource(resources, R.drawable.img_bg)
     // 2.创建空的bitmap ,宽高要等于View的宽高
     val bgBitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888)
     // 3.将资源图片绘制到空的bitmap中
     val bgCanvas = Canvas(bgBitmap)
     bgCanvas.drawBitmap(srcBitmap, null, Rect(0, 0, mWidth, mHeight), mPaintBg)
     // 4.将bgBitmap绘制到View上
     canvas?.drawBitmap(bgBitmap , null, Rect(0, 0, mWidth, mHeight), mPaintBg)
 }

获取Bitmap指定区域和大小的内容

 val srcBitmap = BitmapFactory.decodeResource(resources, R.drawable.img_bg)
 val newBitmap = Bitmap.createBitmap(
            srcBitmap ,  // 原图
            LINE_X.toInt(), // x坐标
            LINE_Y.toInt(), // y坐标
            CARD_SIZE, // 需要截取的大小
            CARD_SIZE // 需要截取的大小
        )

IdleHandler (闲时机制)

IdleHandler是一个回调接口,可以通过MessageQueue的addIdleHandler添加实现类。当MessageQueue中的任务暂时处理完了(没有新任务或者下一个任务延时在之后),这个时候会回调这个接口,返回false,那么就会在回调之后移除它,返回true就会在下次message处理完了的时候继续回调。

registerForActivityResult的使用

registerForActivityResult()是startActivityForResult()的替代,简化了数据回调的写法
参考


谷歌浏览器调试移动端h5页面

1.安卓手机打开开发者模式连接上电脑
2.在谷歌浏览器中访问地址:

chrome://inspect/#devices

3.刷新,显示连接上的设备以及打开的网页,点击inspect就可以查看手机网页上的dom元素,在电脑上调试h5页面。
在这里插入图片描述

transitive和changing的含义

compile('com.crashlytics.sdk.android:answers:1.3.10@aar') {
    transitive = true;
}

简单地说, 由于该语句使用 @aar notation, 所以 gradle 只会下载这一个 aar 文件, 而不会顺带着下载这个 aar 所需要的依赖文件. 所以需要 transitive 让依赖能够自动被下载. 一般而言, 去掉 @aar 以及 { transitive = true } 不会有任何问题.

compile("com.tencent.tinker:tinker-android-lib:1.7.6") { 
    changing = true 
}

这个字段的含义是: 是否自动更新该版本的依赖文件, 但是! 如果像本例中指定了版本, 则只会更新相同版本. 默认是 24 小时检查该依赖. 当然也可以指定检查更新的周期.


gradle 每次运行都会下载依赖的解决办法

在执行build、compile等任务时会解析项目配置的依赖并按照配置的仓库去搜寻下载这些依赖。默认情况下,Gradle会依照Gradle缓存->你配置的仓库的顺序依次搜寻这些依赖,并且一旦找到就会停止搜索。如果想要忽略本地缓存每次都进行远程检索可以通过在执行命令时添加–refresh-dependencies参数来强制刷新依赖

gradle build --refresh-dependencies

当远程仓库上传了相同版本依赖时,有时需要为缓存指定一个时效去检查远程仓库的依赖笨版本,Gradle提供了

cacheChangingModulesFor(int, java.util.concurrent.TimeUnit),
cacheDynamicVersionsFor(int, java.util.concurrent.TimeUnit)两个方法来设置缓存的时效
configurations.all {
    //每隔24小时检查远程依赖是否存在更新
     resolutionStrategy.cacheChangingModulesFor 24, 'hours'
    //每隔10分钟..
    //resolutionStrategy.cacheChangingModulesFor 10, 'minutes'
    // 采用动态版本声明的依赖缓存10分钟,也就是changing: true
    resolutionStrategy.cacheDynamicVersionsFor 10*60, 'seconds'
}

dependencies {
    // 添加changing: true
    compile group: "group", name: "module", version: "1.1-SNAPSHOT", changing: true
    //简写方式
    //compile('group:module:1.1-SNAPSHOT') { changing = true }
}

Gradle在按照配置的仓库去搜寻下载依赖时,下载的依赖默认会缓存到USER_HOME/.gradle/caches目录下,当然也可以手工修改这个位置。

Gradle还提供了一种离线模式,可以让你构建时总是采用缓存的内容而无需去联网检查,如果你并未采用动态版本特性且可以确保项目中依赖的版本都已经缓存到了本地,这无疑是提高构建速度的一个好选择。开启离线模式只需要在执行命令时候添加–offline参数即可。当然,采用这种模式的也是有代价的,如果缓存中搜寻不到所需依赖会导致构建失败。

gradle build --offline

同时兼容高本gradle和低版本gradle插件版本

在项目根gradle文件中修改如下,同时兼容gradle3.5.3和2.3.3插件版本

buildscript {
    repositories {
        mavenLocal()
        jcenter()
        google()
    }
    dependencies {
        if (project.hasProperty('GRADLE_3') && GRADLE_3.equalsIgnoreCase('TRUE')) {
            classpath 'com.android.tools.build:gradle:3.5.3' //gradle3.0之后版本
        } else {
            classpath 'com.android.tools.build:gradle:2.3.3' //gradle3.0之前版本
        }
        ...
    }
    def is_gradle_3() {
       return hasProperty('GRADLE_3') && GRADLE_3.equalsIgnoreCase('TRUE')
    }
}

然后在app的gradle文件添加依赖的时候也要区分下,如下以tinker依赖添加为例

dependencies {
    if (is_gradle_3()) {
        implementation fileTree(dir: 'libs', include: ['*.jar'])
        testImplementation 'junit:junit:4.12'
        implementation "androidx.appcompat:appcompat:1.1.0"
        api("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }
        implementation("com.tencent.tinker:tinker-android-loader:${TINKER_VERSION}") { changing = true }
        annotationProcessor("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
        compileOnly("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
        compileOnly("com.tencent.tinker:tinker-android-anno-support:${TINKER_VERSION}") { changing = true }
        implementation "androidx.multidex:multidex:2.0.1"
    } else {
        compile fileTree(dir: 'libs', include: ['*.jar'])
        testCompile 'junit:junit:4.12'
        compile "androidx.appcompat:appcompat:1.1.0"
        compile("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }
        provided("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
        provided("com.tencent.tinker:tinker-android-anno-support:${TINKER_VERSION}") { changing = true }
        compile "androidx.multidex:multidex:2.0.1"
    }
}

multiDexKeepFile属性 和multiDexKeepProguard属性的区别

这2者的作用都是为了解决multiDex拆分包的过程中会出现在启动时需要用到的某个类没有在主dex中找到而产生java.lang.NoClassDefFoundError的崩溃问题.

1)使用multiDexKeepFile
您在 multiDexKeepFile 中指定的文件应该每行包含一个类,并且类采用 com/example/MyClass.class 格式。例如,您可以创建一个名为 multidex-config.txt 的文件,如下所示:

com/example/MyClass.class
com/example/MyOtherClass.class

然后,您可以针对构建类型声明该文件,如下所示:

android {
    buildTypes {
        release {
            multiDexKeepFile file('multidex-config.txt')
            ...
        }
    }
}

请注意,Gradle 会读取相对于 build.gradle 文件的路径,因此如果 multidex-config.txt 与 build.gradle 文件在同一目录中,以上示例将有效。

2)使用multiDexKeepProguard
multiDexKeepProguard 文件使用与 Proguard 相同的格式,并且支持全部 Proguard 语法。如需详细了解 Proguard 格式和语法,请参阅 Proguard 手册中的 Keep 选项一节。

您在 multiDexKeepProguard 中指定的文件应该在任何有效的 ProGuard 语法中包含 -keep 选项。例如 -keep com.example.MyClass.class。您可以创建一个名为 multidex-config.pro 的文件,如下所示:

-keep class com.example.MyClass
-keep class com.example.MyClassToo

如果您要指定软件包中的所有类,文件将如下所示:

-keep class com.example.** { *; } // All classes in the com.example package

然后,您可以针对构建类型声明该文件,如下所示:

android {
    buildTypes {
        release {
            multiDexKeepProguard file('multidex-config.pro')
            ...
        }
    }
}

更多使用说明可以参考官方文档


build.gradle配置中packagingOptions{}选项说明

参考


gradle配置资源路径

def isModule = true

android {
   ...
    defaultConfig {
      ...
    }
   ...
  sourceSets {
        main {
            // 设置jinLibs目录为src/main/libs
            jniLibs.srcDirs = ['libs']
            // 设置资源文件目录
            res.srcDirs = [
                    'src/main/res',
                    'src/main/res-debug',
            ]
            if (!isModule) {
                // 组件化模式下,独立运行的清单文件路径
                manifest.srcFile 'src/main/debug/AndroidManifest.xml'
            } else {
                // 集成化模式,作为app的依赖module运行的清单文件路径
                manifest.srcFile 'src/main/AndroidManifest.xml'
                java {
                    // 集成化模式下需要排除的目录,可以用通配符标示
                    exclude "**/debug/**"
                }
            }
        }
    }

如下图所示
在这里插入图片描述

Android library打包生成jar包

当我们的Android library是纯代码的情况,如果你只想生成jar包而不想生成aar包,那么可以使用gradle的task来生成

//打包生成jar包
task makeJar(type: Jar) {
    //指定生成的jar名
    baseName 'mylibrary'
    //从哪里打包class文件
    from(new File(buildDir,"intermediates/javac/debug/classes/com/sample/mylibrary"))
    //打包到jar后的目录结构
    into('com/sample/mylibrary')
    //去掉不需要打包的目录和文件
    exclude('BuildConfig.class', 'R.class')
    //去掉R$开头的文件
    exclude { it.name.startsWith('R$'); }
}

执行完task后就会在build/libs目录下生成对应的jar包了
在这里插入图片描述


cmd命令行中logcat输出日志中文乱码

在命令行使用adb logcat命令直接输出日志中文内容显示乱码,原因是中文系统中cmd命令行窗口默认的编码是GBK,而LogCat打印的日志是UTF-8编码,所以adb logcat命令输出的中文内容显示乱码。

修改cmd命令行窗口字符编码即可解决logcat日志中文显示乱码问题:

1.cmd命令行窗口字符编码切换为UTF-8,命令行中执行:chcp 65001

2.修改cmd窗口字体属性,在命令行标题栏上点击右键,选择”属性”->”字体”,将字体修改为”Lucida Console”,点击确定后生效。

修改后logcat -f log.txt方式输出日志到文本文件的乱码问题同样可以解决。

部分字符编码对应代码:
65001——UTF-8
936——简体中文
950——繁体中文
437——美国/加拿大英语
932——日文
949——韩文
866——俄文

cmd窗口字符编码切换回中文:chcp 936


从未安装的APK中获取默认启动的Activity

参考


使用aapt2 修改apk打包的资源id

参考


使用AS查看framework源码

Android有两种类型的API是不能经由SDK访问的。

第一种是位于com.android.internal包中的API。我将称之为internal API。
第二种API类型是一系列被标记为@hide属性的类和方法。
Internal和hidden API的编译时 vs 运行时
当你使用Android SDK进行开发的时候,你引用了一个非常重要的jar文件——android.jar。它位于Android SDK平台的文件夹中(SDK_DIR/platforms/platform-X/android.jar)。这个android.jar移掉了com.android.internal包中所有的类,也移掉了部分标记有@hide的类,枚举,字段和方法。

每个模拟器或真机在运行时都会有一个等同android.jar的东西,叫做framework.jar,它和android.jar等同,而其未移掉internal API和hidden API。当你在设备上启动应用程序时,它将加载framework.jar。

但这样对开发者来说,并不能友好地访问,因此,我将向大家展示不通过反射如何使用这些API。

解决办法如下:
完整版android.jar下载对应版本的android.jar,然后替换 ${Android Sdk}/platforms/andorid-api/ 下的android.jar即可

BottomNavigationView 底部导航栏加入 红点数字提示

参考


TabLayout去掉点击的默认背景色

 <com.google.android.material.tabs.TabLayout
            android:id="@+id/bottom_navigation"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/white"
            android:paddingTop="5dp"
            android:paddingBottom="5dp"
            app:tabBackground="@android:color/transparent"
            app:tabIndicatorColor="@android:color/transparent"
            app:tabRippleColor="@android:color/transparent" />

主要就是app:tabRippleColor的属性


Android9,10反射限制问题分析以及解决

public final class ReflectionLimit {
    
    private static Object sVMRuntime;
    private static Method setHiddenApiExemptions;

    static {
        if ( Build.VERSION.SDK_INT >= 29 ) {
            try {
                Method forName = Class.class.getDeclaredMethod("forName", String.class);
                Method getDeclaredMethod = Class.class.getDeclaredMethod("getDeclaredMethod", String.class, Class[].class);
                Class<?> vmRuntimeClass = (Class<?>) forName.invoke(null, "dalvik.system.VMRuntime");
                Method getRuntime = (Method) getDeclaredMethod.invoke(vmRuntimeClass, "getRuntime", null);
                setHiddenApiExemptions = (Method) getDeclaredMethod.invoke(vmRuntimeClass, "setHiddenApiExemptions", new Class[]{String[].class});
                setHiddenApiExemptions.setAccessible(true);
                sVMRuntime = getRuntime.invoke(null);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    //消除限制
    public static boolean clearLimit() {
        if (sVMRuntime == null || setHiddenApiExemptions == null) {
            return false;
        }
        try {
            setHiddenApiExemptions.invoke(sVMRuntime, new Object[]{new String[]{"L"}});
            return true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }
}

参考


Gradle强制刷新单模块依赖

参考


android:clipToPadding的使用

当父布局设置了padding,而又想子布局在滑动的时候可以无视padding,也就是在padding区域绘制(换句话说就是允许子View滑动的区域包含父容器的padding),此时就需要设置clipToPadding = false,默认是true。
具体可以查看ViewGroup的源码:

protected void dispatchDraw(Canvas canvas) {
        //查询是否设置了clipToPadding
        final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
        if (clipToPadding) { // 默认是true
            clipSaveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
            //对canvas设置裁减区域
            canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop,
                    mScrollX + mRight - mLeft - mPaddingRight,
                    mScrollY + mBottom - mTop - mPaddingBottom);
        }
    }

应用场景参考


RecyclerView嵌套RecyclerView的内存复用

总而言之 如果RecycledView的adapter是一样的话可以考虑共享一个对象池。
比如说: RecycledView嵌套RecycledView,里面的RecycledView大部分都adapter都一样。
注意 如果 LayoutManager是LinearLayoutManager或其子类(如GridLayoutManager) 需要手动开启这个特性:layout.setRecycleChildrenOnDetach(true)

class OuterAdapter extends RecyclerView.Adapter<OuterAdapter.ViewHolder> {
    // 创建复用池
    RecyclerView.RecycledViewPool mSharedPool = new RecyclerView.RecycledViewPool();

    ...

    @Override
public void onCreateViewHolder(ViewGroup parent, int viewType) {

   // 嵌套的子RecyclerView
   RecyclerView view = new RecyclerView(inflater.getContext());

     LinearLayoutManager innerLLM = new LinearLayoutManager(parent.getContext(), LinearLayoutManager.HORIZONTAL);
        // 布局主动开启内存复用
        innerLLM.setRecycleChildrenOnDetach(true);
        innerRv.setLayoutManager(innerLLM);
        // 设置复用池
        innerRv.setRecycledViewPool(mSharedPool);
        return new OuterAdapter.ViewHolder(innerRv);
    }
    ...

更新于2021/06/21


indexOf(String str, int fromIndex)和lastIndexOf(String str, int fromIndex)区别

indexOf(String str, int fromIndex)//从当前 fromIndex位置往从左往右查找最前一个
lastIndexOf(String str, int fromIndex)//从当前 fromIndex位置往从右往左查找最后一个

获取TextView指定span的起点和终点的x坐标

   // 获取span的起始位置x坐标
    private Pair<Integer, Integer> getSpanRangeX(TextView widget, Layout layout, int line,
     Spannable buffer, Object span) {
        int spanStart = buffer.getSpanStart(span); // span的起点index,从0开始
        int spanEnd = buffer.getSpanEnd(span); // span的终点index
        int lineStart = layout.getLineStart(line);// 当前行的start index
        int lineEnd = layout.getLineEnd(line);// 当前行的end index
        String text = widget.getText().subSequence(lineStart, lineEnd).toString(); // 当前行文本
        int width = (int) widget.getPaint().measureText(text); // 当前行文本宽度
        int spanStartX = (int) ((spanStart - lineStart) / (text.length() * 1.0f) * width); // span的起点x坐标
        int spanEndX = (int) ((spanEnd - lineStart) / (text.length() * 1.0f) * width); // span的终点x坐标
        return new Pair<>(spanStartX, spanEndX);
    }

至于如何获取TextView中的某一行line和指定的span,可以这样操作:

if (action == MotionEvent.ACTION_UP
                || action == MotionEvent.ACTION_DOWN
                || action == MotionEvent.ACTION_MOVE
                || action == MotionEvent.ACTION_CANCEL) {
    int x = (int) event.getX();
     int y = (int) event.getY();

     x -= widget.getTotalPaddingLeft();
     y -= widget.getTotalPaddingTop();

     x += widget.getScrollX();
     y += widget.getScrollY();

     Layout layout = widget.getLayout();
     int line = layout.getLineForVertical(y);
     int off = layout.getOffsetForHorizontal(line, x);
     // 获取点击的span,例如:ClickableSpan
     XxxSpan[] XxxSpans = buffer.getSpans(off, off, XxxSpan.class); // 这里XxxSpan换成具体的span即可.
}

图片Uri转File

public static File genNewFileFromUri(Context context, Uri uri) {
            File imgFile = context.getExternalCacheDir();
            File file = null;
            if (!imgFile.exists()) {
                imgFile.mkdir();
            }

            try {
                String mimeType = context.getContentResolver().getType(uri);
                String format = mimeType.contains("gif") ? ".gif" : ".jpg";
                file = new File(imgFile.getAbsolutePath(),System.currentTimeMillis() + format);
                // 使用openInputStream(uri)方法获取字节输入流
                InputStream fileInputStream = context.getContentResolver().openInputStream(uri);
                FileOutputStream fileOutputStream = new FileOutputStream(file);
                byte[] buffer = new byte[1024];
                int byteRead;
                while (-1 != (byteRead = fileInputStream.read(buffer))) {
                    fileOutputStream.write(buffer, 0, byteRead);
                }
                fileInputStream.close();
                fileOutputStream.flush();
                fileOutputStream.close();
            } catch (Exception e) {
                e.printStackTrace();
            }

            return file;
        }

toolbar修改返回按键颜色

参考

Kotlin自动findViewById神器-LayoutContainer

参考

RecycleView的layoutManager.setStackFromEnd(true);layoutManager.setReverseLayout(true);

参考

Android 设置TextView自动调整字体大小

参考

Java正则表达式Pattern.quote()方法详解

参考
通常用来构建表情emoji字符的Pattern,例如:

fun test() {
    val emoji = "[奋斗]"
    val pattern = Pattern.compile(Pattern.quote(emoji)) // Pattern.quote("[奋斗]") == \Q[奋斗]\E
    val spannableString = SpannableString("hello everyone [奋斗]")
    val matcher = pattern.matcher(spannableString)
    if(matcher.find()){
        val imageSpan: ImageSpan? = generateImageSpan(emoji)
        if (imageSpan != null) {
            spannableString.setSpan(imageSpan, matcher.start(), matcher.end(),
                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
        }
    }
}

按钮水波纹效果

参考

扫描Context获取Activity

public static Activity scanForActivity(Context ctx) {
    if (ctx == null)
        return null;
    else if (ctx instanceof Activity)
        return (Activity) ctx;
    else if (ctx instanceof ContextWrapper)
        return scanForActivity(((ContextWrapper) ctx).getBaseContext());
    return null;
}

一般用于在Dialog中show的时候判断Activity的引用是否有效,例如:

@Override
public void show() {
    Activity activity = UIUtils.scanForActivity(getContext());
    if (null != activity && !activity.isFinishing())
        super.show();
}

巧用Z值修改View的层级

参考

给ImageView的图片上色

通过tint属性就可以填充颜色了.

app:tint="@color/color_ffffff_60"

adjustViewBounds使ImageView和图片比例保持一致

adjustViewBounds只有在ImageView一边固定,一边为wrap_content的时候才有意义。设置为true的时候,可以让ImageView的比例和原始图片一样,以达到让图片充满的ImageView的效果。

Android 更换字体和改变字重

参考

TextView设置高亮点击

效果图:
在这里插入图片描述

/**
     * 显示重试
     */
    @JvmOverloads
    fun showRetryView(
        tipText: CharSequence? = R.string.没有匹配结果.asString(), 
        @DrawableRes errorIcon: Int? = R.drawable.common_ic_net_error,
        retryText: CharSequence? = R.string.重试_4150.asString(),
        callback: () -> Unit
    ) {
        tvDefaultTip.setVisible()
        ivDefaultIcon.setVisible()
        setDefaultIcon(errorIcon)
        retryCallback = callback
        // 1.必须要给TextView设置movementMethod,否则点击无效
        tvDefaultTip.movementMethod = LinkMovementMethod.getInstance();
        // 2.设置SpannableStringBuilder或者SpannableString都可以
        tvDefaultTip.text = SpannableStringBuilder("$tipText$retryText").apply {
            setSpan(MClickableSpan(), tipText?.length ?: 0, length, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
        }
        setVisible()
    }
	
   // 可点击的span	
   inner class MClickableSpan: ClickableSpan(){
        override fun updateDrawState(ds: TextPaint) {
            super.updateDrawState(ds)
            //设置点击文本的颜色
            ds.color = R.color.primary_100.asColor()
            //去除点击文本的默认的下划线
            ds.isUnderlineText = false
        }

        override fun onClick(widget: View) {
            //去除点击后字体出现的背景色
            (widget as? TextView)?.highlightColor = Color.TRANSPARENT
            // 回调点击事件
            retryCallback?.invoke()
        }
    }

巧妙利用rotationY属性处理阿语和非阿语环境下返回箭头的方向

下图1是非阿语
在这里插入图片描述
下图2是阿语
在这里插入图片描述
解决办法就是给这个箭头使用rotationY属性

 android:rotationY="@integer/rtl_rotation"

rtl_rotation的值定义在2个不同环境的文件内
在这里插入图片描述
其中values/integer.xml定义如下

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <integer name="rtl_rotation">0</integer>
</resources>

values-ldrtl/integer.xml定义如下, 其实就是翻转了180°形成镜像

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <integer name="rtl_rotation">180</integer>
</resources>

AppBarLayout折叠后底部有阴影

app:elevation="0dp" // 就解决底部阴影问题了

CollapsingToolbarLayout折叠后背景色

app:contentScrim="@null"

去掉TextView的透明色对emoji表情的影响

/**
 * 去掉emoji表情的alpha颜色影响
 * @return
 */
fun String.removeEmojiAlphaEffect(): SpannableString {
    val result = SpannableString(this)
    val matcher = Pattern.compile("[^\\u0000-\\uFFFF]").matcher(this)
    while (matcher.find()) {
        result.setSpan(
            ForegroundColorSpan(Color.parseColor("#ffffff")),
            matcher.start(), matcher.end(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE
        )
    }
    return result
}

判断AppBarLayout的折叠状态

import com.google.android.material.appbar.AppBarLayout
import kotlin.math.abs

/**
 * @Author: chenyousheng
 * @Description: AppBarLayout的状态监听
 */
abstract class AppBarLayoutStateChangeListener : AppBarLayout.OnOffsetChangedListener {
    enum class State {
        EXPANDED,  //展开
        COLLAPSED,  //折叠
        INTERMEDIATE //中间状态
    }

    private var mCurrentState = State.INTERMEDIATE

    override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) {
        mCurrentState = if (verticalOffset == 0) {
            if (mCurrentState != State.EXPANDED) {
                onStateChanged(
                    appBarLayout,
                    State.EXPANDED
                )
            }
            State.EXPANDED
        } else if (abs(verticalOffset) >= appBarLayout.totalScrollRange) {
            if (mCurrentState != State.COLLAPSED) {
                onStateChanged(
                    appBarLayout,
                    State.COLLAPSED
                )
            }
            State.COLLAPSED
        } else {
            if (mCurrentState != State.INTERMEDIATE) {
                onStateChanged(
                    appBarLayout,
                    State.INTERMEDIATE
                )
            }
            State.INTERMEDIATE
        }
    }

    abstract fun onStateChanged(appBarLayout: AppBarLayout, state: State)
}

使用方式

 binding.appBarLayout.addOnOffsetChangedListener(object :
            AppBarLayoutStateChangeListener() {
            override fun onStateChanged(appBarLayout: AppBarLayout, state: State) {
                if (state == State.EXPANDED ) {
                   // 展开
                } else {
                   // 折叠
                }
            }
        })

getChildDrawingOrder实现View层级的倒序排列

class ReverseLinearLayout @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {

    init {
        isChildrenDrawingOrderEnabled = true
    }

    override fun getChildDrawingOrder(childCount: Int, drawingPosition: Int): Int {
        if (childCount - 1 >= drawingPosition) {
            return childCount - 1 - drawingPosition
        }
        return drawingPosition
    }
}

巧用ImageSwitcher实现图片的切换

图像切换器(ImageSwitcher),用于实现类似于Windows操作系统的“Windows照片查看器”中的上一张、下一张切换图片的功能。在使用ImageSwitcher时,必须实现ViewSwitcher.ViewFactory接口,并通过makeView()方法来创建用于显示图片的ImageView。makeView()方法将返回一个显示图片的ImageView。在使用图像切换器时,还有一个方法非常重要,那就是setImageResource方法,该方法用于指定要在ImageSwitcher中显示的图片资源。

通过setInAnimation和setOutAnimation方法可以设置图片切换时的动画效果

public class MainActivity extends Activity{ 

 //声明并初始化一个保存要显示图像id的数组 
 private int[] imageId=new int[]{R.drawable.img01,R.drawable.img02,R.drawable.img03, 
   R.drawable.img04,R.drawable.img05,R.drawable.img06,R.drawable.img07, 
   R.drawable.img08}; 
 private int index=0;//当前显示图像的索引 
 private ImageSwitcher imageSwitcher;//声明一个图像切换器对象 
 
 @Override
 protected void onCreate(Bundle savedInstanceState) { 
  super.onCreate(savedInstanceState); 
  setContentView(R.layout.main); 
  imageSwitcher=(ImageSwitcher)findViewById(R.id.imageSwitcher1);//获取图像切换器 
  //设置动画效果 
  imageSwitcher.setInAnimation(AnimationUtils.loadAnimation(this, android.R.anim.fade_in));//设置淡入动画 
  imageSwitcher.setOutAnimation(AnimationUtils.loadAnimation(this, android.R.anim.fade_out));//设置淡出动画 
  imageSwitcher.setFactory(new ViewFactory() {//设置View工厂 
     
   @Override
   public View makeView() { 
    ImageView imageView=null; 
    imageView=new ImageView(MainActivity.this);//实例化一个ImageView类的对象 
    imageView.setScaleType(ImageView.ScaleType.FIT_CENTER);//设置保持纵横比居中缩放图像 
    imageView.setLayoutParams(new ImageSwitcher.LayoutParams( 
      LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT)); 
    return imageView; 
   } 
  }); 
  imageSwitcher.setImageResource(imageId[index]);//显示默认的图片 
    
  //“上一张”和“下一张”按钮的控制 
  Button up=(Button)findViewById(R.id.button1); 
  Button down=(Button)findViewById(R.id.button2); 
  up.setOnClickListener(new OnClickListener() { 
     
   @Override
   public void onClick(View arg0) { 
    if(index>0){ 
     index--;//图片索引后退一个 
    }else{ 
     index=imageId.length-1;//图片达到最前面一张之后,循环至最后一张 
    } 
    imageSwitcher.setImageResource(imageId[index]);//显示当前图片 
   } 
  }); 
  
  down.setOnClickListener(new OnClickListener() { 
       
     @Override
     public void onClick(View arg0) { 
      if(index<imageId.length-1){ 
       index++;//图片索引前进一个 
      }else{ 
       index=0;//图片达到最后面一张之后,循环至第一张 
      } 
      imageSwitcher.setImageResource(imageId[index]);//显示当前图片 
     } 
    }); 
 } 
}

巧用OnBackPressedCallback实现fragment监听Activity的返回键

class HomeFragment : Fragment() {

    // on back pressed.
    private val nonInboxOnBackCallback = object : OnBackPressedCallback(false) {
        override fun handleOnBackPressed() {
           // 处理返回操作
        }
    }

	override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
       super.onViewCreated(view, savedInstanceState)
     	// 是否启用监听
        nonInboxOnBackCallback.isEnabled = true
        // 设置监听
       requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner,nonInboxOnBackCallback)
	}
}

Android 关于Fragment重叠问题分析和解决

参考
阻止系统恢复Fragment state,在FragmentActivity保存所有Fragment状态前把Fragment从FragmentManager中移除掉。

	private fun initView() {
        binding.tabLayout.apply {
            removeAllTabs()
            removeOldFragment() // 删除旧的fragment
            clearOnTabSelectedListeners()
            addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
                override fun onTabSelected(tab: TabLayout.Tab) {
                	// 设置当前选中位置
                    setCurrentSelectedTab(tab.position)
                }

                override fun onTabUnselected(tab: TabLayout.Tab) {
                }

                override fun onTabReselected(tab: TabLayout.Tab) {
                }
            })
            mTopNavTab.tabs.onEach { tab ->
                addTab(newTab().apply { customView = createTabView(tab.index) }, 
                tab.index, tab.index == mCurrPosition)
            }
        }
    }
    
    private fun setCurrentSelectedTab(position: Int) {
    	mTopNavTab.getCurrentFragment(position)?.let { currentFragment ->
    		switchFragment(currentFragment)
    	}
    }
	
	// 切换fragment	
	private fun switchFragment(target: Fragment) {
        val fm = getFragmentManager()
        fm.beginTransaction().apply {
            for (childFragment in fm.fragments) {
                // 对RESUMED状态的Fragment进行操作STARTED操作,会执行onPause
                setMaxLifecycle(childFragment, Lifecycle.State.STARTED)
                hide(childFragment)
            }
            val tag = target.javaClass.simpleName
            if (!target.isAdded) {
                add(R.id.flContainer, target, tag) //首次添加
            }
            show(target)
            // 执行target fragment的onResume
            setMaxLifecycle(target, Lifecycle.State.RESUMED)
            commitNowAllowingStateLoss()
        }
    }
    
	fun onSaveInstanceState(outState: Bundle) {
	     // 移除所有fragment,避免恢复的时候重叠,因为hide()和show()方法对之前保存的fragment会无效
	     removeOldFragment()
	     outState.putInt(KEY_LAST_POSITION, mCurrPosition)
	 }
	
	fun onRestoreSaveInstance(outState: Bundle?) {
	    val lastPosition = outState?.getInt(TopNavBlock.KEY_LAST_POSITION, -1) ?: -1
	    if (lastPosition != -1) {
	        mCurrPosition = lastPosition
	        initView()
	    }
	}
	
	 /**
     * 移除旧的fragment实例
     */
    private fun removeOldFragment() {
        val fm = getFragmentManager()
        fm.beginTransaction().apply {
        	// 通过tag查找
            fm.findFragmentByTag(AFragment::class.java.simpleName)?.let {
                remove(it)
            }
            fm.findFragmentByTag(BFragment::class.java.simpleName)?.let {
                remove(it)
            }
            commitNowAllowingStateLoss()
        }
    }
    

巧用FragmentTransaction的setMaxLifecycle方法触发Fragment的生命周期方法

参考
当我们使用FragmentTransaction的add、hide、show方法进行fragment的添加、隐藏、显示操作的时候并不会执行fragment的onResume和onPause方法,可以通过setMaxLifecycle方法来解决,在ViewPager2里面就使用使用这个来解决.

	// 切换fragment	
	private fun switchFragment(target: Fragment) {
        val fm = getFragmentManager()
        fm.beginTransaction().apply {
            for (childFragment in fm.fragments) {
                // 对RESUMED状态的Fragment进行操作STARTED操作,会执行onPause方法
                setMaxLifecycle(childFragment, Lifecycle.State.STARTED)
                // 隐藏所有已添加的fragment
                hide(childFragment)
            }
            val tag = target.javaClass.simpleName
            if (!target.isAdded) {
                add(R.id.flContainer, target, tag) //首次添加
            }
            // 显示目标fragment
            show(target)
            // 执行目标fragment的onResume方法
            setMaxLifecycle(target, Lifecycle.State.RESUMED)
            commitNowAllowingStateLoss()
        }
    }

巧用Layout.getDesiredWidth获取TextView的文字宽度

 val tv = TextView(context)
 val tp: TextPaint = tv.paint
 val length: Float = Layout.getDesiredWidth(tv.text.toString(), 0, tv.text.length, tp)

Activity作为弹窗,设置背景透明


    <style name="MyTranslucentTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <!--去掉进退场动画 -->
        <item name="android:activityOpenEnterAnimation">@null</item>
        <item name="android:activityOpenExitAnimation">@null</item>
        <item name="android:activityCloseEnterAnimation">@null</item>
        <item name="android:activityCloseExitAnimation">@null</item>
        <item name="android:taskOpenEnterAnimation">@null</item>
        <item name="android:taskOpenExitAnimation">@null</item>
        <item name="android:taskCloseEnterAnimation">@null</item>
        <item name="android:taskCloseExitAnimation">@null</item>
        <item name="android:taskToFrontEnterAnimation">@null</item>
        <item name="android:taskToFrontExitAnimation">@null</item>
        <item name="android:taskToBackEnterAnimation">@null</item>
        <item name="android:taskToBackExitAnimation">@null</item>
        <item name="android:windowBackground">@android:color/black</item>
        <item name="android:colorBackgroundCacheHint">@null</item>
    </style>

    <!--语音通话邀请对话框使用-->
    <style name="VoiceCallTheme" mce_bogus="1" parent="MyTranslucentTheme">
        <item name="android:windowIsTranslucent">true</item>
        <item name="android:windowBackground">#90000000</item>
        <item name="android:colorBackgroundCacheHint">@null</item>
        <item name="android:windowNoTitle">true</item>
        <item name="android:colorForeground">#fff</item>
        <item name="android:windowAnimationStyle">@android:style/Animation.Translucent</item>
    </style>

需要注意的是Activity的布局中内容的高度要固定高度,剩余空间将会透明展示

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    >

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/rootLayout"
        android:layout_width="match_parent"
        android:layout_height="298dp"
        android:layout_alignParentBottom="true"
        android:background="@drawable/call_shape_bg_accept_call_layout">
		
		子布局省略...

    </androidx.constraintlayout.widget.ConstraintLayout>


</RelativeLayout>      

修改BottomSheetDialogFragment的Theme

参考

abstract class BaseBottomSheetDialogFragment : BottomSheetDialogFragment() {

	//白色模式,会设置导航栏背景色为白灰色,设置导航栏icon为黑色
    protected open fun isLightMode() : Boolean{
        return false
    }

    //黑色模式,会设置导航栏背景色为黑色,设置导航栏icon为白色
    protected open fun isDarkMode() : Boolean{
        return false
    }

    override fun getTheme(): Int {
        if (isLightMode()){
            return R.style.BottomSheetDialogBg2NavBarLightColor
        }
        if (isDarkMode()){
            return R.style.BottomSheetDialogBg2NavBarDarkColor
        }
        return super.getTheme()
    }
}

styles.xml中定义主题

<style name="BottomSheetDialogBg2NavBarColor" parent="@style/Theme.Design.BottomSheetDialog">
    <item name="android:windowIsFloating">false</item>
    <item name="android:navigationBarColor">#fafafa</item>
    <item name="android:statusBarColor">@android:color/transparent</item>
</style>

<style name="BottomSheetDialogBg2NavBarDarkColor" parent="BottomSheetDialogBg2NavBarColor">
    <item name="bottomSheetStyle">@style/bottomSheetStyleWrapper</item>
    <item name="android:navigationBarColor">#525252</item>
</style>

<style name="BottomSheetDialogBg2NavBarLightColor" parent="BottomSheetDialogBg2NavBarColor">
    <item name="bottomSheetStyle">@style/bottomSheetStyleWrapper</item>
    <item name="android:navigationBarColor">#fafafa</item>
</style>

<style name="bottomSheetStyleWrapper" parent="Widget.Design.BottomSheet.Modal">
    <item name="android:background">@android:color/transparent</item>
</style>

判断当前页面是否有弹窗展示

参考
在这里插入图片描述

代码中使用点9图

val ninePatchDrawable = BitmapFactory.decodeResource(resources, R.drawable.ic_party_matching_type_bg).run {
    NinePatchDrawable(resources, this, ninePatchChunk, Rect(), null)
}

给TextView设置背景图作为Span

参考
在这里插入图片描述

class BackgroundImageSpan(
    val textColor: Int,
    val shadowColor: Int,
    val hSpace: Int,
    val vSpace: Int,
    val drawable: Drawable
) :
    ReplacementSpan() {

    override fun getSize(paint: Paint, text: CharSequence?, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int {
        return paint.measureText(text, start, end).toInt() + hSpace
    }

    override fun draw(canvas: Canvas, text: CharSequence?, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) {
        val originColor = paint.color
        // draw background
        val textWidth = getSize(paint, text, start, end, null)
        drawable.setBounds(x.toInt(), top, (x + textWidth).toInt(), (bottom + vSpace / 2f).toInt())
        drawable.draw(canvas)
        // 如果是绘制纯色的矩形,可以这样处理
        // canvas.drawRoundRect(x, top.toFloat(), x + textWidth, (bottom + vSpace / 2f), 10f, 10f, Paint().apply { color = Color.RED })

        // draw text
        text?.let {
            paint.setShadowLayer(5f, 1f, 1f, shadowColor)
            paint.color = textColor
            canvas.drawText(it, start, end, x + hSpace / 2f, y.toFloat(), paint)
            paint.color = originColor
            paint.clearShadowLayer()
        }

    }
}

使用示例:

val finalText = "$subModeText  ${recommendParty.tip}"
val spannableString = SpannableString(finalText).apply {
    val typeface = ResourcesCompat.getFont(ApplicationContext.getContext(), com.yibasan.squeak.common.R.font.font_bold)
    setSpan(CustomTypefaceSpan(typeface), 0, subModeText.length, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
    setSpan(AbsoluteSizeSpan(12.dp), 0, subModeText.length, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
    val ninePatchDrawable = BitmapFactory.decodeResource(resources, R.drawable.ic_party_matching_type_bg).run {
        NinePatchDrawable(resources, this, ninePatchChunk, Rect(), null)
    }
    setSpan(
        BackgroundImageSpan(
            textColor = R.color.white_100.asColor(),
            shadowColor = R.color.black_08,
            hSpace = 8.dp,
            vSpace = 6.dp,
            drawable = ninePatchDrawable
        ), 0, subModeText.length,
        Spannable.SPAN_INCLUSIVE_EXCLUSIVE
    )
}
mBinding.tvTitle.text = spannableString

Android ViewBinding include怎么玩

参考

Android ViewBinding viewStub怎么玩

viewstub需要先inflate, 然后再bind到对应的viewStub的layout生成binding类中, 例如:

<ViewStub
       android:id="@+id/loadingStub"
       android:layout_width="0dp"
       android:layout_height="0dp"
       android:layout="@layout/live_loading"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toBottomOf="@+id/titleBar" />

使用如下:

private val loadingLayout by lazy {
    binding.loadingStub.inflate()
}

private val loadingBinding by lazy {
    LiveLoadingBinding.bind(loadingLayout)
}

如何获取系统组件的属性

以ImageView的src属性为例

public Mycomponent (Context context, AttributeSet attrs) {
	 super(context, attrs);
	 int src_resource = attrs.getAttributeResourceValue("http://schemas.android.com/apk/res/android", "src", 0);
	 this.setImageBitmap(getDrawable(getResources(),src_resource));
}

如何设置TextView的互动条

android:maxLines="7"
android:scrollbarStyle="outsideInset"
android:scrollbarThumbVertical="@drawable/base_bg_scrollbar"
android:scrollbars="vertical"

控制AppBarLayout子View的滚动

 private fun canScrollBanner(canScroll: Boolean) {
        val appBarChildAt: View = binding.appBarLayout.getChildAt(0)
        val appBarParams: AppBarLayout.LayoutParams = appBarChildAt.layoutParams as AppBarLayout.LayoutParams
        if (canScroll) {
            appBarParams.scrollFlags = AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL or AppBarLayout.LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED
            appBarChildAt.layoutParams = appBarParams
        } else {
            appBarParams.scrollFlags = 0
        }
    }

如何在Android P不受限制的使用反射

使用FreeReflection库

设置弹窗的层级

window.setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
这里设置弹窗的层级,数值越大层级就会越高,也就越会浮在最顶端。用到的是一般的应用层级的时候,window的token,也就是我们构造Dialog时候的Context必须是Activity;如果是系统级的类型参数的时候,必须是全局的ApplicationContext才可以,同时还需要获取到系统级应用的权限。比如上面代码中就是系统级层级,需要获取到系统应用权限。

Android TextView 跑马灯效果

参考

如何代码设置textView的style

(1) textStyle

<TextView
  android:id="@+id/my_text"
  android:layout_width="fill_parent"
  android:layout_height="wrap_content"
  android:text="Hello World"
  android:textStyle="bold" />

代码方式:

textview.setTypeface(textview.getTypeface(), Typeface.BOLD);

(2) style

<style name="myStyle" parent="@android:style/Widget.TextView">
    <item name="android:textSize">@dimen/text_size_huge</item>
    <item name="android:textColor">@color/red</item>
    <item name="android:textStyle">bold</item>
</style>

只需像往常一样在XML layout/your_layout.xml文件中创建TextView

 <TextView
     android:id="@+id/tvTitle"
     style="@style/myStyle"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
  />

代码方式设置:

textViewTitle.setTextAppearance(this, R.style.myStyle);

解决FragmentNavigator切换Fragment重建的问题

参考

利用Glide的API获取本地缓存的图片

关键在于onlyRetrieveFromCache(true),如果本地有缓存那么读取缓存,否则才会从网络获取

val file: File? =  Glide.with(context).downloadOnly().load(url)
                        .apply(RequestOptions().onlyRetrieveFromCache(true)).submit().get()

注意要在子线程操作

如何给图片添加蒙层

 private suspend fun addCoverToImage(resultUri: Uri): String {
        val filePath = withContext(Dispatchers.IO) {
            // 原路径, uri是从系统相册选择后的图片uri
            val cropFile = File(PhotoUtil.getImageAbsolutePath(context, resultUri))
            // 目标路径
            val destPath = File(cropFile.parentFile, "${cropFile.nameWithoutExtension}_transform.${cropFile.extension}").absolutePath
            // 原图
            val srcBitmap = BitmapUtils.decodeSampledBitmapFromFile(
                file = cropFile.absolutePath, reqWidth = ScreenUtil.getScreenWidth(), reqHeight = ScreenUtil.getScreenHeight()
            )
            // 方式1: 使用drawColor
//            val destBitmap =srcBitmap.copy(Bitmap.Config.ARGB_8888,true)
//            Canvas(destBitmap).drawColor(R.color.ty_black_60.asColor(), PorterDuff.Mode.SRC_ATOP)
            
            // 方式2: 使用colorFilter
            val destBitmap = Bitmap.createBitmap(srcBitmap.width, srcBitmap.height, Bitmap.Config.ARGB_8888)
            Canvas(destBitmap).drawBitmap(
                srcBitmap,
                0f,
                0f,
                Paint().apply { colorFilter = PorterDuffColorFilter(R.color.ty_black_60.asColor(), PorterDuff.Mode.SRC_ATOP) })
            // 重新保存bitmap到文件中    
            BitmapUtils.writeToFile(destBitmap, destPath, 80)
            return@withContext destPath
        }
        // 返回修改后的图片路径
        return filePath
    }

如何实现第三方App的文本分享

public static void shareText(Context activity, String shareText, String pkgFilter,
                                 @Nullable Bundle extraData) {
        try {
            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP_MR1) {
            	// 处理分享结果的BroadcastReceiver
                Intent receiver = new Intent(activity, ShareBroadcastReceiver.class);
                receiver.setPackage(activity.getPackageName());
              	// 添加参数,在ShareBroadcastReceiver收到结果的时候可以取出
                if (extraData != null) {
                    receiver.putExtra("extraData", extraData);
                }
                int flag = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE;
                PendingIntent pendingIntent = PendingIntent.getBroadcast(activity, 0, receiver, flag);
                // 分享的action
                Intent intent = new Intent(Intent.ACTION_SEND);
                // 分享mimeType
                intent.setType("text/plain");
                // 分享的文本内容
                intent.putExtra(Intent.EXTRA_TEXT, shareText);
                // 如果有指定第三方App的包名,可以设置
                if (!StringUtils.isNullOrEmpty(pkgFilter)) {
                    intent.setPackage(pkgFilter);
                }
                if (intent.resolveActivity(activity.getPackageManager()) != null) {
                	// createChooser() 方法将 Intent 包装在一个新 Intent 中,并显示一个对话框,其中包含用户可以从中选择的应用程序列表。用户成功选择应用程序后,选定应用程序将处理包装的 Intent。
                	// pendingIntent.getIntentSender() 则允许您指定一个 IntentSender 对象,以便在选择器对话框上选择应用程序之前,系统可以在用户选择之后返回结果给您的应用程序。
                    activity.startActivity(Intent.createChooser(intent,
                            "",
                            pendingIntent.getIntentSender()));
                }
            } else {
                Intent sendIntent = new Intent();
                if (!StringUtils.isNullOrEmpty(pkgFilter)) {
                    sendIntent.setPackage(pkgFilter);
                }
                sendIntent.setAction(Intent.ACTION_SEND);
                sendIntent.putExtra(Intent.EXTRA_TEXT, shareText);//shareText 为需要分享的内容
                sendIntent.setType("text/plain");
                sendIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                activity.startActivity(sendIntent);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

ShareBroadcastReceiver的处理如下:

public class ShareBroadcastReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP_MR1) {
            try {
                if (intent.getExtras() != null && intent.getExtras().keySet() != null && !intent.getExtras().keySet().isEmpty()) {
                    Parcelable parcelableExtra = intent.getParcelableExtra(Intent.EXTRA_CHOSEN_COMPONENT);
                    String packageName = null;
                    if (parcelableExtra instanceof ComponentName) {
                        packageName = ((ComponentName) parcelableExtra).getPackageName();
                         Bundle extraData = intent.getBundleExtra("extraData");
                        // todo 分享结果回调处理
                       
                    }
                    
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

解决TextView设置URLSpan后长按事件和url跳转冲突的问题

class CustomLinkMovementMethod(private val onLongClickCallback: DefaultCallback?) : LinkMovementMethod() {
    private val mHandler = Handler()
    private var mLongClickPerformed = false

    override fun onTouchEvent(
        widget: TextView, buffer: Spannable,
        event: MotionEvent
    ): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                mLongClickPerformed = false
                mHandler.postDelayed({
                    mLongClickPerformed = true
                    Selection.removeSelection(buffer)
                    widget.cancelLongPress()
                    // 处理长按事件
                    onLongClickCallback?.invoke()

                }, ViewConfiguration.getLongPressTimeout().toLong())
            }

            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                mHandler.removeCallbacksAndMessages(null)
                if (!mLongClickPerformed) {
                    // 处理点击事件
                    super.onTouchEvent(widget, buffer, event)
                }
            }
        }
        return true
    }
}

fun TextView.highLightLinkAndHandleClick(
    txtContent: String,
    color: Int,
    onLinkClickCallback: ((String) -> Unit)? = null, // 点击url跳转回调
    onLongClickCallback: DefaultCallback? // 长按回调
) {
    val builder = SpannableStringBuilder(txtContent)
    val matcherAutoLink = Pattern.compile("((https|http|ftp|rtsp|mms)?://)\\S+").matcher(builder.toString())
    var hasLink = false
    while (matcherAutoLink.find()) {
        hasLink = true
        val content: String = matcherAutoLink.group()
        builder.delete(matcherAutoLink.start(), matcherAutoLink.end())
        builder.insert(matcherAutoLink.start(), autolink(content, color, onLinkClickCallback = onLinkClickCallback))
    }
    if (hasLink) {
    	// 自定义movementMethod
        movementMethod = CustomLinkMovementMethod(onLongClickCallback)
        text = builder
        setOnClickListener(null)
        setOnLongClickListener(null)
    } else {
        text = txtContent
        setOnClickListener(null)
        movementMethod = null
        setOnLongClickListener {
            onLongClickCallback?.invoke()
            true
        }
    }
    setHintTextColor(Color.TRANSPARENT)
    highlightColor = Color.TRANSPARENT
}

class AutoLinkSpan(url: String?, private val color: Int, private val linkActionCallback: LinkActionCallback?) : URLSpan(url) {
    interface LinkActionCallback {
        fun onClick(view: View?, url: String?): Boolean
    }

    override fun updateDrawState(ds: TextPaint) {
        super.updateDrawState(ds)
        ds.color = color
        ds.isFakeBoldText = true
    }

    override fun onClick(widget: View) {
        if (linkActionCallback != null) {
            val intercept = linkActionCallback.onClick(widget, url)
            if (!intercept) {
                super.onClick(widget)
            }
        } else {
            super.onClick(widget)
        }
    }
}

fun autolink(link: String, color: Int = R.color.basic_primary.asColor(), onLinkClickCallback: ((String) -> Unit)? = null)
        : CharSequence? {
    val builder = SpannableStringBuilder.valueOf(link)
    val linkSpan = AutoLinkSpan(link, color, object : AutoLinkSpan.LinkActionCallback {
        override fun onClick(view: View?, url: String?): Boolean {
            url?.let { onLinkClickCallback?.invoke(it) }
            return true
        }
    })
    builder.setSpan(linkSpan, 0, link.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
    return builder
}

图片保存到相册

fun File.saveImageToGallery() {
    if (exists()) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            val contentValues = ContentValues().apply {
                put(MediaStore.Images.Media.DISPLAY_NAME, name)
                put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
            }
            val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
            uri?.let {
                contentResolver.openOutputStream(it).use { outputStream ->
                    outputStream?.let { os -> FileInputStream(this).copyTo(os) }
                }
            }
        } else {
            MediaStore.Images.Media.insertImage(contentResolver, absolutePath, name, name)
        }
    }
}

如何避免一个操作频繁触发

object OperationDelayHelper{
     private val handler = Handler(Looper.getMainLooper())
     private val msg = HashMap<Long,Runnable>()

     fun postOperation(taskId: Long, operation:()->Unit) {
         handler.post {
             val oldRunnable = msg[taskId]
             if (oldRunnable != null) {
                 handler.removeCallbacks(oldRunnable)
             }
             val newRunnable = Runnable {
                 operation()
                 msg.remove(taskId)
             }
             msg[taskId] = newRunnable
             handler.postDelayed(newRunnable, 200)
         }
     }
 }

通过registerForActivityResult实现Activity跳转结果返回


    private val startForResult: ActivityResultLauncher<Intent> = registerForActivityResult(
        ActivityResultContracts.StartActivityForResult()) { result ->
        if (result.resultCode == Activity.RESULT_OK) {
            // do something..
        }
    }

    private fun startSecondeActivityPage() {
        startForResult.launch(
            Intent(
                fragment.requireContext(), SecondeActivity::class.java
            ).apply {
                putExtra(
                    KEY_VALUE, "xxxxx"
                )
            }, ActivityOptionsCompat.makeCustomAnimation(
                this@FirstActivity,
                R.anim.anim_nav_enter,
                R.anim.anim_nav_exit
            )
        )
    }

然后在SecondeActivity中通过setResult返回结果

 setResult(Activity.RESULT_OK)
 finish()

子Fragment如何给父Fragment传递结果

父Fragment监听

childFragmentManager.setFragmentResultListener(KEY_REQUEST_ID, this) { requestKey, result ->
            if (requestKey == AddAiCharacterDialog.KEY_REQUEST_ID) {
                // do something or parse result
            }
        }

子fragment返回结果

setFragmentResult(KEY_REQUEST_ID, arguments ?: Bundle())

setFragmentResult是系统Fragment类的扩展方法

public fun Fragment.setFragmentResult(requestKey: String, result: Bundle) {
    parentFragmentManager.setFragmentResult(requestKey, result)
}

监听窗口变化适配布局内容

setOnApplyWindowInsetsListener 方法允许你为视图设置一个回调,以便在窗口插入事件发生时自定义视图的行为或外观。通过这个回调,你可以获取窗口插入的信息,例如状态栏的高度、导航栏的高度等,并根据这些信息来调整视图的布局或样式。
这个方法对于处理窗口插入事件非常有用,无论是为了适应不同设备的屏幕尺寸和系统栏的变化,还是为了实现特定的用户界面效果。
在 Android 5.0 及更早的版本中,View 类没有提供 setOnApplyWindowInsetsListener() 方法。但是,通过使用 Android Support Library 中的 ViewCompat 类,你可以在这些早期版本的 Android 上使用 setOnApplyWindowInsetsListener() 方法。
在刘海屏适配布局内容的时候可以使用,例如:

binding.root.setOnApplyWindowInsetsListener(object :View.OnApplyWindowInsetsListener {

   override fun onApplyWindowInsets(v: View, insets: WindowInsets): WindowInsets {
   	   // 通过WindowInsets还可以获取输入法的高度
   	   val imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom.toFloat()	
   	   // 还可以获取屏幕上下左右的间距,例如获取状态栏的高度可以这样用
   	   val statusBarHeight = insets.getInsets(WindowInsetsCompat.Type.systemBars()).top

       if (Build.VERSION.SDK_INT >= 28) {
           // 获取刘海的数量,Android9才有的方法
           val displayCutout = insets.displayCutout
           if (displayCutout != null) {
               // 说明有刘海,获取刘海屏四周的安全距离
               val left = displayCutout.safeInsetLeft
               val top = displayCutout.safeInsetTop
               val right = displayCutout.safeInsetRight
               val bottom = displayCutout.safeInsetBottom
               // 设置顶部按钮(竖屏的顶部边距),左边按钮(横屏的左边距)
               val topParams =  topButton.layoutParams as MarginLayoutParams
               topParams.setMargins(left, top, right, bottom);
               val leftParams = LeftButton.layoutParams as MarginLayoutParams
               leftParams.setMargins(left, top, right, bottom);
           }
       }
       return insets.consumeSystemWindowInsets()
   }
})

ConstraintSet的常用方法

ConstraintSet类提供了一些常用的方法来设置和修改ConstraintLayout布局中的约束条件。以下是一些常用的方法:

connect(int startID, int startSide, int endID, int endSide, int margin):连接两个视图之间的约束关系。startID和endID分别表示起始视图和结束视图的ID,startSide和endSide表示起始视图和结束视图的边界(例如,ConstraintSet.START表示起始边界,ConstraintSet.END表示结束边界),margin表示边界之间的间距。

clear(int viewID):清除指定视图的所有约束关系。

setVisibility(int viewID, int visibility):设置指定视图的可见性。visibility可以是ConstraintSet.VISIBLE、ConstraintSet.INVISIBLE或ConstraintSet.GONE。

setMargin(int viewID, int side, int value):设置指定视图的边界间距。side可以是ConstraintSet.START、ConstraintSet.END、ConstraintSet.TOP或ConstraintSet.BOTTOM。

setHorizontalBias(int viewID, float bias):设置指定视图的水平偏移。bias的值范围为0.0到1.0,其中0.0表示视图靠近起始边界,1.0表示视图靠近结束边界。

setVerticalBias(int viewID, float bias):设置指定视图的垂直偏移。bias的值范围为0.0到1.0,其中0.0表示视图靠近顶部边界,1.0表示视图靠近底部边界。

setDimensionRatio(int viewID, String ratio):设置指定视图的宽高比。ratio是一个字符串,表示宽高比,例如’16:9’。

setHorizontalChainStyle(int viewID, int chainStyle):设置水平链的样式。chainStyle可以是ConstraintSet.CHAIN_SPREAD、ConstraintSet.CHAIN_SPREAD_INSIDE或ConstraintSet.CHAIN_PACKED。

setVerticalChainStyle(int viewID, int chainStyle):设置垂直链的样式。chainStyle可以是ConstraintSet.CHAIN_SPREAD、ConstraintSet.CHAIN_SPREAD_INSIDE或ConstraintSet.CHAIN_PACKED。

setHorizontalWeight(int viewID, float weight):设置指定视图在水平链中的权重。weight表示视图在链中所占的比例。

setVerticalWeight(int viewID, float weight):设置指定视图在垂直链中的权重。weight表示视图在链中所占的比例。

setElevation(int viewID, float elevation):设置指定视图的高度。elevation表示视图的高度值。

constrainWidth(int viewID, int width):方法接受两个参数:viewID表示要设置约束的视图的ID,width表示要设置的宽度值。

constrainHeight(int viewID, int height)方法接受两个参数:viewID表示要设置约束的视图的ID,height表示要设置的高度值。

CodeLocator插件动态调试UI的开发利器

在这里插入图片描述
下载AS插件

如何在gradle中配置build参数

在Gradle中配置build参数,可以使用buildConfigField来定义一个在代码中可用的常量。

android {
    defaultConfig {
        // 其他配置项...
        buildConfigField 'String', 'API_KEY', '\'your_api_key\''
    }
}

如何在gradle中设置清单文件的meta-data值

例如清单文件中定义了meta标签如下:

<application>
     ...
     <meta-data android:name="CHANNEL" android:value="enter_store_name_here"/>
     ...
</application>

在gradle中可以修改CHANNEL的值

android {
    defaultConfig {
        // 其他配置项...
        manifestPlaceholders = [CHANNEL: 'googlePay']
    }
}

当然在代码中也可以获取到这个CHANNEL的值

try {
    ApplicationInfo appInfo = getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA);
    String channel = appInfo.metaData.getString('CHANNEL');
    // 在这里使用channel变量进行你的操作
} catch (PackageManager.NameNotFoundException e) {
    e.printStackTrace();
}

Android 获取手机语言环境 区分简体和繁体,香港,澳门,台湾繁体

参考
资源文件的文件夹命名
参考1
参考2

Simplified:  简化:

values-b+zh (defaults to simplified) 
values-b+zh+Hans or
values-b+zh+Hans+CN (if you want to include regions)
Traditional:  传统的:

values-b+zh+Hant or
values-b+zh+TW (defaults to traditional)
values-b+zh+Hant+TW (if you want to include regions)

values-b+zh+Hant+TW:这是 BCP 47 标准中的一种语言标签表示法,用于表示繁体中文(台湾地区)。其中,b 表示 BCP 47 扩展语言标记,zh 表示中文,Hant 表示繁体中文,TW 表示台湾地区。这种表示法在某些特定的场景下使用,例如在 XML 中指定语言标签。

获取系统语言可以这样获取

fun getSystemLanguage(): String {
     val localLanguage = Locale.getDefault().language
     return if (localLanguage == "zh") {
         val country = Locale.getDefault().country
         if (country == "TW") {
             language_zhTw.code
         } else language_zh.code
     } else localLanguage
 }

在 Android 开发中,通常使用 values-zh-rTW 表示繁体中文(台湾地区)的资源文件夹

values-zh: Simplified
values-zh-rTW: Traditional
values-zh-rHK: Traditional
values-zh-rMO: Traditional

Jetpack Hilt 框架的基本使用

参考

Fragment的LifecycleOwner与viewLifecycleOwner

参考
参考2
推荐使用viewLifecycleOwner,特别是在有调用addToBackStack的场景,fragment从返回栈返回的时候不会触发fragment的onDestory方法,只会触发onDestroyView,而使用viewLifecycleOwner,当触发onDestroyView 之前会销毁使用它所添加的LiveData observe 或者通过它的lifecycle addObserver添加的observer(当触发onDestroyView会触发DefaultLifecycleObserver的onDestory方法回调)

窗口类型

参考
从WmS的角度看,一个窗口并不是Window类,而是一个View类。WmS收到用户消息后,需要把消息派发到窗口,View类本身并不能直接接收WmS传递过来的消息,真正接收用户消息的必须是IWindow类,而实现IWindow类的是ViewRoot.W类,每一个W内部都包含了一个View变量。
WmS并不介意该窗口(View)是属于哪个应用程序的,WmS会按一定的规则判断哪个窗口处于活动状态,然后把用户消息给W类,W类再把用户消息传递给内部的View变量,剩下的消息处理就由View对象完成。
Framework定义了三种窗口类型,三种类型的定义在WindowManager的LayoutParams中。
第一种窗口类型为应用窗口,所谓的应用窗口是指该窗口对应一个Activity,由于加载Activity是由AmS完成的,因此,对于应用程序来说,要创建一个应用类窗口,只能在Activity内部完成。
应用窗口包含以下几类:
在这里插入图片描述
所有Activity默认的窗口类型都是TYPE_APPLICATION,WmS在进行窗口叠加时,会动态改变应用窗口的层值,但层值不会大于99。
第二种窗口类型是子窗口,子窗口是指该窗口必须要有一个父窗口,父窗口可以是一个应用类型窗口,也可以是任何其他类型的窗口。
子窗口包含以下几类:
在这里插入图片描述
创建子窗口时,客户端可以指定窗口类型介于1000-1999之间,而WmS在进行窗口叠加时,会动态调整层值。
第三种窗口类型是系统窗口,系统窗口不需要对应任何Activity,也不需要有父窗口,对于应用程序而言,理论上是无法创建系统窗口的,因为所有的应用程序都没有这个权限,然而系统进程却可以创建系统窗口。
系统窗口有以下类型:
在这里插入图片描述

去掉dialog 或者 dialogFragment的弹窗2边的边距

class DemoDialogFragment : DialogFragment() {

    companion object {
        fun newInstance(): DemoDialogFragment {
            return DemoDialogFragment()
        }
    }

    override fun setupDialog(dialog: Dialog, style: Int) {
        dialog.window?.apply {
            // 移除黑色背景
            clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
            // 允许触摸事件穿透到后面的 Activity
            addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL)

            attributes?.apply {
                gravity = Gravity.BOTTOM
                y = 50.dp
            }
            // 设置弹出动画
            setWindowAnimations(R.style.DialogAnimation)

            // 必需要在onCreateView之后,可以放在onStart
//            setLayout(
//                WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT
//            )
        }
    }


    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
    ): View {
        return FrameLayout(this.requireContext()).apply {
            // 必需包裹一层根布局,否则300.dp不生效
            addView(LinearLayout(this.context).apply {
                setBackgroundColor(Color.BLUE)
                orientation = LinearLayout.VERTICAL
                layoutParams = ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
                addView(TextView(context).apply {
                    text = "klsdjfkljdskfljkdjkfldsfkdksjfjkdsjfdklsjfkldjslfjkldsfjlsdjlkfjdsjfdjsfj"
                    layoutParams = FrameLayout.LayoutParams(
                        FrameLayout.LayoutParams.MATCH_PARENT, 300.dp
                    )
                })
                addView(Button(context).apply {
                    text = "close"
                    setOnClickListener {
                        dismiss()
                    }
                    layoutParams = FrameLayout.LayoutParams(
                        FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT
                    ).apply {
                        gravity = Gravity.BOTTOM
                    }
                })
            })
        }
    }

    override fun onStart() {
        super.onStart()
        // 设置宽度为铺满
        dialog?.window?.setLayout(
            WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT
        )
        // 去掉弹窗四周的间距
        dialog?.window?.decorView?.setPadding(0, 0, 0, 0)
        dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
    }
}

弹窗效果图:
在这里插入图片描述
另一种方式是通过style来实现:

<style name="DialogFragmentNoPaddingStyle" parent="Theme.AppCompat.Light.Dialog">
    <item name="android:windowNoTitle">true</item>
    <item name="android:windowBackground">@android:color/transparent</item>
    <item name="android:paddingLeft">0dp</item>
    <item name="android:paddingRight">0dp</item>
    <item name="android:paddingTop">0dp</item>
    <item name="android:paddingBottom">0dp</item>
</style>

在onCreate的时候设置style

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // 设置 Style,去掉弹窗四周的间距
    setStyle(STYLE_NO_FRAME, R.style.DialogFragmentNoPaddingStyle)
}

动态监听软键盘显示变化

ViewCompat.setOnApplyWindowInsetsListener(rootView) { v, insets ->
    val isKeyboardVisible = insets.isVisible(WindowInsetsCompat.Type.ime())
    keyboardVisibleStateFlow.value = isKeyboardVisible
    insets
}

允许WindowManager弹窗越过状态栏

private val windowManager = activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager
private val windowParams = WindowManager.LayoutParams(
    WindowManager.LayoutParams.MATCH_PARENT,
    WindowManager.LayoutParams.WRAP_CONTENT,
    WindowManager.LayoutParams.TYPE_APPLICATION, // 窗口类型
    WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
            or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH // 触摸外部不关闭窗口
            or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE // 不捕获返回键
            or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN // 允许窗口越过状态栏
            or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, // 无视屏幕限制
    PixelFormat.TRANSLUCENT // 窗口的背景透明度
).apply {
    gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
    y = 0
    dimAmount = 0f
}

windowManager.addView(contentView, windowParams)

关键代码是下面2行:

WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN // 允许窗口越过状态栏
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, // 无视屏幕限制

参考

FLAG_LAYOUT_IN_SCREEN: 将window放置在整个屏幕之内,无视其他的装饰(比如状态栏)

FLAG_LAYOUT_NO_LIMITS: 允许window扩展值屏幕之外

ArgbEvaluator实现颜色过渡

// fraction: 0~1
val textColor = ArgbEvaluator.getInstance().evaluate(fraction,startColor,endColor)
titleTv.setTextColor(textColor)
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值