0%

Andorid 默认提供的 ViewPager 是不可以循环滚动的, 当滑动到边界时就不能滚动了. 但是实现 Banner 时, 体验最好的还是无限滚动, 为此需要自定义 ViewPager 来实现无限滚动.

PagerAdapter部分:

  1. 首先为了实现无限滚动,getCount()需要返回一个很大的数, 比如Integer.MAX_VALUE.

  2. 为了节省内存, 默认ViewPager只缓存前后1个Page, 就内存中只有perIndex, currentIndex, nextIndex 三个 View , 实际上也发现只需要三个 View 就可以实现无限滚动.

  3. instantiateItem() 方法中, 需要注意的就是获取到 position 对应的 View 后, 如果 view.getParent() 是 container 时, 需要先从 container 中移除该 View , 否则会导致 View 重复添加到 parent 报错.

     @Override
     public Object instantiateItem(ViewGroup container, int position) {
         //mView保存了3个View,用于实现轮播.
         //mView[position % 3]即获取该position对应的View
         View view = mViews[position % 3];
         if (container.equals(view.getParent())) {
             //防止添加到同一个parent
             container.removeView(view);
         }
         container.addView(view);
     }
  4. 重写 destroyItem() 方法, 什么都不操作, 不调用super方法. 这里之所以自己管理 View 的添加和移除, 是因为源码中是先调用 instantiateItem 添加再调用 destroyItem 移除的, 如果用它的方法, 3个 View 的时候重复添加 parent 就会 crash.

  5. isViewFromObject() 方法正常重写即可.

     @Override
     public boolean isViewFromObject(View view, Object object) {
         return view == object;
     }

ViewPager部分:

  1. 默认 ViewPager 的 Scroller 滑动时间是250ms, 轮播时滑动太快了,所以需要反射 Scroller 来设置合理的滑动时间:

     public class MyScroller extends Scroller {
         //默认1秒
         private int mDuration = 1000;
    
         public MyScroller(Context context) {
             this(context, null);
         }
    
         public MyScroller(Context context, Interpolator interpolator) {
             super(context, interpolator);
         }
    
         @Override
         public void startScroll(int startX, int startY, int dx, int dy, int duration) {
             // Ignore received duration, use fixed one instead
             super.startScroll(startX, startY, dx, dy, mDuration);
         }
    
         @Override
         public void startScroll(int startX, int startY, int dx, int dy) {
             // Ignore received duration, use fixed one instead
             super.startScroll(startX, startY, dx, dy, mDuration);
         }
     }
    
     //调用以下方法设置
     public void setViewPagerScrollTime() {
         try {
             Field mScroller = ViewPager.class.getDeclaredField("mScroller");
             mScroller.setAccessible(true);
             MyScroller scroller = new MyScroller(getContext());
             mScroller.set(this, scroller);
         } catch (Exception e) {
             e.printStackTrace();
         }
     }
  2. 注意 ViewPager 的 offset 设为1.

     `mViewPager.setOffscreenPageLimit(1);`
  3. 在设置 Adapter 或更新数据后, 记得设置 ViewPager 到中间页, 否则往左滑动就会到边界.

     //数据大小为size.
     int mid = Integer.MAX_VALUE / 2 - ((Integer.MAX_VALUE / 2) % size);
     holder.mViewPager.setCurrentItem(mid, false);

为了更好的贴近Google设计规范,开始使用Toolbar自定义布局同时设置为ActionBar,在设置的过程中,为了更好的贴近App风格,MenuItem的颜色就需要改变了.

  1. style中设置颜色:

     <style name="ActionBarMenuText" parent="TextAppearance.AppCompat.Widget.ActionBar.Menu">
         <item name="android:textColor">@color/dark_pink</item>
         <item name="android:textSize">@dimen/title_text_size</item>
         <item name="android:layout_marginRight">10dp</item>
     </style>

    这种方式在Nexus上测试可以直接改变,当时也就没有太注意,可是突然发现小米竟然没有改变颜色,于是就只能使用反射来改变了

  2. 利用反射改变:

     public static void setActionBarText(final Activity activity) {
         try {
             final LayoutInflater inflater = activity.getLayoutInflater();
             Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
             field.setAccessible(true);
             field.setBoolean(inflater, false);
             LayoutInflaterCompat.setFactory(inflater, new LayoutInflaterFactory() {
                 @Override
                 public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
                 //因为我使用的是supportv7包
                     if (name.equalsIgnoreCase("android.support.v7.view.menu.IconMenuItemView")
                             || name.equalsIgnoreCase("android.support.v7.view.menu.ActionMenuItemView")) {
                         final View view;
                         try {
                             view = inflater.createView(name, null, attrs);
                             if (view instanceof TextView)
                                 ((TextView) view).setTextColor(activity.getResources().getColor(R.color.dark_pink));
                             return view;
                         } catch (ClassNotFoundException e) {
                             e.printStackTrace();
                         } catch (InflateException ex) {
                             ex.printStackTrace();
                         }
                     }
                     return null;
                 }
             });
         } catch (Exception e) {
    
         }
    
     }

    该方法在Activity的onCreate()时调用即可.

  3. 无论是style还是反射去改变,都只会在Activity创建时设置一次,创建后不好再改变其颜色.但事实上MenuItem也是一个View,在运行时也可以通过View的寻找方式去动态设置:

     View title = getWindow().getDecorView().findViewById(<-- textView的id -->);
     //当然也可以通过findViewByTag()等方法
     if (title != null && title instanceof TextView) {
        ((TextView) title).setTextColor(getResources().getColor(R.color.dark_pink));
     }

下拉刷新,在我看来是非常有革命性的一种交互形式,使用过程非常自然.而与之对应的,就有上拉加载.为了实现下拉刷新和上拉加载,通常都是将整个界面分为三部分:

*************************
     TopLoadingView
*************************

       childView

*************************
    BottomLoadingView
*************************

###下拉刷新
目前常用的下拉刷新有几种样式,在这里推荐一个库SwipeToLoadLayout,

这个库的Demo中把刷新的几种样式:

名字 效果
classic 拉动时LoadingView与childView一起滑动
above 拉动时只滑动LoadingView, childView不滑动
below 拉动时只滑动childView,不滑动LoadingView
scale 拉动时childView跟随拉动距离移动,loadingView滑动较慢

###上拉加载
与下拉刷新相同,上拉加载也可以同时支持这几种样式.

###交互上的细节点:

  1. 无论是下拉刷新还是上拉加载,手指在整个过程中都不应该离开屏幕,应该让用户感觉更加自然.解决办法如下:联动子View.
  2. 注意与横向滑动View的冲突解决,如ViewPager等.解决方法可以参考更合理的拦截.
  3. 下拉刷新是很多App培养出来的用户习惯,用户会习惯性下拉刷新.但是上拉加载却不是,用户并不能很明确得了解是否有更多.同时在除classic的其他模式中,刷新完后loadingView隐藏,界面布局并没有任何变化,用户并不知道是否加载出来.于是就想出一种更加直接的加载**滑动到底部自动加载**.

###实现
RefreshView:包含下拉刷新和自动加载的View

作为一个Android开发者,遇到好的交互当然希望引用过来.iphone的应用可以从屏幕左端右滑返回上一个界面,这个交互在单手操作上非常舒服.那么当然希望在Android上实现.

起初看到知乎和微信都实现了这个功能,于是就自己偷懒没有去想怎么实现,而是先去网上找了一下别人的实现,最后找到了这个Android 向右滑动销毁(finish)Activity, 随着手势的滑动而滑动的效果.作者的原理和代码都很简明,但是copy下来后发现还是有点小bug,于是做了点修复.以及我和它的思路有所区别,最后就没有fork他的库了,而是自己写了一个demo:Android 右滑销毁Activity的Layout.

最初实现的时候,我也想像知乎一样可以从屏幕中间滑动,但最后发现知乎并没有ViewPager之类的横向滑动的控件,所以对原来的库做了以下修改:

  1. 采取了边缘滑动的返回方式.这样便不需要考虑横向滑动的控件的触摸事件冲突.
  2. 根据滑动的比例动态改变背景色,使背景色从黑色40%透明到全透明变化,过渡更加自然.
  3. finish()的时候调用overridePendingTransition(0, 0)使Activity的关闭动画取消,用户感觉更加自然.
  4. dispatchTouchEvent()中对Move进行判断,这样当子View不消费Touch事件时,slideFinishLayout可以从任意位置滑动返回.

待解决优化:

因为这里实现滑动返回的是把Activity设为透明,那么上一个Activity的onStop()方法不会被调用,当Activity打开过多,性能下降很快.

ViewPager 作为一个横向滚动的控件, 在 ViewGroup 中嵌套时会有一些可以优化的细节体验.

案例一: ViewPagerSwipeRefreshLayout

  • 问题说明

    SwipeRefreshLayout 中有 ViewPager 控件, 两者的滑动会相互冲突. 具体表现为 ViewPager 的左右滑动不顺畅, 容易被 SwipeRefreshLayout 拦截(即出现刷新的 View ).

  • 问题原因:

    ViewPager 本身是处理了滚动事件的冲突, 它在横向滑动时会调用 requestDisallowInterceptTouchEvent() 方法使父控件不拦截当前的 Touch 事件序列. 但是 SwipeRefreshLayoutrequestDisallowInterceptTouchEvent() 方法置空了, 所以仍然会拦截当前的 Touch 事件序列.

  • 问题分析:

    为什么 SwipeRefreshLayoutrequestDisallowInterceptTouchEvent() 方法什么都不做?

    • 首先 SwipeRefreshLayout 继承自 ViewGroup .
    • requestDisallowInterceptTouchEvent() 方法置空的情况下, 用户可以从底部下拉刷新一次拉出 LoadingView (即手指不需要离开屏幕).
    • 如果方法调用 ViewGrouprequestDisallowInterceptTouchEvent() 方法, 可以解决 ViewPager的 兼容问题, 但是用户在界面底部下拉至头部后, 无法继续下拉, 需要手指放开一次才能拉出 LoadingView .
  • 目标分析:

那么为了更加顺滑地滚动, 想要的效果当然是一次性拉出 LoadingView **.既然 ViewPager 在左右滑动时才会调用 requestDisallowInterceptTouchEvent() 方法, 那么 SwipeRefreshLayout **只应该在上下滑动时才拦截 Touch 事件.

代码具体逻辑如下:

  1. 记录是否调用了 requestDisallowInterceptTouchEvent() 方法,并且设置为true.
  2. SwipeRefreshLayout 中判断是否是上下滑动.
  3. 如果同时满足1,2, 则调用 super.requestDisallowInterceptTouchEvent(true) 拦截事件.
  4. 否则调用 super.requestDisallowInterceptTouchEvent(false) .

注意:因为 ViewGrouprequestDisallowInterceptTouchEvent 方法返回 true 后, 接下来的 Touch 事件在不会再传递到 onInterceptTouchEvent()方法中, 所以需要在 dispatchTouchEvent() 方法中判断是否为上下滑动.

  • 实现代码(部分):

      //非法按键
      private static final int INVALID_POINTER = -1;
    
      //dispatch方法记录第一次按下的x
      private float mInitialDisPatchDownX;
    
      //dispatch方法记录第一次按下的y
      private float mInitialDisPatchDownY;
    
      //dispatch方法记录的手指
      private int mActiveDispatchPointerId = INVALID_POINTER;
    
      //是否请求拦截
      private boolean hasRequestDisallowIntercept = false;
    
      @Override
      public void requestDisallowInterceptTouchEvent(boolean b) {
          hasRequestDisallowIntercept = b;
          // Nope.
      }
    
      @Override
      public boolean dispatchTouchEvent(MotionEvent ev) {
          switch (ev.getAction()) {
              case MotionEvent.ACTION_DOWN:
                  mActiveDispatchPointerId = MotionEventCompat.getPointerId(ev, 0);
                  final float initialDownX = getMotionEventX(ev, mActiveDispatchPointerId);
                  if (initialDownX != INVALID_POINTER) {
                      mInitialDisPatchDownX = initialDownX;
                  }
                  final float initialDownY = getMotionEventY(ev, mActiveDispatchPointerId);
                  if (mInitialDisPatchDownY != INVALID_POINTER) {
                      mInitialDisPatchDownY = initialDownY;
                  }
                  break;
              case MotionEvent.ACTION_MOVE:
                  if (hasRequestDisallowIntercept) {
                      //解决viewPager滑动冲突问题
                      final float x = getMotionEventX(ev, mActiveDispatchPointerId);
                      final float y = getMotionEventY(ev, mActiveDispatchPointerId);
                      if (mInitialDisPatchDownX != INVALID_POINTER && x != INVALID_POINTER &&
                              mInitialDisPatchDownY != INVALID_POINTER && y != INVALID_POINTER) {
                          final float xDiff = Math.abs(x - mInitialDisPatchDownX);
                          final float yDiff = Math.abs(y - mInitialDisPatchDownY);
                          if (xDiff > mTouchSlop && xDiff * 0.7f > yDiff) {
                              //横向滚动不需要拦截
                              super.requestDisallowInterceptTouchEvent(true);
                          } else {
                              super.requestDisallowInterceptTouchEvent(false);
                          }
                      } else {
                          super.requestDisallowInterceptTouchEvent(false);
                      }
                  }
                  break;
              case MotionEvent.ACTION_UP:
              case MotionEvent.ACTION_CANCEL:
                  if (ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_CANCEL) {
                      hasRequestDisallowIntercept = false;
                  }
                  break;
          }
    
          return super.dispatchTouchEvent(ev);
      }
    
      private float getMotionEventY(MotionEvent ev, int activePointerId) {
          final int index = MotionEventCompat.findPointerIndex(ev, activePointerId);
          if (index < 0) {
              return -1;
          }
          return MotionEventCompat.getY(ev, index);
      }
    
      private float getMotionEventX(MotionEvent ev, int activePointerId) {
          final int index = MotionEventCompat.findPointerIndex(ev, activePointerId);
          if (index < 0) {
              return -1;
          }
          return MotionEventCompat.getX(ev, index);
      }

案例二: ViewPagerRecyclerView

如上图, RecyclerView 中嵌套 ViewPager.

  • 问题说明

    1. 当用户滑动 RecyclerView 后放开手指, RecyclerView 会继续滑动并处于 Fling 状态.
    2. 此时用户重新触摸屏幕, RecyclerView 滑动停止, 但是无法左右滑动 ViewPager, 只能上下滑动 RecyclerView.
  • 问题原因

    当用户重新触摸屏幕, 此时 RecyclerViewonInterceptTouchEvent() 方法还是返回了 true , 所以 ViewGroup 还是继续拦截了事件, 导致 ViewPager 无法处理.

  • 解决思路

    1. 如果是 Fling 状态的 RecyclerView, 在处理 ACTION_DOWN 事件时, 应该与 IDLE 状态下保持一致.
    2. Fling 状态下处理 ACTION_DOWN, onInterceptTouchEvent() 方法应该返回 false.
  • 实现代码:

      @Override
      public boolean onInterceptTouchEvent(MotionEvent e) {
          //isScrolling 为 true 表示是 Fling 状态
          boolean isScrolling = getScrollState() == SCROLL_STATE_SETTLING;
          boolean ans = super.onInterceptTouchEvent(e);
          if (ans && isScrolling && e.getAction() == MotionEvent.ACTION_DOWN) {
              //先调用 onTouchEvent() 使 RecyclerView 停下来
              onTouchEvent(e);
              //反射恢复 ScrollState
              try {
                  Field field = RecyclerView.class.getDeclaredField("mScrollState");
                  field.setAccessible(true);
                  field.setInt(this, SCROLL_STATE_IDLE);
              } catch (NoSuchFieldException e1) {
                  e1.printStackTrace();
              } catch (IllegalAccessException e1) {
                  e1.printStackTrace();
              }
              return false;
          }
          return ans;
      }

案例三: ViewPagerScrollView

ScrollView 嵌套 ViewPager, Fling 状态下会有跟 RecyclerView 一样的问题, 所以解决思路也是一样的, 只是代码部分有所不同.

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean ans = super.onInterceptTouchEvent(ev);
        if (ans && ev.getAction() == MotionEvent.ACTION_DOWN) {
            onTouchEvent(ev);
            Field field = null;
            try {
                field = NestedScrollView.class.getDeclaredField("mIsBeingDragged");
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            }
            if (field != null) {
                field.setAccessible(true);
                try {
                    field.setBoolean(this, false);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
            return false;
        }
        return ans;
    }

在做SuperRefreshLayout这个下拉刷新的控件时,发现了一个问题,这也是很多下拉刷新控件都会有的问题.

如果用户在顶部下拉时触发了控件的下拉效果,此时用户不放手向上滑动,会取消该控件的下拉效果.但是**用户继续向上滑动时,子View却不会向下滚动了.**一定要用户抬起手重新向下滚动才可以.

界面状态分别为:

普通状态(图1) 拉动中(图2) 刷新中(图3)

当用户从普通状态(图1)向下拉动进入拉动中状态(图2),用户向上滑动取消拉动动作,恢复普通状态(图1),此时继续向上滑动,界面并不会跟随用户手指滚动.

问题分析:

外层刷新控件称为RefreshLayout, 内部子控件称为子View.

  1. RefreshLayout在由图1向下滑动至图2的过程中,其实是RefreshLayout拦截了当前的Touch事件,此时:

    1. RefreshLayout的onInterceptTouchEvent()方法返回true,后续的Touch事件都会交给RefreshLayout的onTouchEvent()处理.
    2. 子View的dispatchTouchEvent()方法会接收到Action_cancel事件,表示当前触摸事件已经结束,不会再处理接收到的除Action_down之外的事件.
    3. RefreshLayout的onTouchEvent()方法消费Touch事件,界面逐渐变为图2.
  2. RefreshLayout由图2向上滑动恢复图1状态后并继续向上滑动时,界面无法向下滑动,因为:

    1. RefreshLayout拦截了Touch事件,子View无法接收.
    2. RefreshLayout的onTouchEvent()方法返回false(因为当子View可以向下滑动时不需要处理).

解决思路:

我们的最终目标就是把RefreshLayout”错误”拦截的Touch事件交给子View来处理.

问题 原因 解决
怎么判断是”错误”的拦截 有需要拦截的Touch事件(如滑动到顶部后继续下拉) 在拦截后记录标志位, 在RefreshLayout的onTouchEvent()方法中不该处理(return false)的情况中判断该标志位
为什么子View无法接收Touch事件 在一开始滚动的过程中RefreshLayout的onInterceptTouchEvent()方法返回true,拦截了所有的Touch事件 事件传递机制是系统设定的,不好更改.但可以在RefreshLayout的onTouchEvent()方法中调用子View的dispatchTouchEvent()方法,让子View接受并处理该Touch事件
怎么让子View能够处理该Touch事件 RefreshLayout拦截Touch事件后,子View会接收到Action_cancel事件,后续事件无法处理 判断是否为拦截后的第一个Touch事件,是的话先模拟一个Action_down事件并传递给子View,使子View认为这是一个新的Touch事件序列.

解决代码:

以下代码为简略版:

//首先定义一个变量,检测是否是同一点击事件序列中第一个拦截后应该处理的move事件
private boolean isFirstMoveAfterIntercept = false;

 //在dispatchTouchEvent方法中初始化
 @Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        isFirstMoveAfterIntercept = true;
    }
    if (ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_CANCEL) {
        isFirstMoveAfterIntercept = false;
    }
    return super.dispatchTouchEvent(ev);
}

@Override
public boolean onTouchEvent(MotionEvent ev) {
    ......
    //mIsBeingDragged表示被拦截了
    if (mIsBeingDragged) {
        //此时是错误的拦截(不需要拦截的情况)
        if (originalDragPercent < 0) {
            //判断是否是拦截后的第一个Move事件
            if (isFirstMoveAfterIntercept) {
                // 先为子View模拟一个Action_Down事件,使其认为这是一个新的Touch事件序列
                MotionEvent event = MotionEvent.obtain(ev);
                event.setAction(MotionEvent.ACTION_DOWN);
                mTarget.dispatchTouchEvent(event);
                isFirstMoveAfterIntercept = false;
            }
            //将Move事件传递到子View
            mTarget.dispatchTouchEvent(ev);
            return false;
        }
        ...
    }
    ....
}

Android开发固然会用到Toast,如果每次都Toast.makeToast来显示Toast的话,一方面写法麻烦,第二是多次调用Toast会使Toast一直浮在界面最上层,影响交互,最好的方式是封装一个ToastUtil:

public class ToastUtil {

    private static volatile ToastUtil sToastUtil = null;

    private Toast mToast = null;

    /**
     * 获取实例
     *
     * @return
     */
    public static ToastUtil getInstance() {
        if (sToastUtil == null) {
            synchronized (ToastUtil.class) {
                if (sToastUtil == null) {
                    sToastUtil = new ToastUtil();
                }
            }
        }
        return sToastUtil;
    }

    protected Handler handler = new Handler(Looper.getMainLooper());

    /**
     * 显示Toast,多次调用此函数时,Toast显示的时间不会累计,并且显示内容为最后一次调用时传入的内容
     * 持续时间默认为short
     * @param tips 要显示的内容
     *            {@link Toast#LENGTH_LONG}
     */
    public void showToast(final String tips){
        showToast(tips, Toast.LENGTH_SHORT);
    }

    public void showToast(final int tips){
        showToast(tips, Toast.LENGTH_SHORT);
    }
    /**
     * 显示Toast,多次调用此函数时,Toast显示的时间不会累计,并且显示内容为最后一次调用时传入的内容
     *
     * @param tips 要显示的内容
     * @param duration 持续时间,参见{@link Toast#LENGTH_SHORT}和
     *            {@link Toast#LENGTH_LONG}
     */
    public void showToast(final String tips, final int duration) {
        if (android.text.TextUtils.isEmpty(tips)) {
            return;
        }
        handler.post(new Runnable() {
            @Override
            public void run() {
                if (mToast == null) {
                    mToast = Toast.makeText(MyApplication.getMyApplicationContext(), tips, duration);
                    mToast.show();
                } else {
                    //mToast.cancel();
                    //mToast.setView(mToast.getView());
                    mToast.setText(tips);
                    mToast.setDuration(duration);
                    mToast.show();
                }
            }
        });
    }

    public void showToast(final int tips, final int duration) {
        handler.post(new Runnable() {
            @Override
            public void run() {
                if (mToast == null) {
                    mToast = Toast.makeText(MyApplication.getMyApplicationContext(), tips, duration);
                    mToast.show();
                } else {
                    //mToast.cancel();
                    //mToast.setView(mToast.getView());
                    mToast.setText(tips);
                    mToast.setDuration(duration);
                    mToast.show();
                }
            }
        });
    }
}

其实Toast调用的Context为ApplicationContext,在Application中初始化即可.

public class MyApplication extends Application {

    private static Context myContext;

    @Override
    public void onCreate() {
        super.onCreate();
        myContext = this.getApplicationContext();
    }

    public static Context getMyApplicationContext(){
        return myContext;
    }
}

文件存储分内部文件存储和外部存储。

默认情况下应用安装到内部存储,可以通过AndroidMainfest.xml文件下applicationandroid:installLocation属性设置安装位置。

类别 内部存储 外部存储
可用 一直可用 没有挂载时不可用
可访问性 只能被自己的APP访问 全部可读
删除 卸载应用时清除 只有通过getExternalFilesDir()方法返回的路径的文件才会被删除

读取外部存储时,需要添加以下权限:

写入的权限已经在SDK>18的版本中默认拥有了
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
                 android:maxSdkVersion="18" />

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

内部存储

  1. getFilesDir()

    返回app可用的内部存储文件地址。

  2. getCacheDir()

    返回app可用的缓存文件地址。当缓存较大并且系统空间不足时会删除其中的文件。

内部存储保存文件:

File file = new File(context.getFilesDir(), filename);
String filename = "myfile";
String string = "Hello world!";
FileOutputStream outputStream;

try {
      outputStream = openFileOutput(filename, Context.MODE_PRIVATE);
      outputStream.write(string.getBytes());
      outputStream.close();
    } catch (Exception e) {
          e.printStackTrace();
}

设置MODE_PRIVATE可以使其他应用无法访问文件。

内部存储保存缓存文件:

public File getTempFile(Context context, String url) {
    File file;
    try {
        String fileName = Uri.parse(url).getLastPathSegment();
        file = File.createTempFile(fileName, null, context.getCacheDir());
    catch (IOException e) {
        // Error while creating file
    }
    return file;
}

外部存储

因为外部存储可能没有被挂载,所以需要先判断是否可用:

/* Checks if external storage is available for read and write */
public boolean isExternalStorageWritable() {
    String state = Environment.getExternalStorageState();
    if (Environment.MEDIA_MOUNTED.equals(state)) {
        return true;
    }
    return false;
}

/* Checks if external storage is available to at least read */
public boolean isExternalStorageReadable() {
    String state = Environment.getExternalStorageState();
    if (Environment.MEDIA_MOUNTED.equals(state) ||
        Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
        return true;
    }
    return false;
}
  1. getExternalStoragePublicDirectory()

    返回可以任意访问的外部存储路径,删除应用时不会清除该路径。

  2. getExternalFilesDir()

    返回只能自己访问的外部存储路径,删除应用时该目录下的文件会被删除。


getFreeSpace ()getTotalSpace ()会返回路径下的可用空间和全部空间的字节。

Android线程相关这篇文章中说到了利用handler实现多线程并通信的方法,于是翻阅了很多博客,也查看了源码,终于从自己的角度理解了handler的原理。于是来梳理一下~

参考:

  1. Android异步消息处理机制完全解析,带你从源码的角度彻底理解
  2. Android异步处理三:Handler+Looper+MessageQueue深入详解

以线程A为主线程,B为新线程为例:

  1. A线程通过Looper.prepare()创建Looper对象,并初始化其MessageQueue消息队列,此时Looper与线程A绑定。
  2. A线程中new handler对象,构造函数中绑定Looper对象,同时也绑定了MessageQueue。
  3. 新建线程B,并执行操作,最后通过A线程中的handler调用sendMessage()方法或post()方法使Message入队Looper中的MessageQueue。
  4. A线程中的Looper对象通过调用Looper.loop()方法使MessageQueue中的Message不断出队,并分发到handler中。
  5. A线程中的handler的handleMessage方法处理回调。

从源码的角度:

  1. Looper.prepare(),即初始化Looper对象,及MessageQueue,同时绑定线程(ThreadLocal).

     static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
     private static Looper sMainLooper;  // guarded by Looper.class
    
     final MessageQueue mQueue;
     final Thread mThread;
    
     public static void prepare() {
         prepare(true);
     }
    
     private static void prepare(boolean quitAllowed) {
         if (sThreadLocal.get() != null) {
             throw new RuntimeException("Only one Looper may be created per thread");
         }
         sThreadLocal.set(new Looper(quitAllowed));
     }
    
     private Looper(boolean quitAllowed) {
         mQueue = new MessageQueue(quitAllowed);
         mThread = Thread.currentThread();
     }
  2. Handler handler = new Handler(),新建hanlder对象,同时绑定Looper对象及MessageQueue。

     public Handler(Callback callback, boolean async) {
     if (FIND_POTENTIAL_LEAKS) {
         final Class<? extends Handler> klass = getClass();
         if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                 (klass.getModifiers() & Modifier.STATIC) == 0) {
             Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                 klass.getCanonicalName());
         }
     }
    
     mLooper = Looper.myLooper();
     if (mLooper == null) {
         throw new RuntimeException(
             "Can't create handler inside thread that has not called Looper.prepare()");
     }
     mQueue = mLooper.mQueue;
     mCallback = callback;
     mAsynchronous = async;

    }

  3. 新建线程B,在其runnable的run方法中调用线程A的handler的sendMessage(Message)post(Runnable)方法,这两种方法最后都会调用handler中MessageQueue的enqueueMessage方法。该方法使Message按时间顺序排列。

`post(Runnable)`方法会新建带有Runnable对象的Message对象,然后在辗转调用`enqueueMessage`方法。

    public final boolean post(Runnable r)
    {
           return  sendMessageDelayed(getPostMessage(r), 0);
    }

    boolean enqueueMessage(Message msg, long when) {
        if (msg.target == null) {
            throw new IllegalArgumentException("Message must have a target.");
        }
        if (msg.isInUse()) {
            throw new IllegalStateException(msg + " This message is already in use.");
        }

        synchronized (this) {
            if (mQuitting) {
                IllegalStateException e = new IllegalStateException(
                        msg.target + " sending message to a Handler on a dead thread");
                Log.w("MessageQueue", e.getMessage(), e);
                msg.recycle();
                return false;
            }

            msg.markInUse();
            msg.when = when;
            Message p = mMessages;
            boolean needWake;
            if (p == null || when == 0 || when < p.when) {
                // New head, wake up the event queue if blocked.
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                // Inserted within the middle of the queue.  Usually we don't have to wake
                // up the event queue unless there is a barrier at the head of the queue
                // and the message is the earliest asynchronous message in the queue.
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                msg.next = p; // invariant: p == prev.next
                prev.next = msg;
            }

            // We can assume mPtr != 0 because mQuitting is false.
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }
  1. Looper.loop()通过死循环使MessageQueue中的Message出队直到队列为空,message.target即为handler的回调。

     public static void loop() {
         final Looper me = myLooper();
         if (me == null) {
             throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
         }
         final MessageQueue queue = me.mQueue;
    
         // Make sure the identity of this thread is that of the local process,
         // and keep track of what that identity token actually is.
         Binder.clearCallingIdentity();
         final long ident = Binder.clearCallingIdentity();
    
         for (;;) {
             Message msg = queue.next(); // might block
             if (msg == null) {
                 // No message indicates that the message queue is quitting.
                 return;
             }
    
             // This must be in a local variable, in case a UI event sets the logger
             Printer logging = me.mLogging;
             if (logging != null) {
                 logging.println(">>>>> Dispatching to " + msg.target + " " +
                         msg.callback + ": " + msg.what);
             }
    
             msg.target.dispatchMessage(msg);
    
             if (logging != null) {
                 logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
             }
    
             // Make sure that during the course of dispatching the
             // identity of the thread wasn't corrupted.
             final long newIdent = Binder.clearCallingIdentity();
             if (ident != newIdent) {
                 Log.wtf(TAG, "Thread identity changed from 0x"
                     + Long.toHexString(ident) + " to 0x"
                     + Long.toHexString(newIdent) + " while dispatching to "
                     + msg.target.getClass().getName() + " "
                     + msg.callback + " what=" + msg.what);
             }
    
             msg.recycleUnchecked();
         }
     }
  2. handler重写handleMessage方法处理回调,可以看到dispatchMessage方法即先判断有没有callback,而callback就是handler构造函数中的callback。

     public void dispatchMessage(Message msg) {
         if (msg.callback != null) {
             handleCallback(msg);
         } else {
             if (mCallback != null) {
                 if (mCallback.handleMessage(msg)) {
                        return;
                 }
             }
             handleMessage(msg);
         }
     }

所以标准的线程通信写法为(可以从Looper的源码中看到):

class LooperThread extends Thread {
      public Handler mHandler;

  public void run() {
      Looper.prepare();

      mHandler = new Handler() {
          public void handleMessage(Message msg) {
              // process incoming messages here
          }
      };

      Looper.loop();
  }

}

  1. Android studio卡顿优化

    除了机器配置之外,也可以给Android Studio分配更多内存.
    打开Android Studio的安装目录,Mac下进入bin目录,打开studio.vmoptions文件,
    修改以下配置

    1. -Xms:JVM堆内存,默认是物理内存的1/64.

    2. -Xmx:JVM最大内存,默认是物理内存的1/4.

    3. -XX:MaxPermSize:最大非堆内存,默认是物理内存的1/4.

    4. -XX:ReservedCodeCacheSize:缓存大小.

      把这些调大就好了~

  1. Gradle没有自动下载dependence包。

    解决方法:

    1. 检查gradle环境。
    2. 在工程目录下依次运行gradle clean , gradle assemble
  2. Executor Singleton not started

    升级一下gradle到最新版本,然后在project的build.gradle中修改gradle版本即可.

  3. adb连接不到手机

    以魅族mx4为例,直接连接手机的话其实是识别不到的,adb连接需要添加机器的Vender ID。

    • 连接电脑,打开终端,输入

        system_profiler SPUSBDataType
    • 找到手机(mx4),复制Vender ID(我的是0x2a45),注意只复制内容,然后进入adb_usb.ini文件。

        sudo vim ~/.android/adb_usb.ini
    • 将Vender ID直接复制在下面就好。然后依次输入

        adb kill-server    
        adb start-server

    注意adb_usb.ini这个文件不能有错误的行,或者vim打开产生的缓存文件没有删除,这样会导致无法运行adb

    顺便说一下环境变量的配置

    首先进入.bashrc(使用bash的话是/.bashrc,使用zsh的话是/.zshrc)文件:

     vim ~/.bashrc

    然后添加格式为:

     export PATH="/Users/qiu/Documents/adt-bundle-mac-x86_64-20140702/sdk/tools:$PATH"

    这样环境变量便是append进去了,不会覆盖PATH。
    最后记得更新一下环境变量:

     source ~/.bashrc
  1. 工程不在根目录下,比如在Test工程下又添加了Test1工程,就像这样:

    然后因为找不到AndroidManifest.xml文件导致无法运行。

    解决方法如下:

    • File->Project Structure
    • 在左边点击Facets
    • 找到你的项目,没有就添加一个
    • 把Structure里面的路径全部替换为当前实际工程的项目。
    • 顺利运行。
  2. java finished with non-zero exit value xerror code问题,常见的code有:

    1. vcs缓存与新添加的资源冲突,clean 工程再重新运行即可.

    2. 有两种情况:

      1. 可能是libs中添加了重复包,这时候需要看看是哪两个依赖重复了,然后去掉一个.
      2. 可能是方法数过了65535.这时候就需要multi-dex分包了.
    3. 这个我在分包后出现过,目前解决方法是在build.gradle文件中添加:

       dexOptions {
           preDexLibraries = false
           incremental true
           javaMaxHeapSize "4g"
       }