Android 进阶学习(三十四) Android 中一些常用的开发技巧(二)View相关

这里总结的都是平时一些常用的功能,使用系统提供的方法能更快的实现想要的效果

1.圆形裁剪 outlineProvider

动画.gif

想要实现圆形裁剪特别简单,我自己写了一个拓展方法,

fun View.zhomeOutLineProvider(block: (View?,Outline?) -> Unit){
  this.outlineProvider=object :ViewOutlineProvider(){
      override fun getOutline(view: View?, outline: Outline?) {
          block(view,outline)
      }
  }
  // 使用设定的边距
  this.clipToOutline=true
}

使用这个拓展方法的情况,指定outline 为圆形就可以了,宽度为view 的宽,高度为View 的高度

  iv_zhome_out_line_provider.zhomeOutLineProvider { view, outline ->
          outline?.setOval(0,0,view?.width?:0,view?.height?:0)
      }

View 设置完 ViewOutlineProvider 后, 阴影的范围也被改变了,举个栗子
View.outlineProvider=object :ViewOutlineProvider(){}
把一个矩形的View 修改为 一个圆形的View ,但是 clipToOutline 设置的false
那么只是将阴影的绘制区域给改变了,
如果 clipToOutline =true 则是将View 裁剪,同时改变阴影的显示

2.扩充点击范围 从QMUI中学习到这个方法

在将QMUI 的代码下载下来,研究了一下他们的工具类,找到了这个方法,放出他们的源码 ,

  /**
   * 扩展点击区域的范围
   *
   * @param view       需要扩展的元素,此元素必需要有父级元素
   * @param expendSize 需要扩展的尺寸(以sp为单位的)
   */
  public static void expendTouchArea(final View view, final int expendSize) {
      if (view != null) {
          final View parentView = (View) view.getParent();

          parentView.post(new Runnable() {
              @Override
              public void run() {
                  Rect rect = new Rect();
                  view.getHitRect(rect); //如果太早执行本函数,会获取rect失败,因为此时UI界面尚未开始绘制,无法获得正确的坐标
                  rect.left -= expendSize;
                  rect.top -= expendSize;
                  rect.right += expendSize;
                  rect.bottom += expendSize;
                  parentView.setTouchDelegate(new TouchDelegate(rect, view));
              }
          });
      }

  }

发现TouchDelegate 可以扩充点击事件,但是他并不能将这个事件拓展到他的父ViewGroup之外,至于这个方法为什么可以用,我又去看了一下View 的onTouchEvent 的,发现了里面的玄机

  public boolean onTouchEvent(MotionEvent event) {
      if (mTouchDelegate != null) {
          if (mTouchDelegate.onTouchEvent(event)) {
              return true;
          }
      }
      if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
          switch (action) {
              case MotionEvent.ACTION_UP:
            /// 这里判断的click事件  
      }
}

如果View 存在mTouchDelegate ,这个事件的响应会在onClick之前, 而且这个范围也是你自己指定的,一个非常nice的方法,受教了

3.click 优雅防止多次出发事件 从QMUI中学习到这个方法,

我将这个方法给写成了拓展方法,方便使用,这里贴一下我改后的

/**
*  设定时间内,只让第一个事件执行
*/
fun throttleClick(wait: Long = 200, block: ((View) -> Unit)): View.OnClickListener {
  return View.OnClickListener { v ->
      val current = SystemClock.uptimeMillis()
      val lastClickTime = (v.getTag(R.id.zhome_view_click) as? Long) ?: 0
      if (current - lastClickTime > wait) {
          v.setTag(R.id.zhome_view_click, current)
          block(v)
      }
  }
}

/**
* 简化上面的写法
*/
fun View.click(wait: Long = 200, block: ((View) -> Unit)){
  this.setOnClickListener(throttleClick(wait,block))
}

他们写的这个非常巧妙,还有另外一个也是非常好用,

/**
* 设定时间内,让最后一个事件执行
*/
fun debounceClick(wait: Long = 200, block: ((View) -> Unit)): View.OnClickListener {
  return View.OnClickListener { v ->
      var action = (v.getTag(R.id.zhome_view_click) as? DebounceAction)
      if(action == null){
          action = DebounceAction(v, block)
          v.setTag(R.id.zhome_view_click, action)
      }else{
          action.block = block
      }
      v.removeCallbacks(action)
      v.postDelayed(action, wait)
  }
}

class DebounceAction(val view: View,  var block: ((View) -> Unit)): Runnable {
  override fun run() {
      if(view.isAttachedToWindow){
          block(view)
      }
  }
}

虽然感觉自己写UI相关的东西挺多的,了解的东西也算不少了,但是看了他们的代码后,对自己的提升还是非常大的,
下面附上他们项目的链接 https://github.com/Tencent/QMUI_Android

4.clipdrawable 查看progressbar的时候发现一个level属性,查了相关文章后发现超级实用

前一阵子在开发的过程中遇到这样一个需求


image.png

这个seekbar 拖动的部分是一个渐变色,背景是不变的,最开始打算修改seekbar,或者从网上找一些资料,但是效果都不尽如人意,没办法翻了一下progressbar,想从他的源码中发现他是如何实现的这个效果,就找到了clipdrawable

具体一个具体的clip 文件


          
              
              
          
      

其中 clipOrientation 指定的是水平渲染还是垂直渲染 gravity 指定的是从哪里开始渲染
剩下的都是draw的属性了,最终实现的效果


动画.gif

5.RecyclerView 使用Scroller滚动到指定位置 用的地方很多,这个的是使用动画滚动过去,

RecyclerView 滚动的指定item,原生的api是只要这个item的在屏幕上就可以了,不管这个item是否在屏幕的一个上面,从网上看了很多例子,大部分都是根据layoutmanager 来判断的,实现起来超级复杂,下面来看一下我这套方案

class TopSmoothScroller(context: Context?) : LinearSmoothScroller(context) {
  override fun getHorizontalSnapPreference(): Int {
      return SNAP_TO_START //意思是吸附在顶部
  }

  override fun getVerticalSnapPreference(): Int {
      return SNAP_TO_START //意思是吸附在顶部
  }
}

我是重写了一个recyclerview ,把他的smoothScrollToPosition 给替换了,

  @Override
  public void smoothScrollToPosition(int position) {
      if(getContext()!=null){
          LinearSmoothScroller s1  =new TopSmoothScroller(getContext());
          s1.setTargetPosition(position);
          if(getLayoutManager()!=null){
              getLayoutManager().startSmoothScroll(s1);
          }
      }
  }

方便快捷

6.colorfliter 修改颜色,不用替换iamge 节省内存 ,titlebar 滑动渐变初使用

在titlebar 滑动由透明色背景变成白色背景,到达中间时,需要替换返回图片,和右边的按钮,如果使用图片的资源id 来回设置的话,如果图片加载比较多,会发现明显的卡顿,这是内存抖动造成的,如果使用这个方法

img.setColorFilter(new PorterDuffColorFilter(ContextCompat.getColor(child.getContext(), R.color.color_ct1_85), PorterDuff.Mode.SRC_IN));

想要变回来也特别简单

img.setColorFilter(null)

其实在做这个功能的时候,我的想法是能不能将这个滑动渐变的过程整体封装起来,只要使用了我写的titlebar,整个滑动渐变的过程对外都是没有感知的,要不然每次都监听ScrollView 只是copy代码都有点麻烦,我就自己写了一个
SimpleToolBarAlphaBehavior

public class SimpleToolBarAlphaBehavior extends CoordinatorLayout.Behavior {

  private float deltaY;

//    public int textColor;

  private SimpleToolbarListeners listeners;
  public boolean blackToolBar = false;

  public SimpleToolBarAlphaBehavior(Context context) {
      super();
  }

  public SimpleToolBarAlphaBehavior(Context context, AttributeSet attrs) {
      super(context, attrs);
  }

  @Override
  public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
      return dependency instanceof RecyclerView || dependency instanceof NestedScrollView || dependency instanceof SmartRefreshLayout || dependency instanceof ViewPager2;
  }


  public void onConfigChange(){
      deltaY=0;
  }


  @Override
  public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {

      if (deltaY == 0) {
          deltaY = dependency.getY();
      }
      float dy =dependency.getY();

      float pre = prepareDate(dy);

      Log.i("tian.shm","pre:"+pre);

      int alpha = (int) ((pre) * 255);
      int color=Color.argb(alpha,255,255,255);///白色
      child.setBackgroundColor(color);
      addListeners(alpha,pre,child);
      return true;
  }
  public void setSimpleToolbarListeners(SimpleToolbarListeners listeners) {
      this.listeners = listeners;
  }

  private void addListeners(int alpha,float pre,View child) {
      if(listeners!=null){
          listeners.onAlphaChange(pre);
      }
      if (alpha >= 127) {
          if (!blackToolBar) {
              blackToolBar = true;
              if(child instanceof TitleBarView){
                  ((TitleBarView) child).getLeftImageButton().setColorFilter(new PorterDuffColorFilter(ContextCompat.getColor(child.getContext(), R.color.color_ct1_85), PorterDuff.Mode.SRC_IN));
                  if(((TitleBarView) child).getRightImageButton()!=null){
                      ((TitleBarView) child).getRightImageButton().setColorFilter(new PorterDuffColorFilter(ContextCompat.getColor(child.getContext(), R.color.color_ct1_85), PorterDuff.Mode.SRC_IN));
                  }
                  ((TitleBarView) child).getCenterTextView().setTextColor(ContextCompat.getColor(child.getContext(), R.color.color_ct1_85));

              }
              if (listeners != null) {
                  listeners.onFullColor();
              }
          }
      } else {
          if (blackToolBar) {
              blackToolBar = false;
              ((TitleBarView) child).getLeftImageButton().setColorFilter(null);
              ((TitleBarView) child).getCenterTextView().setTextColor(Color.WHITE);
              if(((TitleBarView) child).getRightImageButton()!=null){
                  ((TitleBarView) child).getRightImageButton().setColorFilter(null);
              }
              if (listeners != null) {
                  listeners.onTransparent();
              }
          }
      }
  }
  private float prepareDate(float dy) {
      if(dy==0f){
          return 1;
      }
      dy = dy < 0 ? 0 : dy;
      float pre = 1-(dy / deltaY);
      if (pre > 0.33f) {
          pre = 1;
      } else {
          pre = pre * 3;
      }
      return pre>1?1:pre;
  }
  public static  SimpleToolBarAlphaBehavior from(V view) {
      ViewGroup.LayoutParams params = view.getLayoutParams();
      if (!(params instanceof CoordinatorLayout.LayoutParams)) {
          throw new IllegalArgumentException("The view is not a child of CoordinatorLayout");
      } else {
          CoordinatorLayout.Behavior behavior = ((CoordinatorLayout.LayoutParams) params).getBehavior();
          if (!(behavior instanceof SimpleToolBarAlphaBehavior)) {
              throw new IllegalArgumentException("The view is not associated with SimpleToolbarBehavior");
          } else {
              return (SimpleToolBarAlphaBehavior) behavior;
          }
      }
  }
}

layout 文件的写法 只要CollapsingToolbarLayout 内部有高度,那么就会自己实现渐变改变颜色


      

          
              
          
      
      


          

              



              
          

      

      

  

这样就做到了整个滑动渐变对外是完全没有感知的

7.View 的显示和隐藏 GONE VISIBLE 与 alpha 比较

实现View 的显示和隐藏不止有 GONE VISIBLE ,修改alpha 有时也能达到相同的效果,我在使用ViewDragHelper 的过程中,如果拖动后改变View 的 visibility ,那么本次拖动就失效了,原因是 在拖动过程中使用的是类似ViewCompat.offsetTopAndBottom() 这种方法,这个方法虽然能实现非常丝滑的View 的平移,但是如果重新layout后,他就会重新回到原位,很不幸的是 修改visibility 后,由于宽高收到的了影响,ViewGroup要重新layout ,就导致失效了, 这时使用alpha就是一个非常好的选择,

8.clipChildren 裁剪Child

我相信大家的开发的过程中肯定会遇到ViewPager 居中,但是能看到左右两边的一部分,给需要留边的ViewGroup 添加android:clipChildren=true 这个属性就可以做到,
大致类似下面的效果,


image.png

其实类似这些技巧还有非常多,只不过不实际使用想不起来用哪些,自己想了一些就记录下来了,

你可能感兴趣的:(Android 进阶学习(三十四) Android 中一些常用的开发技巧(二)View相关)