Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all 5930 articles
Browse latest View live

AndroidStudio快捷键大全

$
0
0

Ctrl

快捷键 介绍
Ctrl + F 在当前文件进行文本查找 (必备)
Ctrl + R 在当前文件进行文本替换 (必备)
Ctrl + Z 撤销 (必备)
Ctrl + Y 删除光标所在行 或 删除选中的行 (必备)
Ctrl + X 剪切光标所在行 或 剪切选择内容
Ctrl + C 复制光标所在行 或 复制选择内容
Ctrl + D 复制光标所在行 或 复制选择内容,并把复制内容插入光标位置下面 (必备)
Ctrl + W 递进式选择代码块。可选中光标所在的单词或段落,连续按会在原有选中的基础上再扩展选中范围 (必备)
Ctrl + E 显示最近打开的文件记录列表
Ctrl + N 根据输入的 类名 查找类文件
Ctrl + G 在当前文件跳转到指定行处
Ctrl + J 插入自定义动态代码模板
Ctrl + P 方法参数提示显示
Ctrl + Q 光标所在的变量 \/ 类名 \/ 方法名等上面(也可以在提示补充的时候按),显示文档内容
Ctrl + U 前往当前光标所在的方法的父类的方法 \/ 接口定义
Ctrl + B 进入光标所在的方法\/变量的接口或是定义处,等效于 Ctrl + 左键单击
Ctrl + K 版本控制提交项目,需要此项目有加入到版本控制才可用
Ctrl + T 版本控制更新项目,需要此项目有加入到版本控制才可用
Ctrl + H 显示当前类的层次结构
Ctrl + O 选择可重写的方法
Ctrl + I 选择可继承的方法
Ctrl + + 展开代码
Ctrl + - 折叠代码
Ctrl + \/ 注释光标所在行代码,会根据当前不同文件类型使用不同的注释符号 (必备)
Ctrl + [ 移动光标到当前所在代码的花括号开始位置
Ctrl + ] 移动光标到当前所在代码的花括号结束位置
Ctrl + F1 在光标所在的错误代码处显示错误信息
Ctrl + F3 调转到所选中的词的下一个引用位置
Ctrl + F4 关闭当前编辑文件
Ctrl + F8 在 Debug 模式下,设置光标当前行为断点,如果当前已经是断点则去掉断点
Ctrl + F9 执行 Make Project 操作
Ctrl + F11 选中文件 \/ 文件夹,使用助记符设定 \/ 取消书签
Ctrl + F12 弹出当前文件结构层,可以在弹出的层上直接输入,进行筛选
Ctrl + Tab 编辑窗口切换,如果在切换的过程又加按上delete,则是关闭对应选中的窗口
Ctrl + Enter 智能分隔行
Ctrl + End 跳到文件尾
Ctrl + Home 跳到文件头
Ctrl + Space 基础代码补全,默认在 Windows 系统上被输入法占用,需要进行修改,建议修改为 Ctrl + 逗号 (必备)
Ctrl + Delete 删除光标后面的单词或是中文句
Ctrl + BackSpace 删除光标前面的单词或是中文句
Ctrl + 1,2,3...9 定位到对应数值的书签位置
Ctrl + 左键单击 在打开的文件标题上,弹出该文件路径
Ctrl + 光标定位 按 Ctrl 不要松开,会显示光标所在的类信息摘要
Ctrl + 左方向键 光标跳转到当前单词 \/ 中文句的左侧开头位置
Ctrl + 右方向键 光标跳转到当前单词 \/ 中文句的右侧开头位置
Ctrl + 前方向键 等效于鼠标滚轮向前效果
Ctrl + 后方向键 等效于鼠标滚轮向后效果

Alt

快捷键 介绍
Alt + ` 显示版本控制常用操作菜单弹出层
Alt + Q 弹出一个提示,显示当前类的声明 \/ 上下文信息
Alt + F1 显示当前文件选择目标弹出层,弹出层中有很多目标可以进行选择
Alt + F2 对于前面页面,显示各类浏览器打开目标选择弹出层
Alt + F3 选中文本,逐个往下查找相同文本,并高亮显示
Alt + F7 查找光标所在的方法 \/ 变量 \/ 类被调用的地方
Alt + F8 在 Debug 的状态下,选中对象,弹出可输入计算表达式调试框,查看该输入内容的调试结果
Alt + Home 定位 \/ 显示到当前文件的 Navigation Bar
Alt + Enter IntelliJ IDEA 根据光标所在问题,提供快速修复选择,光标放在的位置不同提示的结果也不同(必备)
Alt + Insert 代码自动生成,如生成对象的 set \/ get 方法,构造函数,toString() 等
Alt + 左方向键 按左方向切换当前已打开的文件视图
Alt + 右方向键 按右方向切换当前已打开的文件视图
Alt + 前方向键 当前光标跳转到当前文件的前一个方法名位置
Alt + 后方向键 当前光标跳转到当前文件的后一个方法名位置
Alt + 1,2,3...9 显示对应数值的选项卡,其中 1 是 Project 用得最多

Shift

快捷键 介绍
Shift + F1 如果有外部文档可以连接外部文档
Shift + F2 跳转到上一个高亮错误 或 警告位置
Shift + F3 在查找模式下,查找匹配上一个
Shift + F4 对当前打开的文件,使用新Windows窗口打开,旧窗口保留
Shift + F6 对文件 \/ 文件夹 重命名
Shift + F7 在 Debug 模式下,智能步入。断点所在行上有多个方法调用,会弹出进入哪个方法
Shift + F8 在 Debug 模式下,跳出,表现出来的效果跟 F9 一样
Shift + F9 等效于点击工具栏的 Debug 按钮
Shift + F10 等效于点击工具栏的 Run 按钮
Shift + F11 弹出书签显示层
Shift + Tab 取消缩进
Shift + ESC 隐藏当前 或 最后一个激活的工具窗口
Shift + End 选中光标到当前行尾位置
Shift + Home 选中光标到当前行头位置
Shift + Enter 开始新一行。光标所在行下空出一行,光标定位到新行位置
Shift + 左键单击 在打开的文件名上按此快捷键,可以关闭当前打开文件
Shift + 滚轮前后滚动 当前文件的横向滚动轴滚动

Ctrl + Alt

快捷键 介绍
Ctrl + Alt + L 格式化代码,可以对当前文件和整个包目录使用 (必备)
Ctrl + Alt + O 优化导入的类,可以对当前文件和整个包目录使用 (必备)
Ctrl + Alt + I 光标所在行 或 选中部分进行自动代码缩进,有点类似格式化
Ctrl + Alt + T 对选中的代码弹出环绕选项弹出层
Ctrl + Alt + J 弹出模板选择窗口,将选定的代码加入动态模板中
Ctrl + Alt + H 调用层次
Ctrl + Alt + B 在某个调用的方法名上使用会跳到具体的实现处,可以跳过接口
Ctrl + Alt + V 快速引进变量
Ctrl + Alt + Y 同步、刷新
Ctrl + Alt + S 打开 IntelliJ IDEA 系统设置
Ctrl + Alt + F7 显示使用的地方。寻找被该类或是变量被调用的地方,用弹出框的方式找出来
Ctrl + Alt + F11 切换全屏模式
Ctrl + Alt + Enter 光标所在行上空出一行,光标定位到新行
Ctrl + Alt + Home 弹出跟当前文件有关联的文件弹出层
Ctrl + Alt + Space 类名自动完成
Ctrl + Alt + 左方向键 退回到上一个操作的地方 (必备)
Ctrl + Alt + 右方向键 前进到上一个操作的地方 (必备)
Ctrl + Alt + 前方向键 在查找模式下,跳到上个查找的文件
Ctrl + Alt + 后方向键 在查找模式下,跳到下个查找的文件

Ctrl + Shift

快捷键 介绍
Ctrl + Shift + F 根据输入内容查找整个项目 或 指定目录内文件 (必备)
Ctrl + Shift + R 根据输入内容替换对应内容,范围为整个项目 或 指定目录内文件 (必备)
Ctrl + Shift + J 自动将下一行合并到当前行末尾 (必备)
Ctrl + Shift + Z 取消撤销 (必备)
Ctrl + Shift + W 递进式取消选择代码块。可选中光标所在的单词或段落,连续按会在原有选中的基础上再扩展取消选中范围 (必备)
Ctrl + Shift + N 通过文件名定位 \/ 打开文件 \/ 目录,打开目录需要在输入的内容后面多加一个正斜杠(必备)
Ctrl + Shift + U 对选中的代码进行大 \/ 小写轮流转换 (必备)
Ctrl + Shift + T 对当前类生成单元测试类,如果已经存在的单元测试类则可以进行选择
Ctrl + Shift + C 复制当前文件磁盘路径到剪贴板
Ctrl + Shift + V 弹出缓存的最近拷贝的内容管理器弹出层
Ctrl + Shift + E 显示最近修改的文件列表的弹出层
Ctrl + Shift + H 显示方法层次结构
Ctrl + Shift + B 跳转到类型声明处
Ctrl + Shift + I 快速查看光标所在的方法 或 类的定义
Ctrl + Shift + A 查找动作 \/ 设置
Ctrl + Shift + \/ 代码块注释 (必备)
Ctrl + Shift + [ 选中从光标所在位置到它的顶部中括号位置
Ctrl + Shift + ] 选中从光标所在位置到它的底部中括号位置
Ctrl + Shift + + 展开所有代码
Ctrl + Shift + - 折叠所有代码
Ctrl + Shift + F7 高亮显示所有该选中文本,按Esc高亮消失
Ctrl + Shift + F8 在 Debug 模式下,指定断点进入条件
Ctrl + Shift + F9 编译选中的文件 \/ 包 \/ Module
Ctrl + Shift + F12 编辑器最大化
Ctrl + Shift + Space 智能代码提示
Ctrl + Shift + Enter 自动结束代码,行末自动添加分号 (必备)
Ctrl + Shift + Backspace 退回到上次修改的地方
Ctrl + Shift + 1,2,3...9 快速添加指定数值的书签
Ctrl + Shift + 左键单击 把光标放在某个类变量上,按此快捷键可以直接定位到该类中 (必备)
Ctrl + Shift + 左方向键 在代码文件上,光标跳转到当前单词 \/ 中文句的左侧开头位置,同时选中该单词 \/ 中文句
Ctrl + Shift + 右方向键 在代码文件上,光标跳转到当前单词 \/ 中文句的右侧开头位置,同时选中该单词 \/ 中文句
Ctrl + Shift + 左方向键 在光标焦点是在工具选项卡上,缩小选项卡区域
Ctrl + Shift + 右方向键 在光标焦点是在工具选项卡上,扩大选项卡区域
Ctrl + Shift + 前方向键 光标放在方法名上,将方法移动到上一个方法前面,调整方法排序
Ctrl + Shift + 后方向键 光标放在方法名上,将方法移动到下一个方法前面,调整方法排序

Alt + Shift

快捷键 介绍
Alt + Shift + N 选择 \/ 添加 task
Alt + Shift + F 显示添加到收藏夹弹出层 \/ 添加到收藏夹
Alt + Shift + C 查看最近操作项目的变化情况列表
Alt + Shift + I 查看项目当前文件
Alt + Shift + F7 在 Debug 模式下,下一步,进入当前方法体内,如果方法体还有方法,则会进入该内嵌的方法中,依此循环进入
Alt + Shift + F9 弹出 Debug 的可选择菜单
Alt + Shift + F10 弹出 Run 的可选择菜单
Alt + Shift + 左键双击 选择被双击的单词 \/ 中文句,按住不放,可以同时选择其他单词 \/ 中文句
Alt + Shift + 前方向键 移动光标所在行向上移动
Alt + Shift + 后方向键 移动光标所在行向下移动

Ctrl + Shift + Alt

快捷键 介绍
Ctrl + Shift + Alt + V 无格式黏贴
Ctrl + Shift + Alt + N 前往指定的变量 \/ 方法
Ctrl + Shift + Alt + S 打开当前项目设置
Ctrl + Shift + Alt + C 复制参考信息

其他

快捷键 介绍
F2 跳转到下一个高亮错误 或 警告位置 (必备)
F3 在查找模式下,定位到下一个匹配处
F4 编辑源
F7 在 Debug 模式下,进入下一步,如果当前行断点是一个方法,则进入当前方法体内,如果该方法体还有方法,则不会进入该内嵌的方法中
F8 在 Debug 模式下,进入下一步,如果当前行断点是一个方法,则不进入当前方法体内
F9 在 Debug 模式下,恢复程序运行,但是如果该断点下面代码还有断点则停在下一个断点上
F11 添加书签
F12 回到前一个工具窗口
Tab 缩进
ESC 从工具窗口进入代码文件窗口
连按两次Shift 弹出 Search Everywhere 弹出层
作者:Me_Dong 发表于2016/11/4 17:24:56 原文链接
阅读:67 评论:0 查看评论

极速完成一个新闻类APP(RxJava+Retrofit)

$
0
0

快速完成一个新闻APP

本Demo主要使用的技术:

  • 看标题就知道了
  • Material Design
  • 聚合数据

效果

直接点吧,先看下效果
这里写图片描述

Demo架构

老司机们一看就知道界面是由ViewPager+Fragment组成,还是比较简单的。新闻详情页面主要是采用了design包下的CoordinatorLayout作为父布局,因为要做出那个下拉折叠效果嘛。然后点击新闻列表时会有一个转场动画,不知道细心的朋友们有木有看出来,上拉刷新是采用的官方的SwipeRefreshLayout
一切都追求原滋原味。
整个Demo的网络请求是通过RxJava+Retrofit来实现的,为什么用这对基友组合呢?
三个字 “太爽了”
OK,后面会有相关的介绍。

代码分析

封装BaseActivity 
虽然整个Demo就两个Activity,那我们还是封装一下,因为我喜欢追求代码简洁(与后面可能会有些出入)额额,,
先上波代码吧

public abstract class BaseActivity extends AppCompatActivity {

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

    protected abstract void initContentView(Bundle savedInstanceState);
    /**
     * 初始化沉浸式状态栏
     */
    private void initStatusBar(){
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT){//4.4 全透明状态栏
            getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
        }
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {//5.0 全透明实现
            Window window = getWindow();
            window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
            window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                    | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
            window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
            window.setStatusBarColor(Color.TRANSPARENT);//calculateStatusColor(Color.WHITE, (int) alphaValue)
        }
    }

    @SuppressWarnings("unchecked")
    public final <E extends View> E findView(int id){
        try {
            return (E) findViewById(id);
        }catch (ClassCastException e){
            throw  e;
        }
    }
}

其实也没什么亮点,就是封装下共同的方法,一个是沉浸式状态栏,一个是我为了偷懒,不想在findviewById的时候加个强制类型转换。当然也可以通过框架注入,和databinding来解决这个,但这次的重点不是他们。

创建实体类
对了,这些数据都是从聚合数据获取下来的,具体细节就不多说了,就是申请个key,填写一些参数,然后它会返回一串json数据。我们就通过这些json数据去生成对应的实体类,用一个良心之作的工具 GsonFormat,具体操作可以自行Google或者百度。
 使用这个工具一是为了偷懒,二是为了配合gson来对json解析。

创建配置文件
项目中可能会遇到一些很多地方都会用到的常量,比如说聚合数据的key等等,我们可以创建一个接口
将这些数据写到这个接口里面,这样的话,哪里要用就直接继承这个接口就OK了。

public interface Config {
     String[] ARRYTITLES ={"头条","社会","科技","国内","国际","娱乐","时尚","军事","体育","财经"};
     String KEY_POSTION="key_postion";
     String[] ARRYTYPE={"top","shehui","keji","guonei","guoji","yule","shishang","junshi","tiyu","caijing"};
     String KEY_IMG_URL="imgurl";
     String KEY_CONTENT_URL="contenturl";
     String KEY_TYPE="type";
     String KEY_JUHE="0489bcea378ce792facda791d0f1e188";

}

因为请求不同的类型的新闻,参数不一样,所以弄个数组,把参数存入进去,记得与类型对应。

主Activity编写
这里因为创建项目的时候手贱了下,点了那个有侧滑的activity,所以一些生成了很多没什么卵用的代码(至少这个项目里面没什么用)

public class MainActivity extends BaseActivity
        implements NavigationView.OnNavigationItemSelectedListener,Config {
    private TabLayout mTabLayout;
    private ViewPager mViewPager;
    private ViewPagerAdapter mAdapter;
    private List<String> mTitles=new ArrayList<>();

    @Override
    protected void initContentView(Bundle savedInstanceState) {
        setContentView(R.layout.activity_main);
        Toolbar toolbar = findView(R.id.toolbar);
        setSupportActionBar(toolbar);
        mTabLayout=findView(R.id.tab_layout);
        mViewPager=findView(R.id.viewpager);

        DrawerLayout drawer = findView(R.id.drawer_layout);
        ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
                this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
        drawer.setDrawerListener(toggle);
        toggle.syncState();

        NavigationView navigationView = findView(R.id.nav_view);
        navigationView.setNavigationItemSelectedListener(this);

        initTitle();
        mAdapter=new ViewPagerAdapter(getSupportFragmentManager(),mTitles);
        mViewPager.setAdapter(mAdapter);
        mTabLayout.setupWithViewPager(mViewPager);
        mTabLayout.setTabMode(TabLayout.MODE_SCROLLABLE);
    }


    private void initTitle(){
        for (int i=0;i<ARRYTITLES.length;i++){
            mTitles.add(ARRYTITLES[i]);
        }

    }
 }

这里就是进行初始化一些view,将fragment添加进去,viewpager+tablayout基友组合。

创建Fragment的适配器
先说说适配器吧,因为这里要用的fragment比较多,所以继承FragmentStatePagerAdapter,Why?

内存优化

因为一个Fragment占的内存还是比较大,一旦fragment数量比较多了,后果你懂的。当页面不可见时, 对应的Fragment实例可能会被销毁,但是Fragment的状态会被保存,所以一些提高了我们的app性能。

public class ViewPagerAdapter extends FragmentStatePagerAdapter {
    private List<String> mTitles;


    public ViewPagerAdapter(FragmentManager fm, List<String> mTitles) {
        super(fm);
        this.mTitles = mTitles;

    }

    @Override
    public Fragment getItem(int position) {

        return ContentFragment.instance(position);
    }

    @Override
    public int getCount(){
        return mTitles.size();
    }

    @Override
    public CharSequence getPageTitle(int position) {
        return mTitles.get(position);
    }
}

还是比较简单的,可能最后一个方法或许有些陌生,用过TabLayout的朋友应该懂,就是设置Tab的标题,因为我们的ViewPager是要与TabLayout进行关联的。

编写网络工具类
好了重头戏来了,也是本项目唯一的特色,RxJava+Retrofit。
记得别忘了引入这些框架

    compile 'io.reactivex:rxjava:1.0.14'
    compile 'io.reactivex:rxandroid:1.0.1'
    compile 'com.squareup.retrofit2:retrofit:2.1.0'
    compile 'com.squareup.retrofit2:converter-gson:2.1.0'
    compile 'com.google.code.gson:gson:2.6.2'
    compile 'com.squareup.retrofit2:adapter-rxjava:2.0.0'

这里我不做过多关于RxJava和Retrofit的描述,因为相关资料网上一堆堆。RxJava说到底就是异步,这是它整个流程非常简洁明了,而且方便线程切换。Retrofit是讲OKHttp更好的封装下,简化我们的网络请求。
首先编写Retrofit接口

public interface NewService {
    String BASE_URL="http://v.juhe.cn/";

    @GET("toutiao/index?")
    Observable<News> getNews(@QueryMap Map<String,String> map);
}

好像News 少了个s

接下来编写我们的请求工具类

 private static final int DEFAULT_TIMEOUT = 5;
    private Retrofit retrofit;
    private NewService newService;

    private RetrofitUtil(){
        //手动创建一个OkHttpClient并设置超时时间
        OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder();
        httpClientBuilder.connectTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS);
        Gson gson = new GsonBuilder()
                //配置Gson
                .setDateFormat("yyyy-MM-dd hh:mm:ss")
                .create();
        retrofit=new Retrofit.Builder()
                .baseUrl(NewService.BASE_URL)
                .addConverterFactory(GsonConverterFactory.create(gson))
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .build();
        newService=retrofit.create(NewService.class);

    }

    //在访问HttpMethods时创建单例
    private static class SingletonHolder{
        private static final RetrofitUtil INSTANCE = new RetrofitUtil();
    }

    public static RetrofitUtil getInstance(){
        return SingletonHolder.INSTANCE;
    }

    public void getNews(Subscriber<News> newsSubscriber,int type){
        newService.getNews(getParams(type))
                .subscribeOn(Schedulers.io())
                .unsubscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(newsSubscriber);

    }

    private Map<String,String> getParams(int type){
        Map<String,String> map=new HashMap<>();
        map.put("type",ARRYTYPE[type]);
        map.put("key",KEY_JUHE);
        return map;
    }
}

我们在外面只用调用getNews就行了,传一个Subscriber和类型就行了。逻辑都不是很复杂吧。里面的精髓就是线程切换 .subscribeOn(Schedulers.io()) ,RxJava给我吗提供了五个选择,这里因为我们是请求网络,所以就用io的,最后切换到主线程 .observeOn(AndroidSchedulers.mainThread())

编写Fragment

既然网络工具类写好了,那么就写个fragment来把这些数据展示出来吧!

public class ContentFragment extends Fragment implements Config {

    private int mType;
    private List<News.ResultBean.DataBean> mData;
    private SwipeRefreshLayout mRefreshLayout;
    private RecyclerView mShowNews;
    private NewsAdapter mAdapter;
    private int mSpacingInPixels;//
    private int mCount=0;


    public static Fragment instance(int postion){
        ContentFragment fragment=new ContentFragment();
        Bundle bundle = new Bundle() ;
        bundle.putInt(KEY_POSTION,postion);

        fragment.setArguments(bundle);
        return fragment;
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view=inflater.inflate(R.layout.fragment_content,container,false);
        return view;
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        Bundle bundle = getArguments() ;
        Log.e("dandy","pos "+bundle.getInt(KEY_POSTION));
        initViews(view);
        mType=bundle.getInt(KEY_POSTION);

        getData();
    }

    private void initViews(View view) {
        mRefreshLayout= (SwipeRefreshLayout) view.findViewById(R.id.refresh_layout);
        mShowNews= (RecyclerView) view.findViewById(R.id.news_recyclerview);
        mRefreshLayout.setColorSchemeResources(R.color.colorPrimary,R.color.tab_select_text_color,R.color.refresh_color,R.color.colorAccent);
        mRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                getData();
            }
        });
        mSpacingInPixels= getResources().getDimensionPixelSize(R.dimen.item_space);
        mShowNews.setHasFixedSize(true);
    }

    private void getData(){
        mRefreshLayout.setRefreshing(true);
        Subscriber<News> subscriber=new Subscriber<News>() {
            @Override
            public void onCompleted() {

            }
            //出现异常回调
            @Override
            public void onError(Throwable e) {
                Log.e("smile","获取失败");
            }
            //获取数据成功后回调
            @Override
            public void onNext(News news) {
                Log.e("smile","获取出来的"+news.getResult().getData().size());
                //mData=news.getResult().getData();
                setData(news);
            }
        };
        RetrofitUtil.getInstance().getNews(subscriber,mType);
    }

    private void setData(News data){

        mData=data.getResult().getData();
        mRefreshLayout.setRefreshing(false);
        mAdapter=new NewsAdapter(getContext(),data);
        mShowNews.setLayoutManager(new LinearLayoutManager(getContext()));
        //避免重复添加间距
        if (mCount==0){
            mShowNews.addItemDecoration(new SpacesItemDecoration(mSpacingInPixels));
        }

        mShowNews.setAdapter(mAdapter);
       // mAdapter.setOnScrollListener(mShowNews);
        mAdapter.setOnItemClickListener(new NewsAdapter.OnItemClickListener() {

            @Override
            public void onItemClick(View view, int position) {
                startDetailActivity(view,mData.get(position));
            }

            @Override
            public void onItemLongClick(View view, int position) {

            }
        }) ;
        mCount++;

    }

    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
    private void startDetailActivity(View view, News.ResultBean.DataBean bean){
        Intent intent=new Intent(getActivity(), NewsDetailActivity.class);
        Bundle bundle=new Bundle();
        bundle.putString(KEY_IMG_URL,bean.getThumbnail_pic_s());
        bundle.putString(KEY_CONTENT_URL,bean.getUrl());
        bundle.putString(KEY_TYPE,ARRYTITLES[mType]);
        intent.putExtras(bundle);
        ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation(getActivity(),view.findViewById(R.id.item_news_img),"photos");
        getContext().startActivity( intent, options.toBundle());
    }
}

其实仔细一看也不是很复杂,就是调用我们开始写的getNews而已,请求成功后会回调onNext方法。对了这里有个转场动画,就是最后一个方法,这里的动画效果是共享元素,所以指定你要共享的元素就行,然后设置下它的 android:transitionName=”photos”。

编写RecyclerView适配器
再见ListView,你好RecyclerView
RecyclerView的优点就不多说了,就是自由,任性
适配器,我就不贴代码了,累,而且没什么特色。

编写详情页面
这里主要都是用了Design里面的一些控件

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">
    <android.support.design.widget.AppBarLayout
        android:id="@+id/app_bar_layout"
        android:layout_width="match_parent"
        android:layout_height="250dp">
        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/toolbar_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:minHeight="?attr/actionBarSize"
            android:fitsSystemWindows="true"
            app:contentScrim="?attr/colorPrimary"
            app:statusBarScrim="?attr/colorAccent"

            app:collapsedTitleGravity="left"
            app:expandedTitleGravity="center_horizontal|bottom"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">
            <ImageView
                android:id="@+id/news_img"
                android:src="@mipmap/ic_launcher"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:scaleType="centerCrop"
                app:layout_collapseMode="parallax"
                android:transitionName="photos"
                app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
                app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
                />
            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                app:layout_collapseMode="pin"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:navigationIcon="?attr/homeAsUpIndicator"
                app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
                app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
                style="@style/ToolbarTheme"
                android:titleTextColor="@color/white"

               />
        </android.support.design.widget.CollapsingToolbarLayout>
    </android.support.design.widget.AppBarLayout>

    <com.dandy.smilenews.ui.MyNestedScrollView
        android:id="@+id/news_content"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">
        <WebView
            android:id="@+id/news_web"
            android:layout_width="match_parent"
            android:layout_height="match_parent"></WebView>
    </com.dandy.smilenews.ui.MyNestedScrollView>
</android.support.design.widget.CoordinatorLayout>

折叠式效果就是通过编写xml就能实现了,还是简单的描述下那些属性的含义
CollapsingToolbarLayout

//折叠后的背景色  -> setContentScrim(Drawable)
  app:contentScrim="?attr/colorPrimary"   
  // 必须设置透明状态栏才有效  -> setStatusBarScrim(Drawable)     
  app:statusBarScrim="?attr/colorAccent"    
  // 标题  
  app:title="title"
  // 折叠后的标题位置
  app:collapsedTitleGravity="right"
  // 打开时的标题位置
  app:expandedTitleGravity="center_horizontal|bottom"

折叠效果

app:layout_collapseMode      
  有两个可选:
       parallax ——  视差模式,就是上面的图片的变化效果
       pin     —— 固定模式,在折叠的时候最后固定在顶端

  // 视差效果
  app:layout_collapseParallaxMultiplier   
  范围[0.0,1.0],值越大视差越大

下面的MyNestedScrollView是我自定义的一个view,继承的NestedScrollView,主要是为了解决时间冲突导致滑动卡顿,就是不让拦截子view的触摸事件。

class MyNestedScrollView extends NestedScrollView {
    private GestureDetector mGestureDetector;
    View.OnTouchListener mGestureListener;
    public MyNestedScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

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

    public MyNestedScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mGestureDetector = new GestureDetector(context, new YScrollDetector());
        setFadingEdgeLength(0);
    }

    class YScrollDetector extends GestureDetector.SimpleOnGestureListener {
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            if (Math.abs(distanceY) > Math.abs(distanceX)) {
                return true;
            }
            return false;
        }
    }

}

到这里差不多也要完工了,就可以看到开头的效果了,貌似第一次写这么长的博客,,,
最后附上源码 传送门
喜欢就给个星星吧!

作者:qq272708698 发表于2016/11/4 17:42:55 原文链接
阅读:300 评论:0 查看评论

UIButton实现上图下字,左图右字等组合形式以及sizeToFit的简单理解

$
0
0

    UI需求中很会遇到很多文字和图片混排的效果,暴力做法就是图片用Image,文字用Label分开来实现,然后组合,

是我们有UIButton,可以根据他的imageEdgeInsets和titleEdgeInsets来实现需要的效果




我们默认情况下给UIButton设置image和titile之后是这样的


左边图片和右边文字,默认居中,而且紧靠着


那么UIButton暴露了两个属性来修改相对位置,需要注意的是

This property is used only for positioning the image during layout. The button does not use this property to determine intrinsicContentSize and sizeThatFits:

这句话的意思是这个属性只是修改其相对位置,不会通过这个属性来改变UIButton的内部大小的


下图是更改后的两个效果

                     

NSLog(@"wowowowoowowowowowowow%@",NSStringFromCGSize(self.button.intrinsicContentSize));
    2016-11-04 16:57:20.204 UIButtonEdge[6173:359806] wowowowoowowowowowowow{66, 21}

可以看出打印结果,根本影响不了其内在本质的大小,也就是UIButton就这么大,无论你偏移去哪了,那么这个时候你点击了“文字”是不会触发事件的,你也可以看出,他的大小根本没有改变


这样不是我们要的最终效果啊,如果大小不进行相应的扩充,那么不是白搞了,就好比外表变了,本质还是没变,一样不适用,看下面这句话

But there is a property that can help, and that's contentEdgeInsetsThe docs for that say, in part:

The button uses this property to determine intrinsicContentSize and sizeThatFits:.

这句话就是让我们根据其他属性进一步进行content的扩充


最终我们要的效果就是

2016-11-04 17:02:30.546 UIButtonEdge[6198:366700] wowowowoowowowowowowow{166, 21}

打印结果和效果都出来了,这样UI变了,UIButton的content也跟着变大了,点击事件正常执行,就能满足我们的需求了



逻辑大概是这么个逻辑,我们分解下,那么首先你得明白Image和titile的相对位置

跟tableView的contentInset是类似的,
如果只有title,那它上下左右都是相对于button的,image也是一样;
如果同时有image和label,那这时候image的上左下是相对于button,右边是相对于label的;title的上右下是相对于button,左边是相对于image的

1.图片在左边,文字在右边(默认的)


// 左图右字
    CGFloat gap = 10.f;
    self.button.imageEdgeInsets = UIEdgeInsetsMake(0, -gap / 2, 0, gap / 2);
    self.button.titleEdgeInsets = UIEdgeInsetsMake(0, gap / 2, 0 , - gap / 2);
    self.button.contentEdgeInsets = UIEdgeInsetsMake(0, gap / 2, 0, gap / 2);


2.图片在右边,文字在左边


// 右图左字
    CGFloat gap = 10.f;
    self.button2.imageEdgeInsets = UIEdgeInsetsMake(0,labelWidth + gap / 2 , 0, -labelWidth - gap / 2);
    self.button2.titleEdgeInsets = UIEdgeInsetsMake(0, -imageWidth - gap / 2, 0, imageWidth+gap / 2);
    self.button2.contentEdgeInsets = UIEdgeInsetsMake(0, gap / 2, 0, gap / 2);


3.图片在上面,文字在下面


 // 上图下字
    // 让UIButton能保证边缘自适应 居中的时候需要
    // 当上下排布的时候,要根据edge来填充content大小
    CGFloat maxWidth = MAX(imageWidth,labelWidth); // 上下排布宽度肯定变小 获取最大宽度的那个
    CGFloat changeWidth = imageWidth + labelWidth - maxWidth; // 横向缩小的总宽度
    CGFloat maxHeight = MAX(imageHeight,labelHeight); // 获取最大高度那个 (就是水平默认排布的时候的原始高度)
    CGFloat changeHeight = imageHeight + labelHeight + gap - maxHeight; // 总高度减去原始高度就是纵向宽大宗高度
    self.button4.imageEdgeInsets = UIEdgeInsetsMake(-imageOffSetY, imageOffSetX, imageOffSetY, -imageOffSetX);
    self.button4.titleEdgeInsets = UIEdgeInsetsMake(labelOffSetY, -labelOffSetX, -labelOffSetY, labelOffSetX);
    self.button4.contentEdgeInsets = UIEdgeInsetsMake(changeHeight - labelOffSetY, - changeWidth / 2, labelOffSetY, -changeWidth / 2);


4.文字在上面,图片在下面


// 上字下图
    // 让UIButton能保证边缘自适应 居中的时候需要
    // 当上下排布的时候,要根据edge来填充content大小
    CGFloat maxWidth = MAX(imageWidth,labelWidth); // 上下排布宽度肯定变小 获取最大宽度的那个
    CGFloat changeWidth = imageWidth + labelWidth - maxWidth; // 横向缩小的总宽度
    CGFloat maxHeight = MAX(imageHeight,labelHeight); // 获取最大高度那个 (就是水平默认排布的时候的原始高度)
    CGFloat changeHeight = imageHeight + labelHeight + gap - maxHeight; // 总高度减去原始高度就是纵向宽大宗高度
    self.button6.imageEdgeInsets = UIEdgeInsetsMake(imageOffSetY, imageOffSetX, -imageOffSetY, -imageOffSetX);
    self.button6.titleEdgeInsets = UIEdgeInsetsMake(-labelOffSetY, -labelOffSetX, labelOffSetY, labelOffSetX);
    self.button6.contentEdgeInsets = UIEdgeInsetsMake(labelOffSetY, -changeWidth / 2, changeHeight - labelOffSetY, -changeWidth / 2);


注:这上面四种ContentEdgeInsets都是扩充或者缩小移动后的Button大小的,这样保证了效果也保证了点击事

件,感觉没必要再单独封装成Category了,直接调用也很简单,需要的自行可以参考下




这里涉及到contentInsets的变化,UIButton的这种情况需要手动更改,我们另一种实现(UILabel)


sizeThatFits 和 sizeToFit


- (CGSize)sizeThatFits:(CGSize)size;     // return 'best' size to fit given size. does not actually resize view. Default is return existing view size

- (void)sizeToFit;                       // calls sizeThatFits: with current view bounds and changes bounds size.


注释的个人理解:

当我们调用sizeToFit的时候,会call sizeThatFits来计算出best size来适应内在控件的最小尺寸,而且该方法还会change bounds size,就是直接更改调用者的frame

而直接调用sizeThatFits只是计算出最适合内部控件的size返回,仅此而已,根本不会更改调用者的frame


UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(50, 64, 0, 0)];
    label.backgroundColor = [UIColor redColor];
    label.font = [UIFont systemFontOfSize:18];
    label.text = @"呵呵呵呵";
    CGSize sizeThatFit = [label sizeThatFits:CGSizeMake(1000, 1000)];
    NSLog(@"sizeThatFit------> width = %lf,height = %lf",sizeThatFit.width,sizeThatFit.height);
    // sizeThatFit------> width = 73.500000,height = 21.500000
    
    NSLog(@"sizeThatFit之后的labelFrame ------> width = %lf,height = %lf",label.frame.size.width,label.frame.size.height);
    // sizeThatFit之后的labelFrame ------> width = 0.000000,height = 0.000000  没有变化 只是返回值而已,不改变调用者frame
    [label sizeToFit];
    NSLog(@"sizeTofit之后的labelFrame ——----> width = %lf.height = %lf",label.frame.size.width,label.frame.size.height);
    
    //sizeTofit之后的labelFrame ——----> width = 73.500000.height = 21.500000 第一计算frame,第二根据sizeThatFit计算返回的frame改变调用者frame





作者:Deft_MKJing 发表于2016/11/4 18:11:24 原文链接
阅读:47 评论:0 查看评论

Qt之图形视图(QGraphicsTextItem - 文本项)

$
0
0

简述

QGraphicsTextItem 类提供了一个文本项,可以添加到 QGraphicsScene 显示格式化的文本。

如果只需要在项目中显示纯文本,可以考虑使用 QGraphicsSimpleTextItem。

详细说明

QGraphicsTextItem 使用文本的格式化大小和相关联的字体提供了一个合理的实现 boundingRect()、shape() 和 contains()。可以通过调用 setFont() 设置字体。

项目的首选文本宽度可以使用 setTextWidth() 设置,并使用 textWidth() 获取。

注意:为了在中心对齐 HTML 文本,必须设置项目的文本宽度。否则,可以在设置项目的文本后调用 adjustSize()。

注意: QGraphicsTextItem 默认接受 hover 事件,可以使用 setAcceptHoverEvents() 更改此值。

纯文本

这里写图片描述

首先,通过调用 QGraphicsTextItem 的构造函数创建一个文本项(也可使用 QGraphicsScene 的 addText() 函数创建,并将文本项添加至场景中)。要设置项目的文本,传递 QString 至 QGraphicsTextItem 的构造函数,或调用 setPlainText()。

setDefaultTextColor() 如果要设置文本项的字体,通过调用 setFont() 设置。

// 定义一个文本项
QGraphicsTextItem *pItem = new QGraphicsTextItem();
pItem->setPlainText(QString::fromLocal8Bit("一去丶二三里"));
pItem->setDefaultTextColor(QColor(0, 160, 230));  // 文本色

// 字体
QFont font = pItem->font();
font.setPixelSize(20);  // 像素大小
font.setItalic(true);  // 斜体
font.setUnderline(true);  // 下划线
pItem->setFont(font);

// 将文本项添加至场景中
QGraphicsScene *pScene = new QGraphicsScene();
pScene->addItem(pItem);

// 为视图设置场景
QGraphicsView *pView = new QGraphicsView();
pView->setScene(pScene);
pView->setStyleSheet("border:none; background:transparent;");

pView->show();

富文本

假设文本是 HTML 格式,显示不同颜色的文本以及图片。

这里写图片描述

QString strHTML = QString("<html> \
                               <head> \
                               <style> \
                               font{color:white;} #f{font-size:18px; color: #00A0E6;} \
                               </style> \
                               </head> \
                               <body>\
                               <font>%1</font><font id=\"f\">%2</font> \
                               <br/><br/> \
                               <img src=\":/Images/logo\" width=\"100\" height=\"100\"> \
                               </body> \
                               </html>").arg("I am a ").arg("Qter");
pItem->setHtml(strHTML);

超链接

我们需要简单使用标签 <a></a>写一段简单的 HTML 超链接代码

方式一:

比较简单,直接调用 setOpenExternalLinks(true) 即可。

pItem->setHtml(QString("<a href = \"%1\">%2</a>").arg("http://blog.csdn.net/liang19890820").arg(QStringLiteral("一去丶二三里")));
pItem->setOpenExternalLinks(true);
pItem->setTextInteractionFlags(Qt::TextBrowserInteraction);

方式二:

连接 linkActivated() 信号,然后调用 QDesktopServices 的 openUrl() 打开链接:

pItem->setHtml(QString("<a href = \"%1\">%2</a>").arg("http://blog.csdn.net/liang19890820").arg(QStringLiteral("一去丶二三里")));
pItem->setTextInteractionFlags(Qt::TextBrowserInteraction);
connect(pItem, &QGraphicsTextItem::linkActivated, [=](QString link) {
            QDesktopServices::openUrl(QUrl(link));
});

注意:这两种方式都需要调用 setTextInteractionFlags(Qt::TextBrowserInteraction),指定交互方式为文本浏览器交互。

编辑

通过使用 setTextInteractionFlags() 设置 Qt::TextEditorInteraction 标志来使项目可编辑。

这里写图片描述

pItem->setTextInteractionFlags(Qt::TextEditorInteraction);  // 可编辑
connect(pItem->document(), &QTextDocument::contentsChanged, [=]() {
    qDebug() << pItem->toPlainText();
});

输出如下:

“Q”
“Qt”
“Qte”
“Qter”

作者:u011012932 发表于2016/11/4 18:12:00 原文链接
阅读:73 评论:0 查看评论

iOS应用程序执行的生命周期

$
0
0

main函数探究

在iOS项目中有一个main.m的文件,它是程序的入口类,代码如下:

#import <UIKit/UIKit.h>

#import "AppDelegate.h"

int main(int argc, char * argv[])
{
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

参数argc与argv与标准的C语言main函数一致,argc(arguments count)代表参数个数,argv(arguments value)是一个字符型指针数组。

默认的argc为1,即包含一个参数,这个参数是程序完整路径;我们也可以添加一些额外的参数来测试一下:

P.s. 通过Xcode菜单栏的Product—Scheme—Edit Scheme打开编辑窗体。

int main(int argc, char * argv[])
{
    for(int i=0; i<argc; i++)
    {
        NSLog(@"arg %i: %s",i,argv[i]);
    }
    
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

输出结果:

arg 0: /var/mobile/Applications/C12B5C94-CAD7-489D-AB8E-13280E7CD886/Sample0616.app/Sample0616
arg 1: god
arg 2: bless
arg 3: me
arg 4: New
arg 5: York

可以看出参数是以空格来分割的。第一个参数为真机上程序的完整路径。

main函数中调用了一个UIApplicationMain的函数,先来看看这个函数的原型:

int UIApplicationMain (
   int argc,
   char *argv[],
   NSString *principalClassName,
   NSString *delegateClassName
);

前面两个参数前面已经分析过了,重点是最后两个参数principalClassName和delegateClassName

principalClassName是应用程序类的名字,该类必须继承自UIApplication类;如果传递nil,UIKit就缺省使用UIApplication类;每一个iOS应用程序都包含一个UIApplication对象,iOS系统通过该UIApplication对象监控应用程序生命周期全过程。

delegateClassName是应用程序委托类的名字,默认为AppDelegate类,该委托类处理应用程序的生命周期事件和系统事件。

通过调用main函数创建了一个UIApplication对象,UIApplication对象负责监听应用程序的生命周期时间,并将生命周期事件交由AppDelegate代理对象处理。

iOS程序运行的生命周期

app的状态有五种:

not running — 没有启动app
inactive — app运行在前台,但是没有处理任何事件
active — app运行在前台,并且在处理事件
background — app运行在后台,还在内存中,并且执行代码
suspend — app还在内存中,但是不运行任何代码,如果内存不足,会自动kill掉

app各种状态之间的改变图示:

启动阶段

当一个app开始运行前存在一个启动阶段(Launch Time),这个阶段包含两个系统方法:

- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    return YES;
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // Override point for customization after application launch.
    return YES;
}

这两个方法的作用几乎完全一样,只是执行顺序有先后。AppDelegate类默认出现的是application: didFinishLaunchingWithOptions:,一般情况下只需要处理这个方法就可以了。

didFinishLaunchingWithOptions函数的参数launchOptions是一个NSDictionary类型的对象,存储的是程序启动的原因。

  • 用户(点击icon)直接启动程序,launchOptions内无数据;
  • 其它程序通过openURL:方式启动,则可以通过键UIApplicationLaunchOptionsURLKey来获取传递过来的url
    NSURL *url = (NSURL *)[launchOptions valueForKey:UIApplicationLaunchOptionsURLKey];

     

  • 由本地通知启动,则可以通过键UIApplicationLaunchOptionsLocalNotificationKey来获取本地通知对象(UILocalNotification)
  • 由远程通知启动,则可以通过键UIApplicationLaunchOptionsRemoteNotificationKey来获取远程通知信息(NSDictionary)

程序launchOptions中的可能键值可以参考UIApplication Class Reference的”Launch Options Keys”。

didFinishLaunchingWithOptions函数的返回值是一个BOOL类型,将决定是否处理URL资源,如果返回YES,则会由application:openURL:sourceApplication:annotation:方法处理URL。如果应用程序有一个远程通知启动,返回值会被忽略

P.s. 如果同时出现了application: willFinishLaunchingWithOptions:和application: didFinishLaunchingWithOptions:,那么这两个方法都要返回YES,才会处理URL资源。

启动阶段结束后进入运行阶段,程序可能会进入前台(foreground)运行,也可能进入后台(background)运行,下面是两种运行方式的图示:

加载到前台:

foreground      

 

加载到后台:

background

 

前台运行比较常见,后台运行出现在较后的iOS版本,这里不做很深的研究,只提供一个后台运行的配置及测试方法:

step 1:在application: didFinishLaunchingWithOptions:里设置数据获取时间间隔,并监控当前的程序运行状态

- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    [application setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];
    
    NSLog(@"current state:%d.(%d,%d,%d)",application.applicationState,
                                         UIApplicationStateActive,
                                         UIApplicationStateInactive,
                                         UIApplicationStateBackground);
    return YES;
}

step 2:在Capabilities标签页中启用”Background Modes”,并勾选”Background fetch”

step 3:在AppDelegate里实现-application:performFetchWithCompletionHandler:,系统将会在执行fetch的时候调用这个方法。

- (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
    //do something...
}

step 4:使用Scheme更改Xcode运行程序的方式。Product—Scheme—Manage Schemes,然后新建或编辑一个scheme项:

step 5:启动调试。

运行阶段(只考虑直接Launch到前台的情况)

启动阶段执行完后,程序的状态为”inactive”,进入到运行阶段执行下面方法会变成”active”:

- (void)applicationDidBecomeActive:(UIApplication *)application
{
    // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}

官方留下的注释翻译成中文是:“当应用程序处在不活动状态时,重新启动一个被暂停(或还未启动)的任务。如果程序之前就在后台,根据情况可以刷新用户界面。”这个方法触发很频繁,在程序第一次启动或程序恢复前台状态时,都会先执行这个方法。

中断情况

接下来考虑中断情况,有以下几种常见情况:

1. 按下home键或双击home键点击任务栏icon运行其它app
2. 有来电通知
3. 有短信或其它app的顶部通知或推送消息。

下面详细分析下每个动作,打印出触发的方法及状态:

case 1:当按下home键
function:[applicationWillResignActive], state:[Active]
function:[applicationDidEnterBackground], state:[Background]

case 2:当双击home键,然后选择返回当前app:
function:[applicationWillResignActive], state:[Active]
function:[applicationDidBecomeActive], state:[Active]

case 3:当双击home键,然后选择其它app:
function:[applicationWillResignActive], state:[Active]
function:[applicationDidEnterBackground], state:[Background]

case 4:当有来电,拒绝接听:
function:[applicationWillResignActive], state:[Active]
function:[applicationDidBecomeActive], state:[Active]

case 5:当有来电,接听后并挂断:
function:[applicationWillResignActive], state:[Active]
function:[applicationDidEnterBackground], state:[Background]
function:[applicationWillEnterForeground], state:[Background]
function:[applicationDidBecomeActive], state:[Active]

case 6:当有短信或顶部的推送消息,点击查看:
function:[applicationWillResignActive], state:[Active]
function:[applicationDidEnterBackground], state:[Background]

从上面的各种情况可以看出来,只要存在中断操作,第一个执行的方法都是:

- (void)applicationWillResignActive:(UIApplication *)application
{
    // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
    // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
}

通常情况下,我们应该在applicationWillResignActive:方法中执行下列操作:

  • 停止timer和其它周期性任务
  • 停止任何正在运行的请求
  • 暂停视频的播放
  • 如果是游戏那就暂停它
  • 减少OpenGL ES的频率
  • 挂起任何分发的队列和不重要的操作队列(你可以继续处理网络请求或其它时间敏感的后台任务)

当程序回到active状态,应该使用applicationDidBecomeActive:方法将上面暂停的操作重新开发或恢复运行,比如重新开始timer,继续分发队列,提高OpenGL ES的频率。对于游戏可能会回到一个暂停状态,有用户点击再开始。

如果程序中断后进入了后台,会调用方法:

- (void)applicationDidEnterBackground:(UIApplication *)application
{
    // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 
    // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}

官方留下的注释翻译成中文是:“使用这个方法去释放共享资源,存储用户数据,取消定时器和存储足够的应用程序状态信息以便在终止前恢复到它当前的状态。如果你的应用程序支持后台操作,在用户离开程序后它将代替方法applicationWillTerminate:被调用”。

中断后返回的情况

中断情况发生后,再返回当前app,会调用方法:

- (void)applicationWillEnterForeground:(UIApplication *)application
{
    // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
}

这里有个疑问,当app从后台转回前台时,applicationWillEnterForeground:和applicationDidBecomeActive:都会被调用,两者有什么区别呢?我的理解是,applicationWillEnterForeground:只有当程序从后台返回到前台这一种情况下才会被调用;而applicationDidBecomeActive:除了从后台返回前台时被调用,还会在程序运行在前台时也被调用(例如之前提到的收到来电提醒后取消接听,双击home键后依旧返回当前app等操作)。所以applicationWillEnterForeground:适合处理那种加载前只需要执行一次的初始化。

终止阶段

当程序终止时将调用方法:

- (void)applicationWillTerminate:(UIApplication *)application
{
    // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}

这个方法通常是用来保存数据和一些退出前的清理工作,需要要设置UIApplicationExitsOnSuspend的键值。

作者:LOSER_LOSER1 发表于2016/11/4 18:17:57 原文链接
阅读:47 评论:0 查看评论

java打包下载

$
0
0

近段时间在做一个项目,其中有一个功能是需要将用户填的信息以及上传的附件等进行打包下载。于是乎,网上各种找资料,现将成果分享如下:

	/**
	 * 打包下载文件 信息
	 * @param mav
	 * @param request
	 * @param response
	 * @param attachmentId
	 */
	@Transactional(propagation=Propagation.REQUIRED, readOnly = true)
	public void zipDownLoadAttachment(ModelAndView mav, HttpServletRequest request,Map<String, Object> result,HttpServletResponse response,String id){
		
		List<SupplierSubAttachmentEntity> listAttachment=supplierSubAttachmentDao.findByChiefId(Integer.valueOf(id));
		
		List<SupplierSubContactEntity> listContact=supplierSubContactDao.findByChiefId(Integer.valueOf(id));
		
		List<SupplierSubTrackEntity> listTrack=supplierSubTrackDao.findByChiefId(Integer.valueOf(id));
		
		SupplierChiefEntity chief=supplierChiefDao.findById(Integer.valueOf(id));
		
		//导出基础数据
		this.outPutExcel(request,chief,listContact,listTrack);
		
		String filePath=BusSystemConfig.downLoadPath;
		
		String path=BusSystemConfig.contentFileUpLocalPath;
//	    //生成的ZIP文件名为Demo.zip    
        String tmpFileName = chief.getName()+".zip";    
        byte[] buffer = new byte[1024];    
        String strZipPath = filePath +tmpFileName;    
        try {    
            ZipOutputStream out = new ZipOutputStream(new FileOutputStream(    
                    strZipPath)); 
            String filename="";
            // 需要同时下载的多个文件
            File[] file1=new File[listAttachment.size()+1];
            for(int i=0;i<listAttachment.size();i++){
            	file1[i]=new File(path + "/"+listAttachment.get(i).getAttachmentUrl());
            }
           file1[listAttachment.size()]=new File(filePath+chief.getName()+".xls");
            for (int i = 0; i < file1.length; i++) {    
            	if(!file1[i].exists()){
            		continue;
            	}
            	if(i<listAttachment.size()){
            		filename=listAttachment.get(i).getName();
            	}else{
            		filename=file1[i].getName();
            	}
                FileInputStream fis = new FileInputStream(file1[i]);    
                out.putNextEntry(new ZipEntry(filename));    
                //设置压缩文件内的字符编码,不然会变成乱码    
                //out.setEncoding("GBK");    
                int len;    
                // 读入需要下载的文件的内容,打包到zip文件    
                while ((len = fis.read(buffer)) > 0) {    
                    out.write(buffer, 0, len);    
                }    
                out.closeEntry(); 
                fis.close(); 
            }
            out.flush();
            out.close();
            this.downLoad(mav,request,response,tmpFileName,filePath);  
        } catch (Exception e) {    
        	logger.error("文件下载出错", e);   
        }finally{
        	 try {
                 File f = new File(filePath+chief.getName()+".xls");
                 f.delete();
             } catch (Exception e) {
                 e.printStackTrace();
             }
        }
	}




//这段代码是将压缩包下载到本地

public void downLoad(ModelAndView mav,HttpServletRequest request,HttpServletResponse response,String str,String filePath){
		String path=filePath+str;
		try { 
            File file = new File(path);   
            if(!file.exists()){
            	 file.createNewFile();
            }
            
			 OutputStream os = response.getOutputStream();
			 response.reset();// 清空输出流   
			 response.setContentType(request.getSession().getServletContext().getMimeType(str));
		    // 先去掉文件名称中的空格,然后转换编码格式为utf-8,保证不出现乱码,这个文件名称用于浏览器的下载框中自动显示的文件名
		    response.setHeader("Content-disposition", "attachment;filename=" + new String(str.getBytes("UTF-8"),"ISO8859-1"));
		    
		    InputStream fis = new FileInputStream(path);
		    byte[] buffer = new byte[fis.available()];
		   
	        fis.read(buffer);
		    fis.close();
		    os.write(buffer);// 输出文件
		    os.flush();
		    os.close();
        } catch (IOException e) {    
            logger.error("文件下载出错", e);
        } finally{
            try {
                File f = new File(path);
                f.delete();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
	}

//这段代码是将子数据导出到excel表中

public void outPutExcel(HttpServletRequest request,SupplierChiefEntity chiefEntity, List<SupplierSubContactEntity> listContact,List<SupplierSubTrackEntity> listTrack) {
		try {

			String fileName = chiefEntity.getName()+".xls";
			String filePath=BusSystemConfig.downLoadPath;
			WritableWorkbook workbook = Workbook.createWorkbook(new File(filePath+fileName));
			if (workbook != null) {

				
				WritableFont wf = new WritableFont(WritableFont.ARIAL, 10, WritableFont.BOLD, false, UnderlineStyle.NO_UNDERLINE, jxl.format.Colour.BLACK); // 定义格式
				WritableCellFormat wcf = new WritableCellFormat(wf); // 单元格定义
				WritableCellFormat wcfmt = new WritableCellFormat(); // 单元格定义
				// 设置标题 sheet.addCell(new jxl.write.Label(列(从0开始), 行(从0开始),
				// 内容.));
				try {
					String[] baseTitles=baseTitle();
					
					wcf.setAlignment(jxl.format.Alignment.CENTRE); // 设置对齐方式
					wcf.setBorder(Border.ALL, BorderLineStyle.THIN, jxl.format.Colour.BLACK);
					wcfmt.setBorder(Border.ALL, BorderLineStyle.THIN, jxl.format.Colour.BLACK);
					
					WritableSheet sheet = workbook.createSheet("基础信息", 0);
					sheet.addCell(new Label(0, 0, "基础信息", wcf));
					// 合并单元格
					sheet.mergeCells(0, 0, baseTitles.length-1, 0);
					// 设置行高度
					sheet.setRowView(0, 500);
					for(int i=0;i<baseTitles.length;i++){
						// 设置单元格的宽度
						sheet.setColumnView(i, 25);
						
						sheet.addCell(new Label(i, 1, baseTitles[i], wcf));
					}
					
					sheet.setRowView(2, 400);
					sheet.addCell(new Label(0, 2 , chiefEntity.getName(), wcfmt));
					sheet.addCell(new Label(1, 2 , chiefEntity.getChiefBusinissShow(), wcfmt));
					sheet.addCell(new Label(2, 2 , chiefEntity.getCorporate(), wcfmt));
					sheet.addCell(new Label(3, 2 , chiefEntity.getMobile(), wcfmt));
					sheet.addCell(new Label(4, 2 , chiefEntity.getEmail(), wcfmt));
					sheet.addCell(new Label(5, 2 , chiefEntity.getSetupDate(), wcfmt));
					sheet.addCell(new Label(6, 2 , chiefEntity.getRegistFund(), wcfmt));
					sheet.addCell(new Label(7, 2 , chiefEntity.getTinNumber(), wcfmt));
					sheet.addCell(new Label(8, 2 , chiefEntity.getFixPhone(), wcfmt));
					sheet.addCell(new Label(9, 2 , chiefEntity.getFax(), wcfmt));
					sheet.addCell(new Label(10, 2 , chiefEntity.getAddress(), wcfmt));
					sheet.addCell(new Label(11, 2 , chiefEntity.getCompanyUrl(), wcfmt));
					
					//==========================导出业务联系人信息=======================
					String[] contactTitles=contactTitle();
					WritableSheet sheet1 = workbook.createSheet("业务联系人信息", 1);

						sheet1.addCell(new Label(0, 0, "业务联系人信息", wcf));
						
						// 合并单元格
						sheet1.mergeCells(0, 0,contactTitles.length-1, 0);

						
						// 设置行高度
						sheet1.setRowView(0, 400);
						
						
						for(int i=0;i<contactTitles.length;i++){
							// 设置单元格的宽度
							sheet1.setColumnView(i, 22);
							
							sheet1.addCell(new Label(i, 1, contactTitles[i], wcf));
						}
						
						SupplierSubContactEntity contactEntity=null;
						for(int i=0;i<listContact.size();i++){
							contactEntity=listContact.get(i);
							sheet1.setRowView((i+2), 400);
							sheet1.addCell(new Label(0, (i+2) , contactEntity.getName(), wcfmt));
							sheet1.addCell(new Label(1, (i+2) , contactEntity.getSexTypeShow(), wcfmt));
							sheet1.addCell(new Label(2, (i+2) , contactEntity.getDepartShow(), wcfmt));
							sheet1.addCell(new Label(3, (i+2) , contactEntity.getJobShow(), wcfmt));
							sheet1.addCell(new Label(4, (i+2) , contactEntity.getMobile(), wcfmt));
							sheet1.addCell(new Label(5, (i+2) , contactEntity.getEmail(), wcfmt));
						}
						//==========================导出业务联系人END=============================
						String[] trackTitles=trackTitle();
						WritableSheet sheet2 = workbook.createSheet("主要业绩信息", 2);

							sheet2.addCell(new Label(0, 0, "主要业绩信息", wcf));
							
							// 合并单元格
							sheet2.mergeCells(0, 0,trackTitles.length-1, 0);

							
							// 设置行高度
							sheet2.setRowView(0, 400);
							
							
							for(int i=0;i<trackTitles.length;i++){
								// 设置单元格的宽度
								sheet2.setColumnView(i, 22);
								
								sheet2.addCell(new Label(i, 1, trackTitles[i], wcf));
							}
							
							SupplierSubTrackEntity trackEntity=null;
							for(int i=0;i<listTrack.size();i++){
								trackEntity=listTrack.get(i);
								sheet2.setRowView((i+2), 400);
								sheet2.addCell(new Label(0, (i+2) , trackEntity.getEngineerName(), wcfmt));
								sheet2.addCell(new Label(1, (i+2) , trackEntity.getMoney().toString(), wcfmt));
								sheet2.addCell(new Label(2, (i+2) , trackEntity.getEngineerStartDate(), wcfmt));
								sheet2.addCell(new Label(3, (i+2) , trackEntity.getEngineerEndDate(), wcfmt));
								sheet2.addCell(new Label(4, (i+2) , trackEntity.getFirstParty(), wcfmt));
								sheet2.addCell(new Label(5, (i+2) , trackEntity.getRemark(), wcfmt));
							}
						
						//============================导出主要业绩Start===========================

						
						//============================导出主要业绩END=============================

					// 从内存中写入文件中
					workbook.write();
					// 关闭资源,释放内存
					workbook.close();
				} catch (RowsExceededException e) {
					logger.error("sheet不存在", e);
				} catch (WriteException e) {
					logger.error("创建列名出错", e);
				}
			}

		} catch (IOException e) {
			logger.error("文件创建出错", e);
		}
	}
	
	
	public String[] baseTitle(){
		String[] titles=new String[]{"单位名称","主要业务","法人代表","法人手机","法人邮箱","成立时间","注册资金(万元)","税务登记","固定电话","公司传真","公司地址","公司网址"};
		return titles;
	}
	
	public String[] contactTitle(){
		String[] titles=new String[]{"姓名","性别","所在部门","职务","手机号码","电子邮箱"};
		return titles;
	}
	
	public String[] trackTitle(){
		String[] titles=new String[]{"工程名称","合同金额","工程开始时间","工程结束时间","开发商","备注"};
		return titles;
	}

至些,打包下载全部完成。欢迎批评指正。

作者:Sugar_521 发表于2016/11/4 18:35:21 原文链接
阅读:68 评论:0 查看评论

Android NDK——配置NDK及使用Android studio开发Hello JNI并简单打包so库

$
0
0

引言

尽管Android Studio已经越来越流行了,但很多人还是习惯于Eclipse或源码环境下开发JNI应用。笔者是从以前在学校参加谷歌大学学术合作项目的时候接触JNI的,当时是为了模仿底层的方法实现一个功能,使用的是Eclipse,直到最近项目中需要用到SmartLink、AirKiss才又再次接触到JNI,第一次使用Android Studio开发NDK,遇到不少弯路再次总结下,以期能帮助新手快速入门不再迷茫。

一、NDK相关角色概述

1、NDK和SO

这里写图片描述
NDK 全称 Native Development Kit,是Google在Android开发中提供的一套用于快速创建native工程的一个工具。从上图这个Android系统框架来看,我们上层是通过JNI方式来调用NDK层的,使用这个工具可以很方便的编写和调试JNI的代码。因为C语言不跨平台,在Windows系统下使用NDK编译在Linux下能执行的函数库——SO文件,全称Shared Objects,其实质就是一堆c、c++的头文件和实现文件打包成一个库。目前Android系统目前支持以下七种不同的CPU架构,每一种对应着各自的应用程序二进制接口ABI:(Application Binary Interface)定义了二进制文件(尤其是.so文件)如何运行在相应的系统平台上,从使用的指令集,内存对齐到可用的系统函数库。

  • ARMv5——armeabi
  • ARMv7 ——armeabi-v7a
  • ARMv8——arm64- v8a
  • x86——x86
  • MIPS ——mips
  • MIPS64——mips64
  • x86_64——x86_64

以本人ODM经验来说,你应该尽可能的提供专为每个ABI优化过的.so文件,但不应该混合着使用。你应该为每个ABI目录提供对应的.so文件。当一个应用安装在设备上,只有该设备支持的CPU架构对应的.so文件会被安装。在x86设备上,libs/x86目录中如果存在.so文件的 话,会被安装,如果不存在,则会选择armeabi-v7a中的.so文件,如果也不存在,则选择armeabi目录中的.so文件(因为x86设备也支 持armeabi-v7a和armeabi)。ps: Native Libs Monitor 这个应用可以帮助我们理解手机上安装的APK用到了哪些.so文件,以及.so文件来源于哪些函数库或者框架。

2、JNI

这里写图片描述
JNI 全称Java Native Inteface,即Java本地接口,是Java中定义的一种用于连接Java和C/C++接口的一种实现方式。Java语言装载到虚拟机中,不能和硬件交互,不能驱动开发。JNI扩展了Java虚拟机的能力,驱动开发、无线热点共享,底层语言(C、C++)效率高,数学运算、实时渲染的游戏,音视频处理等等,简而言之,就是Java代码调用c、c++代码,JNI模式一共涉及到三个角色:C/C++ 代码本地方法接口类Java层中具体业务类

1、JNI简要流程

这里写图片描述

2、JNI的变量类型与Java类型对比表

Java中变量的类型 JNI对应的类型名 C/C++变量类型
boolean jboolean unsigned char
float jfloat float
double jdouble double
byte jbyte signed char
char jchar unsigned short
short jshort short
int jint/jsize int
long jlong long long
Object jobject void *
String jstring void *

3、JNI的基本语法

  • 命名规则
    JNIExport jstring JNICALL Java_com_example_hellojni_HelloJni_stringFromJNI( JNIEnv* env,jobject thiz )
    这里写图片描述

  • Java端加载so

 System.loadLibrary("helloJni");//加载so文件,不要带上前缀lib和后缀.so
  • Java端的调用jni
((TextView)findViewById(R.id.jni_text)).setText(helloJni());

3、Gradle

Gradle 是一个基于Apache Ant和Apache Maven概念的项目自动化建构工具。它使用一种基于Groovy的特定领域语言(DSL)来声明项目设置。以往Android NDK开发需要在Eclipse或源码环境下,建立并配置Android.mk和Application.mk,且还要通过java命令生成.h头文件,才能编译生成so库。但在Android Studio中这些步骤都不需要,因为Gradle足够强大,只需配置Gradle即可编译生成so库。

三、开发JNI的步骤

JNI代码主要又分为Native代码和Java代码,所以我们得实现Native端和Java端

1、安装NDK配置环境变量和相关插件(NDK、CMake、LLDB)

这里写图片描述

把NDK里build目录添加到path变量 ,比如我的目录是D:\AndroidDevlopment\SDK\ndk-bundle

这里写图片描述

2、新建一个Android标准工程,并在工程设置中配置NDK路径。

这里写图片描述

3、打开app下的 build.gradle,配置NDK和在gradle.properties里配置android.useDeprecatedNdk=true

 ndk{
    moduleName "helloJni"//*生成的so文件名,必填
    abiFilters "armeabi", "armeabi-v7a", "x86" //配置输出的abi体系结构下的so库,
}

配置支持NDK

android.useDeprecatedNdk=true//是灰色的不影响

这里写图片描述
若不配置android.useDeprecatedNdk=true点击脚本同步,之后会报这个错误
这里写图片描述

4、在Actiivty类文件里定义jni本地接口方法

public class MainActivity extends AppCompatActivity {

    static {
        System.loadLibrary("helloJni");//加载so文件,不要带上前缀lib和后缀.so
    }
    public native String helloJni();//定义本地方法接口,这个方法类似虚方法,实现是用c或者c++实现的

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

5、生成对应的C文件默认实现

在我们定义了本地接口方法之后,我们在方法上按alt+Enter,然后生成对应的方法,可是不出意外的话生成的c文件只是有一个头文件的,并没有为我们生成对应的方法框架,

#include <jni.h>

如果你熟悉Jni的语法可以自己去实现,但是太麻烦了,幸好谷歌为我们提供了一个插件gradle-experimental,我们只需要在app下的gradle.build脚本里配置(仅仅在我们生成jni方法框架时添加,当我们全部添加完JNI方法框架之后,必须注释或者删除掉,否则run的时候就绝对报错)

  • gradle-experimental插件
    在2015年5月的Google I/O大会上, Google宣布Android Studio开始支持NDK开发,通过和JetBrains的合作,将Clion整合进了Android Studio 1.3,并免费支持NDC++开发。同年7月,在Android Studio 1.3版本上添加了 gradle-experimental插件,该插件支持NDK开发和调试,且带有代码不全和重构等高级功能。
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:24.2.1'
    compile 'com.android.tools.build:gradle-experimental:0.7.0'//仅仅在我们生成jni方法框架时添加,当我们全部添加完JNI方法框架之后,必须注释或者删除掉,否则run的时候就绝对报错
}

配置成功了之后,再执行下脚本,把原来生成的jni文件夹删除掉,再按万能键,这时候就妥妥滴生成了c文件

#include <jni.h>

JNIEXPORT jstring JNICALL Java_crazymo_train_jnitraining_MainActivity_helloJni(JNIEnv *env,jobject instance){
    // TODO
    return (*env)->NewStringUTF(env, "Hello Jni");
}

6、完成上面5个步骤之后,就可以make或者run了,然后就会生成对应的so文件(..\build\intermediates\ndk\debug\obj\local)

完整的Activity实现如下:

package crazymo.train.jnitraining;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    static {
        System.loadLibrary("helloJni");//加载so文件,不要带上前缀lib和后缀.so
    }
    public native String helloJni();//定义本地方法接口,这个方法类似虚方法,实现是用c或者c++实现的

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ((TextView)findViewById(R.id.jni_text)).setText(helloJni());//调用JNI
    }
}

二、HelloJNI源码

这么简单的也上传源码Hello JNI源码

作者:CrazyMo_ 发表于2016/11/4 19:05:06 原文链接
阅读:46 评论:0 查看评论

Android6.0-新控件(二)

$
0
0

前言:

Android 6.0新控件(一),这里将介绍一下NavigationView,CoordinatorLayout,AppBarLayout,CollapsingToolbarLayout的使用.文章都是自己学习过程中的记录,难免会有失误,还望大家不吝指出,谢谢.这里是Android 6.0新控件(一)FloatingActionButton,TextInputLayout,Snackbar,TabLayout的使用,有需要的可以看下.网址:http://blog.csdn.net/listeners_Gao/article/details/52886162

下面继续NavigationView,CoordinatorLayout,AppBarLayout,CollapsingToolbarLayout控件的使用.

使用:

因为使用的是Support库中提供的新控件,所以当你使用这些控件中的任意一个时,都需要添加依赖.在build.gradle(moudle:app)中添加依赖:compile ‘com.android.support:design:24.2.1’ 版本号可自己匹配.

  • 简介:

    在Material Design中,Navigation drawer导航抽屉,被设计用于应用导航,提供了一种通用导航方式,体现了设计的一致性.而NavigationView的出现就是为了配合DrawerLayout的使用,作为Drawer的一部分,即导航菜单.NavigationView是一个导航菜单框架,使用menu资源填充数据是我们可以更简单高效的实现导航菜单.它提供了一些默认样式,选中项高亮,分组单选,分组子标题,以及可选的header.好了,说了这么说,不如几行代码来的直接…

  • 具体使用:

    • 布局文件:

      <?xml version="1.0" encoding="utf-8"?>
      <android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:app="http://schemas.android.com/apk/res-auto"
          xmlns:tools="http://schemas.android.com/tools"
          android:id="@+id/drawerLayout"
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          android:fitsSystemWindows="true"
          tools:context="com.listenergao.mytest.activity.NavigationViewActivity">
          <!--content-->
          <LinearLayout
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical">
      
              <include layout="@layout/toolbar_layout" />
      
              <FrameLayout
                  android:id="@+id/fl_content"
                  android:layout_width="match_parent"
                  android:layout_height="0dp"
                  android:layout_weight="1">
              </FrameLayout>
          </LinearLayout>
      
          <android.support.design.widget.NavigationView
              android:id="@+id/navigation"
              android:layout_width="wrap_content"
              android:layout_height="match_parent"
              android:layout_gravity="start"
              app:headerLayout="@layout/drawer_header"
              app:menu="@menu/drawer_menu">
      
          </android.support.design.widget.NavigationView>
      
      
      </android.support.v4.widget.DrawerLayout>
      

      这里需要注意NavigationView的两个属性:app:headerLayout=”“和app:menu=”“,

      app:headerLayout:接收一个layout布局,作为导航菜单顶部的Header.

      app:menu:接收一个menu,作为导航菜单的菜单项,这个属性几乎是必选项,不然这个控件就是去意义了.不过也可以在运行时动态改变menu属性.

      该布局的最外层容器是DrawerLayout,这里注意下android:fitsSystemWindows属性,这个属性的作用是根据系统的窗口调整布局,如系统的状态栏.(这里不细说)LinearLayout里面摆放了界面要显示的内容,其中使用include引入的布局是Toolbar(这篇文章简单介绍了Toolbar,有需要可以看下),和FrameLayout.最后就是用于展示侧拉菜单中的菜单内容NavigationView,它的两个重要属性上面已经介绍了,这里就不罗嗦了.

      这里是NavigationView添加的头布局内容和使用menu填充的菜单内容.

      **//添加的头布局drawer_header.xml**
      <?xml version="1.0" encoding="utf-8"?>
      <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:app="http://schemas.android.com/apk/res-auto"
          android:layout_width="match_parent"
          android:layout_height="172dp"
          android:background="@drawable/drawer_background"
          android:paddingTop="25dp">
      
          <de.hdodenhof.circleimageview.CircleImageView
              android:id="@+id/civ_head"
              android:layout_width="64dp"
              android:layout_height="64dp"
              android:layout_margin="16dp"
              android:elevation="4dp"
              android:src="@drawable/hacker"
              app:civ_border_color="@color/white"
              app:civ_border_width="2dp" />
      
          <TextView
              android:id="@+id/tv_username"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:layout_above="@+id/tv_email"
              android:layout_marginLeft="16dp"
              android:layout_marginStart="16dp"
              android:textAppearance="@style/TextAppearance.AppCompat.Body2"
              android:textColor="@color/white" />
      
          <TextView
              android:id="@+id/tv_email"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:layout_alignParentBottom="true"
              android:layout_marginBottom="16dp"
              android:layout_marginLeft="16dp"
              android:textAppearance="@style/TextAppearance.AppCompat.Body1"
              android:textColor="@color/white" />
      
      
      </RelativeLayout>
      
      
      **//drawer_menu.xml**
      <?xml version="1.0" encoding="utf-8"?>
      <menu xmlns:android="http://schemas.android.com/apk/res/android">
      
          <group android:checkableBehavior="single">
              <item
                  android:id="@+id/drawer_home"
                  android:icon="@drawable/ic_home_black"
                  android:title="@string/home" />
      
              <item
                  android:id="@+id/drawer_favourite"
                  android:icon="@drawable/ic_favorite_black"
                  android:title="@string/favourite" />
      
              <item
                  android:id="@+id/drawer_download"
                  android:icon="@drawable/ic_file_download_black"
                  android:title="@string/download" />
      
              <item
                  android:id="@+id/sub"
                  android:title="subTitle">
                  <menu>
                      <item
                          android:id="@+id/drawer_more"
                          android:icon="@drawable/ic_more_horiz_black"
                          android:title="@string/more" />
                      <item
                          android:id="@+id/drawer_settings"
                          android:icon="@drawable/ic_settings_black"
                          android:title="@string/settings" />
                  </menu>
              </item>
          </group>
      
      </menu>
      
    • Activity文件

      public class NavigationViewActivity extends BaseActivity {
      
          @BindView(R.id.toolbar)
          Toolbar toolbar;
          @BindView(R.id.drawerLayout)
          DrawerLayout drawerLayout;
          @BindView(R.id.navigation)
          NavigationView navigationView;
      
          @Override
          protected int getLayoutResId() {
              return R.layout.activity_navigation_view;
          }
      
          @Override
          protected void initView() {
              ButterKnife.bind(this);
      
              //设置状态栏为全透明。
              StatusBarUtil.setTransparent(this);
      
              toolbar.setTitle("NavigationView");
              setSupportActionBar(toolbar);
              //设置DrawerLayout
              ActionBarDrawerToggle drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.open, R.string.close) {
              };
              drawerToggle.syncState();
              drawerLayout.setDrawerListener(drawerToggle);
              //获取头布局内部的控件.在旧版本中,假如你要获得 NavigationView 中的文本控件,你可以在 activity 中直接调用 findViewById() 方法来获得。
              // 但是在 Support Library 23.1.0 版本之后,这个不再适用,在我使用的 Support Library 23.1.1 版本中,
              // 可以直接调用 getHeaderView()方法先得到 Header,然后在通过header来获得头部内的控件。
              View header = navigationView.getHeaderView(0);
              TextView userName = (TextView) header.findViewById(R.id.tv_username);
              TextView email = (TextView) header.findViewById(R.id.tv_email);
              userName.setText("Listener Gao");
              email.setText("hnthgys@gmail.com");
      
              //处理菜单列表种图标不显示原始颜色的问题.  当你设置完图标后,会发现无论图标的原始颜色是什么,都会全部变成灰色.
              // 此时,你可以通过app:itemIconTint="@color/..."属性为图标设置统一的颜色.当然,如果你需要图标显示自己原始的颜色,
              // 可以调用NavigationView的setItemIconTintList(ColorStateList tint)方法,参数传为null即可.
              //navigationView.setItemIconTintList(null);
      
              //设置菜单列表的点击事件。
              navigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {
                  @Override
                  public boolean onNavigationItemSelected(@NonNull MenuItem item) {
                      switch (item.getItemId()) {
                          case R.id.drawer_home:
                              Toast.makeText(NavigationViewActivity.this, "Home", Toast.LENGTH_SHORT).show();
                              drawerLayout.closeDrawers();    //点击条目后,关闭侧滑才菜单
                              item.setChecked(true);          //设置点击过的item为选中状态,字体和图片的颜色会发生变化,颜色会变为和Toolbar的颜色一致。
                              toolbar.setTitle("Home");       //修改Toolbar显示的Title
                              break;
                          case R.id.drawer_favourite:
                              Toast.makeText(NavigationViewActivity.this, "Favourite", Toast.LENGTH_SHORT).show();
                              drawerLayout.closeDrawers();
                              item.setChecked(true);
                              toolbar.setTitle("Favourite");
                              break;
                          case R.id.drawer_download:
                              Toast.makeText(NavigationViewActivity.this, "Download", Toast.LENGTH_SHORT).show();
                              drawerLayout.closeDrawers();
                              item.setChecked(true);
                              toolbar.setTitle("Download");
                              break;
                          case R.id.drawer_more:
                              Toast.makeText(NavigationViewActivity.this, "More", Toast.LENGTH_SHORT).show();
                              drawerLayout.closeDrawers();
                              item.setChecked(true);
                              toolbar.setTitle("More");
                              break;
                          case R.id.drawer_settings:
                              Toast.makeText(NavigationViewActivity.this, "Settings", Toast.LENGTH_SHORT).show();
                              drawerLayout.closeDrawers();
                              item.setChecked(true);
                              toolbar.setTitle("Settings");
                              break;
                      }
                      return false;
                  }
              });
          }
      
          @Override
          protected void initData() {
      
          }
      
          @Override
          public void onBackPressed() {
              //重写返回键,当侧滑菜单处于打开状态,点击返回键就关闭侧滑菜单。
              if (drawerLayout.isDrawerOpen(GravityCompat.START))
                  drawerLayout.closeDrawers();
              else
                  super.onBackPressed();
          }
      }
      

      代码中的注视已经很详细了,这里说一下就是当打开侧滑菜单时,设置状态栏的颜色,我这里是使用了StatusBarUtil将状态栏设置成了透明的(可以直接在github上直接搜到),再一个就是当打开侧滑菜单时,点击返回键,直接就把当前的Activity关闭了,这里是重写了返回键,判断侧滑菜单是否处于打开状态,如果处于打开状态就关闭侧滑菜单.

  • 效果图:

CoordinatorLayout,AppBarLayout,CollapsingToolbarLayout

这三个控件之间存在着一定的关系,这里就放在一起介绍了.

  • 一,首先是CoordinatorLayout: CoordinatorLayout是一个超强的FrameLayout.CoordinatorLayout旨在用于两个基本用例:

    • 1,作为顶级应用程序的装饰或镀布局.(作为顶层布局)
    • 2,作为一个或多个子视图的特异性相互作用的容器.(调度协调子布局)

    CoordinatorLayout使用新的思路通过协调调度子布局的形式实现触摸影响布局的形式产生动画效果.CoordinatorLayout通过设置子View的Behaviors来调度子View.系统(v7包)提供了AppBarLayout.Behavior,AppBarLayout.ScrollingViewBehavior,FloatingActionButton.Behavior,SwipDismissBehavior等.CoordinatorLayout是怎么协调子控件呢?大家可以看下这篇文章:https://segmentfault.com/a/1190000005024216?utm_source=Weibo&utm_medium=shareLink&utm_campaign=socialShare&from=singlemessage&isappinstalled=0其实说简单点:CoordinatorLayout主要用于协调内部的各个子控件进行交互,通过设置子控件的behavior来操控子控件的一些操作和动画。

  • 二,然后是AppBarLayout: AppBarLayout继承自LinearLayout,布局方向为垂直方向.AppBarLayout是在LinearLayout上加了一些材料设计的概念,它可以让你定制当某个可滚动View的滚动手势发生变化时,其内部的子View实现何种动作.一般情况下我们会在AppBarLayout中包裹Toolbar或者TabLayout,当滑动是可以选择需要隐藏的部分。AppBarLayout中有一个内置的Behavior类,而Toolbar和TabLayout中没有,由此可以看出正是因为AppBarLayout中又Behavior类,才能与CoordinatorLayout产生交互(AppBarLayout必须是CoordinatorLayout的直接子View)。需要注意的是AppBarLayout的子控件需要设置app:layout_scrollFlags属性(或者通过setScrollFlags()方法)才能达到其滑动效果。layout_scrollFlags属性有以下几种:

    • 1,scroll:设置该flag,该控件可以滑动到屏幕之外,若未设置则会固定在屏幕的顶部。
    • 2,enterAlways:设置该flag,向下任意的滑动都会导致该控件(设置layout_scrollFlags=”enterAlways”属性的控件)变为可见,启用快速“返回模式”。
    • 3,enterUntilCollapsed:向上滚动时收缩View,但可以固定Toolbar一直在上面。
    • 4,enterAlwaysCollapsed:当你的View已经设置minHeight属性又使用此标志时,你的View只能以最小高度进入,只有当滚动视图到达顶部时才扩大到完整高度。
    • 5,snap(v23.1):简单理解就是View有一种吸附效果,View不会存在局部显示的效果,要么全部滑动出屏幕,要么会在屏幕中显示,有点类似于ViewPager的左右滑动。

    关于Lsyout_ScrollFlags属性的详细解释可以看这篇文章,解释的很详细。http://codecloud.net/17998.html

  • 三,最后是CollapsingToolbarLayout: CollapsingToolbarLayout作用是提供了一个可以折叠的Toolbar,它继承至FrameLayout,给它设置layout_scrollFlags,它可以控制包含在CollapsingToolbarLayout中的控件(如:ImageView、Toolbar)在响应layout_behavior事件时作出相应的scrollFlags滚动事件(移除屏幕或固定在屏幕顶端)。CollapsingToolbarLayout可以通过app:contentScrim设置折叠时工具栏布局的颜色,通过app:statusBarScrim设置折叠时状态栏的颜色。默认contentScrim是colorPrimary的色值,statusBarScrim是colorPrimaryDark的色值。CollapsingToolbarLayout的子布局有3种折叠模式(Toolbar中设置的app:layout_collapseMode):

    • 1,none:这个是默认属性,布局将正常显示,没有折叠的行为。
    • 2,pin: CollapsingToolbarLayout折叠后,Toolbar布局将固定在顶部。
    • 3,parallax: CollapsingToolbarLayout折叠时,此布局会有折叠视差效果。

    当CollapsingToolbarLayout的子布局设置了parallax模式时,我们还可以通过app:layout_collapseParallaxMultiplier设置视差滚动因子,值为:0~1。

以上三种布局需要结合使用,并设置相应的属性才能达到很好的效果。下面是我写的两个效果:

  • 效果一:向上滑动隐藏App bar,因为这里设置了app:layout_scrollFlags=”scroll|enterAlways”,所以当向下滑动时会立即显示App bar。

    • 布局文件:这里需要注意的是需要使用Toolbar,对Toolbar不熟悉的可以参考这篇文章:Toolbar使用详解

      <?xml version="1.0" encoding="utf-8"?>
      <android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:app="http://schemas.android.com/apk/res-auto"
          android:layout_width="match_parent"
          android:layout_height="match_parent">
      
          <android.support.design.widget.AppBarLayout
              android:layout_width="match_parent"
              android:layout_height="wrap_content">
      
              <android.support.v7.widget.Toolbar
                  android:id="@+id/toolbar"
                  android:layout_width="match_parent"
                  android:layout_height="?attr/actionBarSize"
                  android:background="@color/colorPrimary"
                  app:layout_scrollFlags="scroll|enterAlways"
                  app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
                  app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
      
              </android.support.v7.widget.Toolbar>
      
      
          </android.support.design.widget.AppBarLayout>
      
          <android.support.v4.widget.NestedScrollView
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              app:layout_behavior="@string/appbar_scrolling_view_behavior">
      
              <TextView
                  android:id="@+id/tv_content"
                  android:layout_width="match_parent"
                  android:layout_height="wrap_content" />
      
          </android.support.v4.widget.NestedScrollView>
      
      
      </android.support.design.widget.CoordinatorLayout>
      

      这里使用了NestedScrollView控件,因为只有能支持滑动的控件才会有这种效果。还有就是设置的app:layout_behavior=”@string/appbar_scrolling_view_behavior”属性,可以看下上面的解释。

      源码:
      https://github.com/ListenerGao/MyTest/blob/master/app/src/main/java/com/listenergao/mytest/activity/AppBarLayoutActivity.java

  • 效果二:在CollapsingToolbarLayout中设置了一个ImageView和一个Toolbar。并把这个CollapsingToolbarLayout放到AppBarLayout中作为一个整体,并添加FloatActionButton。

    • 布局:

      <?xml version="1.0" encoding="utf-8"?>
      <android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:app="http://schemas.android.com/apk/res-auto"
          xmlns:tools="http://schemas.android.com/tools"
          android:id="@+id/activity_collapsing_toolbar_layout"
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          android:fitsSystemWindows="true"
          tools:context="com.listenergao.mytest.activity.CollapsingToolbarLayoutActivity">
      
          <android.support.design.widget.AppBarLayout
              android:id="@+id/app_bar_layout"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:fitsSystemWindows="true">
      
              <android.support.design.widget.CollapsingToolbarLayout
                  android:id="@+id/collapsing_toolbar_layout"
                  android:layout_width="match_parent"
                  android:layout_height="wrap_content"
                  app:contentScrim="?attr/colorPrimary"
                  app:expandedTitleMarginEnd="30dp"
                  app:expandedTitleMarginStart="40dp"
                  app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
      
                  <ImageView
                      android:layout_width="match_parent"
                      android:layout_height="wrap_content"
                      android:fitsSystemWindows="true"
                      android:scaleType="centerCrop"
                      android:src="@drawable/hacker"
                      app:layout_collapseMode="parallax" />
      
                  <android.support.v7.widget.Toolbar
                      android:id="@+id/toolbar"
                      android:layout_width="match_parent"
                      android:layout_height="?attr/actionBarSize"
                      app:layout_collapseMode="pin"
                      app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
                      app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" />
              </android.support.design.widget.CollapsingToolbarLayout>
      
          </android.support.design.widget.AppBarLayout>
      
          <android.support.v4.widget.NestedScrollView
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              app:layout_behavior="@string/appbar_scrolling_view_behavior">
      
              <TextView
                  android:id="@+id/tv_content"
                  android:layout_width="match_parent"
                  android:layout_height="wrap_content" />
      
          </android.support.v4.widget.NestedScrollView>
      
          <android.support.design.widget.FloatingActionButton
              android:id="@+id/fab"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:layout_margin="20dp"
              app:backgroundTint="@color/colorPrimary"
              android:src="@drawable/ic_favorite"
              app:elevation="4dp"
              app:fabSize="normal"
              app:layout_anchor="@id/app_bar_layout"
              app:layout_anchorGravity="bottom|right"
              app:pressedTranslationZ="8dp"
              app:rippleColor="@color/colorPrimaryDark"
              app:useCompatPadding="false" />
      
      
      </android.support.design.widget.CoordinatorLayout>
      
    • 代码:

      package com.listenergao.mytest.activity;
      
      public class CollapsingToolbarLayoutActivity extends BaseActivity {
      
          @BindView(R.id.toolbar)
          Toolbar toolbar;
          @BindView(R.id.activity_collapsing_toolbar_layout)
          CoordinatorLayout activityCollapsingToolbarLayout;
          @BindView(R.id.tv_content)
          TextView tvContent;
          @BindView(R.id.collapsing_toolbar_layout)
          CollapsingToolbarLayout collapsingToolbarLayout;
          @BindView(R.id.app_bar_layout)
          AppBarLayout appBarLayout;
          @BindView(R.id.fab)
          FloatingActionButton fab;
      
          @Override
          protected int getLayoutResId() {
              return R.layout.activity_collapsing_toolbar_layout;
          }
      
          @Override
          protected void initView() {
              ButterKnife.bind(this);
      
              //设置状态栏为全透明。(这里使用了一个三方库,将状态栏设置为透明)
              StatusBarUtil.setTransparent(this);
      
      //        toolbar.setTitle("CollapsingToolbarLayout");
              setSupportActionBar(toolbar);
              getSupportActionBar().setDisplayHomeAsUpEnabled(true);
      
              collapsingToolbarLayout.setTitle("CollapsingToolbarLayout");
              collapsingToolbarLayout.setExpandedTitleColor(Color.BLACK);     //设置展开时Title的颜色
              collapsingToolbarLayout.setCollapsedTitleTextColor(Color.WHITE);    //设置收缩时Title的颜色
      
              tvContent.setText("文字内容就省略了。。。");
      
      
          }
      
          @Override
          protected void initData() {
      
          }
      
          @OnClick(R.id.fab)
          public void onClick(View view) {
              Snackbar snackbar = Snackbar.make(activityCollapsingToolbarLayout,"已收藏...",Snackbar.LENGTH_LONG);
              snackbar.getView().setBackgroundColor(getResources().getColor(R.color.colorPrimary));
              snackbar.show();
          }
      }
      

      源码:
      https://github.com/ListenerGao/MyTest/blob/master/app/src/main/java/com/listenergao/mytest/activity/CollapsingToolbarLayoutActivity.java

作者:listeners_Gao 发表于2016/11/4 19:37:20 原文链接
阅读:58 评论:0 查看评论

Android 性能优化之String篇

$
0
0

Android 性能优化之 String篇

关于String相关知识都是老掉牙的东西了,但我们经常可能在不经意的String 字符串拼接的情况下浪费内存,影响性能,也常常会成为触发内存OOM的最后一步。
所以本文对String字符串进行深度解析,有助于我们日常开发中提高程序的性能,解决因String 而导致的性能问题。

首先我们先回顾一下String类型的本质

String类型的本质

先看一下String的头部源码

/** Strings are constant; their values cannot be changed after they
 * are created. String buffers support mutable strings.
 * Because String objects are immutable they can be shared. 
 * @see StringBuffer
 * @see StringBuilder
 * @see Charset
 * @since 1.0
 */
public final class String implements Serializable, Comparable<String>, CharSequence {

    private static final long serialVersionUID = -6849794470754667710L;

    private static final char REPLACEMENT_CHAR = (char) 0xfffd;

打开String的源码,类注释中有这么一段话“Strings are constant; their values cannot be changed after they are created. String buffers support mutable strings.Because String objects are immutable they can be shared.”。

这句话总结归纳了String的一个最重要的特点:

String是值不可变(immutable)的常量,是线程安全的(can be shared)。
接下来,String类使用了final修饰符,表明了String类的第二个特点:String类是不可继承的。

String类表示字符串。java程序中的所有字符串,如“ABC”,是实现这个类的实例

字符串是常量,它们的值不能被创建后改变。支持可变字符串字符串缓冲区。因为字符串对象是不可改变的,所以它们可以被共享。例如:

String str = "abc";

相当于

String s = new String("abc");

这里实际上创建了两个String对象,一个是”abc”对象,存储在常量空间中,一个是使用new关键字为对象s申请的空间,存储引用地址。

在执行到双引号包含字符串的语句时,JVM会先到常量池里查找,如果有的话返回常量池里的这个实例的引用,否则的话创建一个新实例并置入常量池里,如上面所示,str 和 s 指向同一个引用.

String的定义方法归纳起来总共为以下四种方式:

  • 直接使用”“引号创建;
  • 使用new String()创建;
  • 使用new String(“abcd”)创建以及其他的一些重载构造函数创建;
  • 使用重载的字符串连接操作符+创建。

常量池

在讨论String的一些本质,先了解一下常量池的概念java中的常量池(constant pool)技术,是为了方便快捷地创建某些对象而出现的,当需要一个对象时,就可以从池中取一个出来(如果池中没有则创建一个),则在需要重复重复创建相等变量时节省了很多时间。常量池其实也就是一个内存空间,不同于使用new关键字创建的对象所在的堆空间。
在编译期被确定,并被保存在已编译的.class文件中的一些数据。它包括了关于类、方法、接口等中的常量,也包括字符串常量。常量池还具备动态性(java.lang.String.intern()),运行期间可以将新的常量放入池中。

常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。

java中基本类型的包装类的大部分都实现了常量池技术,
即Byte,Short,Integer,Long,Character,Boolean;

Java String对象和字符串常量的关系?

JAVA中所有的对象都存放在堆里面,包括String对象。字符串常量保存在JAVA的.class文件的常量池中,在编译期就确定好了。

比如我们通过以下代码块:

String s = new String( "myString" );

其中字符串常量是”myString”,在编译时被存储在常量池的某个位置。在运行阶段,虚拟机发现字符串常量”myString”,它会在一个内部字符串常量列表中查找,如果没有找到,那么会在堆里面创建一个包含字符序列[myString]的String对象s1,然后把这个字符序列和对应的String对象作为名值对( [myString], s1 )保存到内部字符串常量列表中。如下图所示:

image

如果虚拟机后面又发现了一个相同的字符串常量myString,它会在这个内部字符串常量列表内找到相同的字符序列,然后返回对应的String对象的引用。维护这个内部列表的关键是任何特定的字符序列在这个列表上只出现一次。

例如,String s2 = “myString”,运行时s2会从内部字符串常量列表内得到s1的返回值,所以s2和s1都指向同一个String对象。但是String对象s在堆里的一个不同位置,所以和s1不相同。

JAVA中的字符串常量可以作为String对象使用,字符串常量的字符序列本身是存放在常量池中,在字符串内部列表中每个字符串常量的字符序列对应一个String对象,实际使用的就是这个对象。

这个目前网上阐述的最多关于这个String对象和字符串常量的关系,网上各有说法,但是这个猜想也是有问题的
引自感谢博主
http://blog.csdn.net/sureyonder/article/details/5569366

String 在 JVM 的存储结构

String 在 JVM 的存储结构
一般而言,Java 对象在虚拟机的结构如下:
对象头(object header):8 个字节
Java 原始类型数据:如 int, float, char 等类型的数据,各类型数据占内存如 表 1. Java 各数据类型所占内存.
引用(reference):4 个字节
填充符(padding)

如果对于 String(JDK 6)的成员变量声明如下:

private final char value[]; 
  private final int offset; 
  private final int count; 
  private int hash;

JDK6字符串内存占用的计算方式:

首先计算一个空的 char 数组所占空间,在 Java 里数组也是对象,因而数组也有对象头,故一个数组所占的空间为对象头所占的空间加上数组长度,即 8 + 4 = 12 字节 , 经过填充后为 16 字节。

那么一个空 String 所占空间为:

对象头(8 字节)+ char 数组(16 字节)+ 3 个 int(3 × 4 = 12 字节)+1 个 char 数组的引用 (4 字节 ) = 40 字节。

因此一个实际的 String 所占空间的计算公式如下:

8*( ( 8+12+2*n+4+12)+7 ) / 8 = 8*(int) ( ( ( (n) *2 )+43) /8 )

其中,n 为字符串长度。

String 方法很多时候我们移动客户端常用于文本分析及大量字符串处理,
比如高频率的拼接字符串,Log日志输出,会对内存性能造成一些影响。可能导致内存占用太大甚至OOM。
频繁的字符串拼接,使用StringBuffer或者StringBuilder代替String,可以在一定程度上避免OOM和内存抖动。

String 一些提高性能方法

String的contact()方法

public String concat(String str) {
    int otherLen = str.length();
    if (otherLen == 0) {
        return this;
    }
    char buf[] = new char[count + otherLen];
    getChars(0, count, buf, 0);
    str.getChars(0, otherLen, buf, count);
    return new String(0, count + otherLen, buf);
    }

这是concat()的源码,它看上去就是一个数字拷贝形式,我们知道数组的处理速度是非常快的,但是由于该方法最后是这样的:return new String(0, count + otherLen, buf);这同样也创建了10W个字符串对象,这是它变慢的根本原因。

String的intern()方法

当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串(该对象由 equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到池中,并且返回此 String 对象的引用。

例如:

“abc”.intern()方法的返回值还是字符串”abc”,表面上看起来好像这个方法没什么用处。但实际上,它做了个小动作:
检查字符串池里是否存在”abc”这么一个字符串,如果存在,就返回池里的字符串;如果不存在,该方法会把”abc”添加到字符串池中,然后再返回它的引用。

String s1 = new String("111");
String s2 = "sss111";
String s3 = "sss" + "111";
String s4 = "sss" + s1;
System.out.println(s2 == s3); //true
System.out.println(s2 == s4); //false
System.out.println(s2 == s4.intern()); //true

过多得使用 intern()将导致 PermGen 过度增长而最后返回 OutOfMemoryError,因为垃圾收集器不会对被缓存的 String 做垃圾回收,所以如果使用不当会造成内存泄露。

关于截取字符串方法的性能比较

  • 对于从大文本中截取少量字符串的应用,String.substring()将会导致内存的过度浪费。
  • 对于从一般文本中截取一定数量的字符串,截取的字符串长度总和与原始文本长度相差不大,现有的 String.substring()设计恰好可以共享原始文本从而达到节省内存的目的。

更多详细比较请查看这篇博文
http://blog.csdn.net/songylwq/article/details/9016609

使用StringBuilder 提高性能

在拼接动态字符串时,尽量用 StringBuffer 或 StringBuilder的 append,这样可以减少构造过多的临时 String 对象。但是如何正确的使用StringBuilder呢?

初始合适的长度

StringBuilder继承AbstractStringBuilder,打开AbstractStringBuilder的源码

/**
 * A modifiable {@link CharSequence sequence of characters} for use in creating
 * and modifying Strings. This class is intended as a base class for
 * {@link StringBuffer} and {@link StringBuilder}.
 *
 * @see StringBuffer
 * @see StringBuilder
 * @since 1.5
 */
abstract class AbstractStringBuilder {

    static final int INITIAL_CAPACITY = 16;

    private char[] value;

    private int count;

    private boolean shared;

我们可以看到
StringBuilder的内部有一个char[], 不断的append()就是不断的往char[]里填东西的过程。
new StringBuilder(),并且 时char[]的默认长度是16,

private void enlargeBuffer(int min) {
        int newCount = ((value.length >> 1) + value.length) + 2;
        char[] newData = new char[min > newCount ? min : newCount];
        System.arraycopy(value, 0, newData, 0, count);
        value = newData;
        shared = false;
    }

然后如果StringBuilder的剩余容量,无法添加全部内容,如果要append第17个字符,怎么办?可以看到enlargeBuffer函数,用System.arraycopy成倍复制扩容!导致内存的消耗,增加GC的压力。
这要是在高频率的回调或循环下,对内存和性能影响非常大,或者引发OOM。

同时StringBuilder的toString方法,也会造成char数组的浪费。

public String toString() {
    // Create a copy, don't share the array
    return new String(value, 0, count);
}

我们的优化方法是StringBuilder在append()的时候,不是直接往char[]里塞东西,而是先拿一个String[]把它们都存起来,到了最后才把所有String的length加起来,构造一个合理长度的StringBuilder。

重用的StringBuilder

/**
     * 参考BigDecimal, 可重用的StringBuilder, 节约StringBuilder内部的char[]
     *
     * 参考下面的示例代码将其保存为ThreadLocal.
     *
     * <pre>
     * private static final ThreadLocal<StringBuilderHelper> threadLocalStringBuilderHolder = new ThreadLocal<StringBuilderHelper>() {
     *  &#64;Override
     *  protected StringBuilderHelper initialValue() {
     *      return new StringBuilderHelper(256);
     *  }
     * };
     *
     * StringBuilder sb = threadLocalStringBuilderHolder.get().resetAndGetStringBuilder();
     *
     * </pre>
     */
    public class StringBuilderHolder {

        private final StringBuilder sb;

        public StringBuilderHolder(int capacity) {
            sb = new StringBuilder(capacity);
        }

        /**
         * 重置StringBuilder内部的writerIndex, 而char[]保留不动.
         */
        public StringBuilder resetAndGetStringBuilder() {
            sb.setLength(0);
            return sb;
        }
    }

这个做法来源于JDK里的BigDecimal类

Log真正需要时候做拼接

对于那些需要高频率拼接打印Log的场景,封装一个LogUtil,来控制日志在真正需要输出时候才去做拼接。比如:

public void log(String  msg ){
        if (BuildConfig.DEBUG){
            Log.e("TAG","Explicit concurrent mark sweep " +
                    "GC freed 10477(686KB) AllocSpace objects, 0(0B) " +
                    "LOS objects, 39% free, 9MB/15MB, paused 915us total 28.320ms"+msg);
        }
    }

总结几个简单题目

String s1 = new String("s1") ; 
String s2 = new String("s1") ;

上面创建了几个String对象?

答案:3个 ,编译期Constant Pool中创建1个,运行期heap中创建2个.

String s1 = "s1";  
String s2 = s1;  
s2 = "s2";

s1指向的对象中的字符串是什么?

答案: “s1”

总结

关于String 性能优化,了解String 在 JVM 中的存储结构,String 的 API 使用可能造成的性能问题以及解决方法,就总结到这。若有错漏,欢迎补充。

参考文章
https://www.ibm.com/developerworks/cn/java/j-lo-optmizestring/

更多Android 之美,请阅读《Android 之美 从0到1 – 高手之路》系列文章

水平有限,若有错漏,欢迎指正,批评,如需转载,请注明出处–http://blog.csdn.net/vfush,谢谢!

作者:fuchenxuan
出处:http://blog.csdn.net/vfush
欢迎访问我的个人站点:http://fuchenxuan.cn
转载请注明出处–http://blog.csdn.net/vfush

Android 之美 从0到1

作者:vfush 发表于2016/11/4 20:32:50 原文链接
阅读:60 评论:0 查看评论

Android上定义一个懒人专用的log

$
0
0

在Android上打印log时,我习惯已类名为tag,这样在每一个类都得定义一个tag变量,然后又填入第一个参数,后来觉得很麻烦,在网上找到了一篇有用的博客,这个log类通过获取调用所在的类名为tag进行打印,这样就省了每次填第一个参数的烦恼了,方便了许多.原出处不知道在哪里,这个我是转载过来稍微修改的,下面贴上我修改过的源码.修改AppConfig.DEBUG_MODEL的值就可以控制打印或者不打印了.




import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Formatter;
import java.util.Locale;



import android.os.Environment;
import android.text.TextUtils;

/**
 * Log工具,类似android.util.Log。 tag自动产生,格式:
 * customTagPrefix:className.methodName(Line:lineNumber),
 * customTagPrefix为空时只输出:className.methodName(Line:lineNumber)。
 * http://blog.csdn.net/finddreams
 * 复制的博客地址:http://blog.csdn.net/iplayvs2008/article/details/40422999
 * 我调用另一个类转换了,所以此类的类名可以为Log
 */
public class LogLazy {

	public static String customTagPrefix = "junjing"; // 自定义Tag的前缀,可以是作者名
	private static final boolean isSaveLog = false; // 是否把保存日志到SD卡中
	public static final String ROOT = Environment.getExternalStorageDirectory().getPath() + "/finddreams/"; // SD卡中的根目录
	private static final String PATH_LOG_INFO = ROOT + "info/";

	private LogLazy() {
	}

	// 容许打印日志的类型,默认是true,设置为false则不打印
	public static boolean allowD = true;
	public static boolean allowE = true;
	public static boolean allowI = true;
	public static boolean allowV = true;
	public static boolean allowW = true;
	public static boolean allowWtf = true;
	public static final boolean DEBUG = AppConfig.DEBUG_MODEL;

	private static String generateTag(StackTraceElement caller) {
		String tag = "%s.%s(Line:%d)"; // 占位符
		String callerClazzName = caller.getClassName(); // 获取到类名
		callerClazzName = callerClazzName.substring(callerClazzName.lastIndexOf(".") + 1);
		callerClazzName = callerClazzName.substring(callerClazzName.lastIndexOf(".") + 1);// 我的修改,获取倒数第二个类名,原因是我用另一个log输出的.
		tag = String.format(tag, callerClazzName, caller.getMethodName(), caller.getLineNumber()); // 替换
		tag = TextUtils.isEmpty(customTagPrefix) ? tag : customTagPrefix + ":" + tag;
		return tag;
	}

	/**
	 * 自定义的logger
	 */
	public static CustomLogger customLogger;

	public interface CustomLogger {
		void d(String tag, String content);

		void d(String tag, String content, Throwable tr);

		void e(String tag, String content);

		void e(String tag, String content, Throwable tr);

		void i(String tag, String content);

		void i(String tag, String content, Throwable tr);

		void v(String tag, String content);

		void v(String tag, String content, Throwable tr);

		void w(String tag, String content);

		void w(String tag, String content, Throwable tr);

		void w(String tag, Throwable tr);

		void wtf(String tag, String content);

		void wtf(String tag, String content, Throwable tr);

		void wtf(String tag, Throwable tr);
	}

	public static void d(String content) {
		if (!DEBUG)
			return;
		if (!allowD)
			return;
		StackTraceElement caller = getCallerStackTraceElement();
		String tag = generateTag(caller);

		if (customLogger != null) {
			customLogger.d(tag, content);
		} else {
			MyLog.d(tag, content);
		}
	}

	public static void d(String content, Throwable tr) {
		if (!DEBUG)
			return;
		if (!allowD)
			return;
		StackTraceElement caller = getCallerStackTraceElement();
		String tag = generateTag(caller);

		if (customLogger != null) {
			customLogger.d(tag, content, tr);
		} else {
			MyLog.d(tag, content, tr);
		}
	}

	public static void e(String content) {
		if (!DEBUG)
			return;
		if (!allowE)
			return;
		StackTraceElement caller = getCallerStackTraceElement();
		String tag = generateTag(caller);

		if (customLogger != null) {
			customLogger.e(tag, content);
		} else {
			MyLog.e(tag, content);
		}
		if (isSaveLog) {
			point(PATH_LOG_INFO, tag, content);
		}
	}

	public static void e(String content, Throwable tr) {
		if (!DEBUG)
			return;
		if (!allowE)
			return;
		StackTraceElement caller = getCallerStackTraceElement();
		String tag = generateTag(caller);

		if (customLogger != null) {
			customLogger.e(tag, content, tr);
		} else {
			MyLog.e(tag, content, tr);
		}
		if (isSaveLog) {
			point(PATH_LOG_INFO, tag, tr.getMessage());
		}
	}

	public static void i(String content) {
		if (!DEBUG)
			return;
		if (!allowI)
			return;
		StackTraceElement caller = getCallerStackTraceElement();
		String tag = generateTag(caller);

		if (customLogger != null) {
			customLogger.i(tag, content);
		} else {
			MyLog.i(tag, content);
		}

	}

	public static void i(String content, Throwable tr) {
		if (!DEBUG)
			return;
		if (!allowI)
			return;
		StackTraceElement caller = getCallerStackTraceElement();
		String tag = generateTag(caller);

		if (customLogger != null) {
			customLogger.i(tag, content, tr);
		} else {
			MyLog.i(tag, content, tr);
		}

	}

	public static void v(String content) {
		if (!DEBUG)
			return;
		if (!allowV)
			return;
		StackTraceElement caller = getCallerStackTraceElement();
		String tag = generateTag(caller);

		if (customLogger != null) {
			customLogger.v(tag, content);
		} else {
			MyLog.v(tag, content);
		}
	}

	public static void v(String content, Throwable tr) {
		if (!DEBUG)
			return;
		if (!allowV)
			return;
		StackTraceElement caller = getCallerStackTraceElement();
		String tag = generateTag(caller);

		if (customLogger != null) {
			customLogger.v(tag, content, tr);
		} else {
			MyLog.v(tag, content, tr);
		}
	}

	public static void w(String content) {
		if (!DEBUG)
			return;
		if (!allowW)
			return;
		StackTraceElement caller = getCallerStackTraceElement();
		String tag = generateTag(caller);

		if (customLogger != null) {
			customLogger.w(tag, content);
		} else {
			MyLog.w(tag, content);
		}
	}

	public static void w(String content, Throwable tr) {
		if (!DEBUG)
			return;
		if (!allowW)
			return;
		StackTraceElement caller = getCallerStackTraceElement();
		String tag = generateTag(caller);

		if (customLogger != null) {
			customLogger.w(tag, content, tr);
		} else {
			MyLog.w(tag, content, tr);
		}
	}

	public static void w(Throwable tr) {
		if (!DEBUG)
			return;
		if (!allowW)
			return;
		StackTraceElement caller = getCallerStackTraceElement();
		String tag = generateTag(caller);

		if (customLogger != null) {
			customLogger.w(tag, tr);
		} else {
			MyLog.w(tag, tr);
		}
	}

	public static void wtf(String content) {
		if (!DEBUG)
			return;
		if (!allowWtf)
			return;
		StackTraceElement caller = getCallerStackTraceElement();
		String tag = generateTag(caller);

		if (customLogger != null) {
			customLogger.wtf(tag, content);
		} else {
			MyLog.wtf(tag, content);
		}
	}

	public static void wtf(String content, Throwable tr) {
		if (!DEBUG)
			return;
		if (!allowWtf)
			return;
		StackTraceElement caller = getCallerStackTraceElement();
		String tag = generateTag(caller);

		if (customLogger != null) {
			customLogger.wtf(tag, content, tr);
		} else {
			MyLog.wtf(tag, content, tr);
		}
	}

	public static void wtf(Throwable tr) {
		if (!DEBUG)
			return;
		if (!allowWtf)
			return;
		StackTraceElement caller = getCallerStackTraceElement();
		String tag = generateTag(caller);

		if (customLogger != null) {
			customLogger.wtf(tag, tr);
		} else {
			MyLog.wtf(tag, tr);
		}
	}

	private static StackTraceElement getCallerStackTraceElement() {
		return Thread.currentThread().getStackTrace()[4];
	}

	public static void point(String path, String tag, String msg) {
		if (isSDAva()) {
			Date date = new Date();
			SimpleDateFormat dateFormat = new SimpleDateFormat("", Locale.SIMPLIFIED_CHINESE);
			dateFormat.applyPattern("yyyy");
			path = path + dateFormat.format(date) + "/";
			dateFormat.applyPattern("MM");
			path += dateFormat.format(date) + "/";
			dateFormat.applyPattern("dd");
			path += dateFormat.format(date) + ".log";
			dateFormat.applyPattern("[yyyy-MM-dd HH:mm:ss]");
			String time = dateFormat.format(date);
			File file = new File(path);
			if (!file.exists())
				createDipPath(path);
			BufferedWriter out = null;
			try {
				out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file, true)));
				out.write(time + " " + tag + " " + msg + "\r\n");
			} catch (Exception e) {
				e.printStackTrace();
			} finally {
				try {
					if (out != null) {
						out.close();
					}
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}

	/**
	 * 根据文件路径 递归创建文件
	 * 
	 * @param file
	 */
	public static void createDipPath(String file) {
		String parentFile = file.substring(0, file.lastIndexOf("/"));
		File file1 = new File(file);
		File parent = new File(parentFile);
		if (!file1.exists()) {
			parent.mkdirs();
			try {
				file1.createNewFile();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	/**
	 * A little trick to reuse a formatter in the same thread
	 */
	private static class ReusableFormatter {

		private Formatter formatter;
		private StringBuilder builder;

		public ReusableFormatter() {
			builder = new StringBuilder();
			formatter = new Formatter(builder);
		}

		public String format(String msg, Object... args) {
			formatter.format(msg, args);
			String s = builder.toString();
			builder.setLength(0);
			return s;
		}
	}

	private static final ThreadLocal<ReusableFormatter> thread_local_formatter = new ThreadLocal<ReusableFormatter>() {
		protected ReusableFormatter initialValue() {
			return new ReusableFormatter();
		}
	};

	public static String format(String msg, Object... args) {
		ReusableFormatter formatter = thread_local_formatter.get();
		return formatter.format(msg, args);
	}

	public static boolean isSDAva() {
		if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)
				|| Environment.getExternalStorageDirectory().exists()) {
			return true;
		} else {
			return false;
		}
	}

}


作者:OnionOmelette 发表于2016/11/4 20:38:27 原文链接
阅读:14 评论:0 查看评论

利用Viewpager实现引导界面

$
0
0

利用viewPager实现应用的引导界面
不废话,先上效果图:

这里写图片描述

这里写图片描述


这里是随便从网上盗了几张引导界面的图,意思到了就行了。
引导界面,肯定是在用户第一次打开的时候显示,第二次打开程序就不需要显示了。这里比较简单,就直接上代码了:

package students.startuitest;

import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;

/**
 * Created by 24540 on 2016/11/3.
 */
public class WelcomeActivity extends AppCompatActivity {

    private boolean mIsFirstUse;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_welcome);
        SharedPreferences preferences = getSharedPreferences("mIsFirstUse",MODE_PRIVATE);
        mIsFirstUse = preferences.getBoolean("mIsFirstUse", true);
        if(mIsFirstUse){//如果是第一次进入打开程序,设置进入引导界面,否则直接进入主界面
            Intent intent = new Intent(this,GuideActivity.class);
            startActivity(intent);
        }else{
            Intent intent = new Intent(this,MainActivity.class);
            startActivity(intent);
        }
        SharedPreferences.Editor editor = preferences.edit();
        editor.putBoolean("mIsFirstUse",false);
        editor.commit();
    }
}

xml布局文件比较简单,这里就不贴代码了。下面重头戏来了,下面就是我们引导界面实现的主要逻辑,先看代码:

package students.startuitest;

import android.content.Intent;
import android.os.Bundle;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;

import java.util.ArrayList;
import java.util.List;

import students.startuitest.transormer.ZoomOutTransformer;

public class GuideActivity extends AppCompatActivity implements ViewPager.OnPageChangeListener{

    private ViewPager viewPager;
    private List<View> views = new ArrayList<>();
    private ViewPagerAdapter mAdapter;
    private View view1,view2,view3,view4;
    private Button mState;

    //底部小点图片
    private LinearLayout mPoint;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_guilde);
        initView();
        initData();
    }

    private void initData() {
        views.add(view1);
        views.add(view2);
        views.add(view3);
        views.add(view4);
        mAdapter = new ViewPagerAdapter(views);
        viewPager.setOnPageChangeListener(this);
        addPoint();
        viewPager.setAdapter(mAdapter);
//        viewPager.setPageTransformer(true,new DepthTransformer());//设置翻页动画效果
        viewPager.setPageTransformer(true,new ZoomOutTransformer());//设置淡入淡出动画效果


        mState.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent= new Intent(GuideActivity.this,MainActivity.class);
                startActivity(intent);
            }
        });
    }

    private void initView() {
        viewPager = (ViewPager) findViewById(R.id.viewpager);
        mPoint = (LinearLayout) findViewById(R.id.llPoint);
        LayoutInflater inflater =LayoutInflater.from(this);
        view1 = inflater.inflate(R.layout.view_guide001,null);
        view2 = inflater.inflate(R.layout.view_guide002,null);
        view3 = inflater.inflate(R.layout.view_guide003,null);
        view4 = inflater.inflate(R.layout.view_guide004,null);
        mState = (Button) view4.findViewById(R.id.start_use);

    }

    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

    }

    @Override
    public void onPageSelected(int position) {
        Log.d("TAG",position+"pageSelected");
        monitorPoint(position);
//        showCurrentIndex(position);
    }

    @Override
    public void onPageScrollStateChanged(int state) {

    }

    class ViewPagerAdapter extends PagerAdapter{
    //这里必须重写以下方法,
     private  List<View> views;

        public ViewPagerAdapter(List<View> views) {
            this.views = views;
        }

        @Override
        public int getCount() {
            return views.size();
        }

        //销毁postion位置的界面
        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {
            (container).removeView(views.get(position));
        }
        /**
         *  初始化position位置的界面
         */
        @Override
        public Object instantiateItem(ViewGroup container, int position) {
           View view = views.get(position);
            container.addView(view);
            return view;
        }
        //判断是否由对象生成界面
        @Override
        public boolean isViewFromObject(View view, Object object) {
            return view==object;
        }
    }

    /**
     * 添加小圆点
     */
    private  void addPoint(){
        for (int i = 0; i < views.size(); i++) {
            LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
                    ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
            if(i<1){
                params.setMargins(0,0,0,0);
            }else{
                params.setMargins(10,0,0,0);
            }
            ImageView iv = new ImageView(this);
            iv.setLayoutParams(params);
            iv.setBackgroundResource(R.mipmap.select_select);
            mPoint.addView(iv);
        }
        mPoint.getChildAt(0).setBackgroundResource(R.mipmap.select);
    }

    /**
     * 标识当前查看进度
     * @param position
     */
    private void monitorPoint(int position){
        for (int i = 0; i <=views.size(); i++) {
            if(i==position){
                mPoint.getChildAt(position).setBackgroundResource(R.mipmap.select_select);
            }else{
                mPoint.getChildAt(position).setBackgroundResource(R.mipmap.select);
            }
        }

    }

    /**
     * 标识当前查看的是那个目录
     * @param position
     */
    private void showCurrentIndex(final int position){
        for (int i = 0; i < views.size(); i++) {
            if(i==position){
                mPoint.getChildAt(i).setBackgroundResource(R.mipmap.select);
            }else{
                mPoint.getChildAt(i).setBackgroundResource(R.mipmap.select_select);
            }
        }
    }

}

这里值的注意的是,实现ViewPagerAdapter时,必须实现里面的四个方法

 @Override
        public int getCount() {
            return views.size();
        }

        //销毁postion位置的界面
        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {
            (container).removeView(views.get(position));
        }
        /**
         *  初始化position位置的界面
         */
        @Override
        public Object instantiateItem(ViewGroup container, int position) {
           View view = views.get(position);
            container.addView(view);
            return view;
        }
        @Override
        public boolean isViewFromObject(View view, Object object) {
            return view==object;
        }

否则会抛出异常,以前就是因为只实现了其中的两个方法而导致了奔溃。
下面是对应的xml文件布局

<?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">

    <android.support.v4.view.ViewPager
        android:id="@+id/viewpager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        />

    <LinearLayout
        android:id="@+id/llPoint"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"
        android:layout_marginBottom="24.0dip"
        android:gravity="center_horizontal"
        android:orientation="horizontal"
        /><!--这里用来放置阅读进度提示-->
</RelativeLayout>

参考了鸿洋大神的博客,添加了一点特殊的view的切换效果。源码如下,具体原理请参考原作者.

package students.startuitest.transormer;

import android.support.v4.view.ViewPager;
import android.view.View;

/**
 * 设置翻页的动画效果
 * Created by 24540 on 2016/11/4.
 */
public class DepthTransformer implements ViewPager.PageTransformer {

    private static final float MIN_SCALE = 0.75f;
    public void transformPage(View view, float position) {
        int pageWidth = view.getWidth();

        if (position < -1) {
            view.setAlpha(0);
        } else if (position <= 0) {
            view.setAlpha(1);
            view.setTranslationX(0);
            view.setScaleX(1);
            view.setScaleY(1);
        } else if (position <= 1) {
            view.setAlpha(1 - position);
            view.setTranslationX(pageWidth * -position);
            float scaleFactor = MIN_SCALE
                    + (1 - MIN_SCALE) * (1 - Math.abs(position));
            view.setScaleX(scaleFactor);
            view.setScaleY(scaleFactor);
        } else {
            view.setAlpha(0);
        }
    }
}

package students.startuitest.transormer;

import android.annotation.SuppressLint;
import android.support.v4.view.ViewPager;
import android.util.Log;
import android.view.View;

/**
 * Created by 24540 on 2016/11/4.
 */
public class ZoomOutTransformer implements ViewPager.PageTransformer{
    private static final float MIN_SCALE = 0.85f;
    private static final float MIN_ALPHA = 0.5f;
    @SuppressLint("NewApi")
    public void transformPage(View view, float position)
    {
        int pageWidth = view.getWidth();
        int pageHeight = view.getHeight();

        Log.e("TAG", view + " , " + position + "");

        if (position < -1)
        { // [-Infinity,-1)
            // This page is way off-screen to the left.
            view.setAlpha(0);

        } else if (position <= 1) //a页滑动至b页 ; a页从 0.0 -1 ;b页从1 ~ 0.0
        { // [-1,1]
            // Modify the default slide transition to shrink the page as well
            float scaleFactor = Math.max(MIN_SCALE, 1 - Math.abs(position));
            float vertMargin = pageHeight * (1 - scaleFactor) / 2;
            float horzMargin = pageWidth * (1 - scaleFactor) / 2;
            if (position < 0)
            {
                view.setTranslationX(horzMargin - vertMargin / 2);
            } else
            {
                view.setTranslationX(-horzMargin + vertMargin / 2);
            }

            // Scale the page down (between MIN_SCALE and 1)
            view.setScaleX(scaleFactor);
            view.setScaleY(scaleFactor);

            // Fade the page relative to its size.
            view.setAlpha(MIN_ALPHA + (scaleFactor - MIN_SCALE)
                    / (1 - MIN_SCALE) * (1 - MIN_ALPHA));

        } else
        { // (1,+Infinity]
            // This page is way off-screen to the right.
            view.setAlpha(0);
        }
    }
}

使用方法在GuideActivity中有提示,主要实现逻辑代码

  viewPager.setPageTransformer(true,new DepthTransformer());//设置翻页动画效果
        viewPager.setPageTransformer(true,new ZoomOutTransformer());//设置淡入淡出动画效果

至此,引导界面基本完成了。最后,贴上manifest中的代码:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="students.startuitest">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/select"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".WelcomeActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <activity android:name=".GuideActivity"/>
        <activity android:name=".MainActivity"/>
    </application>

</manifest>

引导界面xml的一个布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:background="@drawable/guide001">

</LinearLayout>

好了,利用viewPager实现应用程序的引导界面就到这里了,奉上源码下载地址.

参考博客:http://blog.csdn.net/lmj623565791/article/details/40411921
http://blog.csdn.net/li5685918/article/details/51547681

作者:Reoger 发表于2016/11/4 20:39:47 原文链接
阅读:12 评论:0 查看评论

Android输入事件从读取到分发五:事件分发前的拦截过程

$
0
0

在前面的文章:Android输入事件从读取到分发三:InputDispatcherThread线程分发事件的过程 一文中已经提过事件在分发前要做拦截的事情,只不过当时没有展开来分析,因此这篇文章的主要目的就是分析事件在分发前的拦截过程。(注:Android源码版本为6.0)
Android输入事件从读取到分发三:InputDispatcherThread线程分发事件的过程 一文中我们分析到InputDispatcher类的notifyKey方法中,第一次尝试拦截事件,可以在看看这个方法:

void InputDispatcher::notifyKey(const NotifyKeyArgs* args) {
...
    KeyEvent event;
    event.initialize(args->deviceId, args->source, args->action,
            flags, keyCode, args->scanCode, metaState, 0,
            args->downTime, args->eventTime);

    mPolicy->interceptKeyBeforeQueueing(&event, /*byref*/ policyFlags);
...
}

这里是事件进入队列前的拦截,这里将其称为第一次拦截吧。
除此之外,在事件分发之前还要做一次拦截,也就是事件进入到InputDispatcherThread线程后,在发送事件之前,做一次拦截,调用流程如下:
dispatchOnce
->dispatchOnceInnerLocked
->dispatchKeyLocked
->doInterceptKeyBeforeDispatchingLockedInterruptible
->mPolicy->interceptKeyBeforeDispatching
这个过程这里将其称为二次拦截吧。

有了上面知识的铺垫,下面,我们逐一分析两次拦截过程。

第一次事件拦截

首先看下时序图:
这里写图片描述
接下来,跟着时序图,我们分析下事件拦截的源码:
当我们在InputDispatcher::notifyKey调用mPolicy->interceptKeyBeforeQueueing方法后,就进入到NativeInputManager::interceptKeyBeforeQueueing方法了:

void NativeInputManager::interceptKeyBeforeQueueing(const KeyEvent* keyEvent,
        uint32_t& policyFlags) {
    // Policy:
    // - Ignore untrusted events and pass them along.
    // - Ask the window manager what to do with normal events and trusted injected events.
    // - For normal events wake and brighten the screen if currently off or dim.
    bool interactive = mInteractive.load();
    if (interactive) {
        policyFlags |= POLICY_FLAG_INTERACTIVE;
    }
    if ((policyFlags & POLICY_FLAG_TRUSTED)) {
        nsecs_t when = keyEvent->getEventTime();
        JNIEnv* env = jniEnv();
        jobject keyEventObj = android_view_KeyEvent_fromNative(env, keyEvent);
        jint wmActions;
        if (keyEventObj) {
            wmActions = env->CallIntMethod(mServiceObj,
                    gServiceClassInfo.interceptKeyBeforeQueueing,
                    keyEventObj, policyFlags);
            if (checkAndClearExceptionFromCallback(env, "interceptKeyBeforeQueueing")) {
                wmActions = 0;
            }
            android_view_KeyEvent_recycle(env, keyEventObj);
            env->DeleteLocalRef(keyEventObj);
        } else {
            ALOGE("Failed to obtain key event object for interceptKeyBeforeQueueing.");
            wmActions = 0;
        }

        handleInterceptActions(wmActions, when, /*byref*/ policyFlags);
    } else {
        if (interactive) {
            policyFlags |= POLICY_FLAG_PASS_TO_USER;
        }
    }
}

这个函数首先根据传下来的KeyEvent类型的参数构造一个keyEventObj,构造的过程是调用android_view_KeyEvent_fromNative方法实现的:

jobject android_view_KeyEvent_fromNative(JNIEnv* env, const KeyEvent* event) {
    jobject eventObj = env->CallStaticObjectMethod(gKeyEventClassInfo.clazz,
            gKeyEventClassInfo.obtain,
            nanoseconds_to_milliseconds(event->getDownTime()),
            nanoseconds_to_milliseconds(event->getEventTime()),
            event->getAction(),
            event->getKeyCode(),
            event->getRepeatCount(),
            event->getMetaState(),
            event->getDeviceId(),
            event->getScanCode(),
            event->getFlags(),
            event->getSource(),
            NULL);
    if (env->ExceptionCheck()) {
        ALOGE("An exception occurred while obtaining a key event.");
        LOGE_EX(env);
        env->ExceptionClear();
        return NULL;
    }
    return eventObj;
}

这个方法使用了jni来调用java层的一个静态方法obtain,使用这个方法构造了一个eventObj 并返回。这里不是我们关注的,暂时这样吧,回NativeInputManager::interceptKeyBeforeQueueing方法中,构造好keyEventObj对象后,又使用jni调用了java层的返回值为int的实例方法,这个实例由mServiceObj决定,它其实就是InputManagerService的实例。大家稍微追踪一下就会明白,这里就不啰嗦了。
然后就进入到一系列的interceptKeyBeforeQueueing方法的调用了:

    // Native callback.
    private int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
        return mWindowManagerCallbacks.interceptKeyBeforeQueueing(event, policyFlags);
    }

mWindowManagerCallbacks的实现类是InputMonitor,它的interceptKeyBeforeQueueing方法如下:

    @Override
    public int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
        return mService.mPolicy.interceptKeyBeforeQueueing(event, policyFlags);
    }

这个函数中的mPolicy定义如下:

final WindowManagerPolicy mPolicy = new PhoneWindowManager();

因此,接下来进入到了PhoneWindowManager的interceptKeyBeforeQueueing方法了。


    /** {@inheritDoc} */
    @Override
    public int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
        if (!mSystemBooted) {
            // If we have not yet booted, don't let key events do anything.
            return 0;
        }
        ...
        final boolean interactive = (policyFlags & FLAG_INTERACTIVE) != 0;
        final boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
        final boolean canceled = event.isCanceled();
        switch (keyCode) {
            case KeyEvent.KEYCODE_VOLUME_DOWN:
            case KeyEvent.KEYCODE_VOLUME_UP:
            case KeyEvent.KEYCODE_VOLUME_MUTE: {

             if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
                    if (down) {
                        if (interactive && !mScreenshotChordVolumeDownKeyTriggered
                                && (event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
                            mScreenshotChordVolumeDownKeyTriggered = true;
                            mScreenshotChordVolumeDownKeyTime = event.getDownTime();
                            mScreenshotChordVolumeDownKeyConsumed = false;
                            cancelPendingPowerKeyAction();
                            interceptScreenshotChord();
                        }
                    } else {
                        mScreenshotChordVolumeDownKeyTriggered = false;
                        cancelPendingScreenshotChordAction();
                    }
                } else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
                    if (down) {
                        if (interactive && !mScreenshotChordVolumeUpKeyTriggered
                                && (event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
                            mScreenshotChordVolumeUpKeyTriggered = true;
                            cancelPendingPowerKeyAction();
                            cancelPendingScreenshotChordAction();
                        }
                    } else {
                        mScreenshotChordVolumeUpKeyTriggered = false;
                        cancelPendingScreenshotChordAction();
                    }
                }       return result;
        ...
    }

这个方法很长,这里只贴出一小部分。这个方法的返回值很关键,返回0则意味着事件被拦截,返回1则意味着事件允许被发送到应用程序中。我们看下最终返回值的处理。再次回到NativeInputManager::interceptKeyBeforeQueueing方法,返回值保存在wmActions变量中,然后调用handleInterceptActions方法处理返回值。其定义如下:

void NativeInputManager::handleInterceptActions(jint wmActions, nsecs_t when,
        uint32_t& policyFlags) {
    if (wmActions & WM_ACTION_PASS_TO_USER) {
        policyFlags |= POLICY_FLAG_PASS_TO_USER;
    } else {
#if DEBUG_INPUT_DISPATCHER_POLICY
        ALOGD("handleInterceptActions: Not passing key to user.");
#endif
    }
}

WM_ACTION_PASS_TO_USER定义如下:

enum {
    WM_ACTION_PASS_TO_USER = 1,
};

这里位运算,但结果就是如果返回值为1,二者位与后为1,则给policyFlags 添加POLICY_FLAG_PASS_TO_USER标志,意味着可以把该事件发送到应用程序,否则,从注释中可以知道不会发送事件给用户。
第一次事件拦截具体会拦截什么事件,大家可以自己去看,你可以直接去看PhoneWindowManager的interceptKeyBeforeQueueing方法,看看这个方法中,那些事件处理后返回值为0。如果返回值为0则说明这个事件被拦截了。
接下来我们看下第二次拦截

第二次事件拦截

首先看下时序图:
这里写图片描述
从图中可以看到其调用过程和第一阶段完全相同,因此这里就不再追踪源码了。感兴趣可以看看PhoneWindowManager的interceptKeyBeforeDispatching方法,这个方法对事件做了二次拦截,这个方法的返回值为-1则说明事件被拦截,返回值为0则说明事件被放行。
我们看下返回值的处理过程:

            jlong delayMillis = env->CallLongMethod(mServiceObj,
                    gServiceClassInfo.interceptKeyBeforeDispatching,
                    inputWindowHandleObj, keyEventObj, policyFlags);
            bool error = checkAndClearExceptionFromCallback(env, "interceptKeyBeforeDispatching");
            android_view_KeyEvent_recycle(env, keyEventObj);
            env->DeleteLocalRef(keyEventObj);
            if (!error) {
                if (delayMillis < 0) {
                    result = -1;
                } else if (delayMillis > 0) {
                    result = milliseconds_to_nanoseconds(delayMillis);
                }
            }

这里可以看到返回值存放在delayMillis 变量中,接着判断:
如果返回值为负数,那么result=-1,入则,返回值等于0则不处理,因为result默认初始化值为0,如果返回值大于0则把milliseconds_to_nanoseconds的返回值给result。milliseconds_to_nanoseconds方的定义如下:

static CONSTEXPR inline nsecs_t milliseconds_to_nanoseconds(nsecs_t secs)
{
    return secs*1000000;
}

可以看到就是给返回值*1000000.
result最终会返回到InputDispatcher中:

void InputDispatcher::doInterceptKeyBeforeDispatchingLockedInterruptible(
        CommandEntry* commandEntry) {
    KeyEntry* entry = commandEntry->keyEntry;

    KeyEvent event;
    initializeKeyEvent(&event, entry);

    mLock.unlock();

    nsecs_t delay = mPolicy->interceptKeyBeforeDispatching(commandEntry->inputWindowHandle,
            &event, entry->policyFlags);

    mLock.lock();

    if (delay < 0) {
        entry->interceptKeyResult = KeyEntry::INTERCEPT_KEY_RESULT_SKIP;
    } else if (!delay) {
        entry->interceptKeyResult = KeyEntry::INTERCEPT_KEY_RESULT_CONTINUE;
    } else {
        entry->interceptKeyResult = KeyEntry::INTERCEPT_KEY_RESULT_TRY_AGAIN_LATER;
        entry->interceptKeyWakeupTime = now() + delay;
    }
    entry->release();
}

这个方法中,会根据返回值给entry->interceptKeyResult变量赋值。从名字上我们可以猜测,返回值小于0则拦截事件,等于0则放行事件,大于0是待会再检测是否需要拦截?
这三种类型对应的处理方式在InputDispatcher::dispatchKeyLocked方法中:

    // Handle case where the policy asked us to try again later last time.
    if (entry->interceptKeyResult == KeyEntry::INTERCEPT_KEY_RESULT_TRY_AGAIN_LATER) {
        if (currentTime < entry->interceptKeyWakeupTime) {
            if (entry->interceptKeyWakeupTime < *nextWakeupTime) {
                *nextWakeupTime = entry->interceptKeyWakeupTime;
            }
            return false; // wait until next wakeup
        }
        entry->interceptKeyResult = KeyEntry::INTERCEPT_KEY_RESULT_UNKNOWN;
        entry->interceptKeyWakeupTime = 0;
    }

这里展示了对INTERCEPT_KEY_RESULT_TRY_AGAIN_LATER的处理,会判断拦截时间和当前时间,如果当前时间小于拦截时间,则下次循环再处理。所以我们理解的是对的。

 if (entry->interceptKeyResult == KeyEntry::INTERCEPT_KEY_RESULT_SKIP) {
        if (*dropReason == DROP_REASON_NOT_DROPPED) {
            *dropReason = DROP_REASON_POLICY;
        }
    }

这里展示了INTERCEPT_KEY_RESULT_SKIP类型的处理,如果dropReason 的状态为不没有丢弃事件的话,那就把它的状态改为因为策略丢弃。也就是事件被拦截了。
INTERCEPT_KEY_RESULT_CONTINUE则是不做处理了。所有没有对应这种状态的处理代码。

作者:u011913612 发表于2016/11/7 17:26:18 原文链接
阅读:211 评论:0 查看评论

欢迎使用CSDN-markdown编辑器

$
0
0

1. 什么是dumpsys

The dumpsys tool runs on the device and provides information about the status of system services.
dumpsys这个工具可以查看当前设备系统服务信息。

2. 如何使用dumpsys

如果你直接运行adb shell dumpsys的话,会得到所有系统服务的输出,输出结果比你想要的多得多。为了控制输出内容,需要指定想要查看的服务。如:
$ adb shell dumpsys input

3. dumpsys能查看的信息有那些

用下面的命令可以列出所支持查看的系统服务

$ adb shell dumpsys -l
Currently running services:
DockObserver
SurfaceFlinger
accessibility
account
activity
alarm
…

常用命令举例:
这里写图片描述
这里写图片描述

不同服务可以跟不同的命令选项,以activity为例:
这里写图片描述

获取Activity信息:

adb shell dumpsys activity
加上-h可以获取帮助信息
获取当前界面的activity详情,可以用:
adb shell dumpsys activity top(控件都看得到)
对了,还有一种情况,就是当前界面是一个activity动态加载的fragment的话,这时候就算知道了activity也对应不上界面的内容,不过这时候还是可以用这个命令,去看输出结果的Fragment关键字,排在最上面的就是当前界面对应的Fragment。
要获取当前界面的Activity(结果显示的是最顶层的activity,但activity可能不是当前界面上的最顶层,自行体会):
adb shell dumpsys activity top | findstr ACTIVITY这个其实就是把上面命令的结果过滤一下只显示第一行
(findstr不可用的话用grep)
最近activity:
这里写图片描述

获取电源管理信息:

adb shell dumpsys power
可以获取到是否处于锁屏状态:mWakefulness=Asleep或者mScreenOn=false
亮度值:mScreenBrightness=255
屏幕休眠时间:Screen off timeout: 60000 ms
屏幕分辨率:mDisplayWidth=1440,mDisplayHeight=2560
wake lock:Wake Locks:size =
这里写图片描述
GsmConnection是一个tag,在new wake_lock的时候自行定义,通过pid可以确定其所在的服务进程。

查看手机telephony状态:

可以看网络注册状态,数据链接状态,是否漫游,信号强度,等等,参数我就不一一解毒了,跟android系统版本也有关系
这里写图片描述

查看notification:

adb shell dumpsys notification
查看是由哪个应用发出的通知。比如你用着手机的时候出来一个神烦的广告,但是你却不知道它是哪个应用弹出的,那去卸载哪个应用呢?
这里写图片描述
我们截图输出结果的一部分

NotificationRecord(0x01b93b62: pkg=com.tencent.mobileqq user=UserHandle{0} id=121 tag=null score=0 key=0|com.tencent.mobileqq|121|null|10125: Notification(pri=0 contentView=null vibrate=null sound=null tick defaults=0x0 flags=0x11 color=0x00000000 vis=PRIVATE))
      uid=10125 userId=0
      icon=Icon(typ=RESOURCE pkg=com.tencent.mobileqq id=0x7f0204bf) / com.tencent.mobileqq:drawable/name
      pri=0 score=0
      key=0|com.tencent.mobileqq|121|null|10125
      seen=true
      groupKey=0|com.tencent.mobileqq|121|null|10125
      contentIntent=PendingIntent{bc254f3: PendingIntentRecord{e9265b0 com.tencent.mobileqq startActivity}}
      deleteIntent=PendingIntent{5711c29: PendingIntentRecord{873abae com.tencent.mobileqq broadcastIntent}}
      tickerText=Flour_Mo(MoKee Project):[挖鼻孔]

比较关键的是contentIntent和deleteIntent,这两个类型都是PendingIntent。注意这里能看到的是应用的包名,并不能看到点击后会跳转到哪个activity(大家代码都会混淆的好嘛,想看也看不到吧)。

查看SurfaceFlinger:

adb shell dumpsys SurfaceFlinger
这里写图片描述
用来查看当前界面上有几个frame,分别的源是什么。

查看wifi信息(连接记录):

adb shell dumpsys wifi
这个厉害了,可以看看他/她有没有连过闺蜜/老王加的wifi!
这里写图片描述
最后一个乱码,因为是中文wifi,不过后面也可以看到中文名
这里写图片描述
可以看到很多信息的,自己去发掘吧(密码没有,要看密码去看系统文件吧)。

4. 使用到dumpsys的案例

4.1 接听视频来电屏幕会休眠

一般来说,处于视频电话的时候,屏幕应该保持常亮的,这样才能方便用户查看视频内容。
问题出现的时候根据以往的经验,知道这很有可能是某个wake_lock没有申请到,比较MO和MT的wake_lock看到:
adb shell sumpsys power
这里写图片描述
Mt端少了SCREEN_BRIGHT_WAKE_LOCK,而它在PowerManager.java中的定义正是:
这里写图片描述
直接搜关键字SCREEN_BRIGHT_WAKE_LOCK没有在InCallUI,Telecomm,Telephony搜到相关的wake_lock类型,但是我们看到这个wake_lock和FLAG_KEEP_SCREEN_ON有联系,多数应用都是用的这个flag,那么我们再搜这个关键字的收,就在InCallUI中找到了出问题的地方,找到问题了就好改啦~

4.2 设置双卡询问,弹出的dialog属于Dialer还是InCallUI

双卡手机在设置了拨号前每次询问的时候,会弹出一个dialog(当然也有厂家定制的在拨号盘上之间诶选择),我们的入口是Dialer,而通话界面属于InCallUI,那这个选择SIM卡的dialog属于谁?
用下面的命令可以查看
这里写图片描述
如果你觉得这个方法不是那么有必要,那你应该还没有遇见过没title没内容的dialog/斜眼笑
还有这个方法适合于大多数场景,但也有例外。比如这个顶层界面(不一定是dialog)上的东西不是从Activity中显示出来的,(纳尼哦?!居然有这种事?!)这时候可以尝试用adb shell dumpsys SurfaceFlinger,这个命令可以查看界面是由哪些内容绘制的(想知道原理的可以去看SurfaceFlinger机制)
这里写图片描述
上图的信息显示,当前界面有状态栏,导航栏,还有一个联想的com.lenovo.ideafriend/com.lenovo.ideafriend.alias.DialtactsActivity,那状态栏和导航栏都很好认,界面上剩下的就是那个DialtactsActivity了。
感谢阅读。

作者:aaa111 发表于2016/11/7 17:30:18 原文链接
阅读:78 评论:0 查看评论

116.iOS获取设备的唯一标识的方法总结以及最好的方法

$
0
0

各种获取设备唯一标识的方法介绍

一.UDID(Unique Device Identifier)

UDID的全称是Unique Device Identifier,它就是苹果iOS设备的唯一识别码,它由40位16进制数的字母和数字组成(越狱的设备通过某些工具可以改变设备的UDID)。移动网络可利用UDID来识别移动设备,但是,从IOS5.0(2011年8月份)开始,苹果宣布将不再支持用uniqueIdentifier方法获取设备的UDID,iOS5以下是可以用的。苹果从iOS5开始就移除了通过代码访问UDID的权限。从2013年5月1日起,试图访问UIDIDs的程序将不再被审核通过,替代的方案是开发者应该使用“在iOS 6中介绍的Vendor或Advertising标示符”。所以UDID是绝对是不能再使用了。

//UUID , 已废除
NSString *udid = [[UIDevice currentDevice] uniqueIdentifier];

为什么苹果反对开发人员使用UDID?
iOS 2.0版本以后UIDevice提供一个获取设备唯一标识符的方法uniqueIdentifier,通过该方法我们可以获取设备的序列号,这个也是目前为止唯一可以确认唯一的标示符。 许多开发者把UDID跟用户的真实姓名、密码、住址、其它数据关联起来;网络窥探者会从多个应用收集这些数据,然后顺藤摸瓜得到这个人的许多隐私数据。同时大部分应用确实在频繁传输UDID和私人信息。 为了避免集体诉讼,苹果最终决定在iOS 5 的时候,将这一惯例废除,开发者被引导生成一个唯一的标识符,只能检测应用程序,其他的信息不提供。现在应用试图获取UDID已被禁止且不允许上架。

二.UUID(Universally Unique Identifier)

UUID是Universally Unique Identifier的缩写,中文意思是通用唯一识别码。它是让分布式系统中的所有元素,都能有唯一的辨识资讯,而不需要透过中央控制端来做辨识资讯的指定。这样,每个人都可以建立不与其它人冲突的 UUID。在此情况下,就不需考虑数据库建立时的名称重复问题。苹果公司建议使用UUID为应用生成唯一标识字符串。
获得的UUID值系统没有存储, 而且每次调用得到UUID,系统都会返回一个新的唯一标示符。如果你希望存储这个标示符,那么需要自己将其存储到NSUserDefaults, Keychain, Pasteboard或其它地方。

1.CFUUID

从iOS2.0开始,CFUUID就已经出现了。它是CoreFoundatio包的一部分,因此API属于C语言风格。CFUUIDCreate 方法用来创建CFUUIDRef,并且可以获得一个相应的NSString,如下代码:

CFUUIDRef cfuuid = CFUUIDCreate(kCFAllocatorDefault);NSString *cfuuidString = (NSString*)CFBridgingRelease(CFUUIDCreateString(kCFAllocatorDefault, cfuuid));

获得的这个CFUUID值系统并没有存储。每次调用CFUUIDCreate,系统都会返回一个新的唯一标示符。如果你希望存储这个标示符,那么需要自己将其存储到NSUserDefaults, Keychain, Pasteboard或其它地方。

2.NSUUID

NSUUID在iOS 6中才出现,这跟CFUUID几乎完全一样,只不过它是Objective-C接口。+ (id)UUID 是一个类方法,调用该方法可以获得一个UUID。通过下面的代码可以获得一个UUID字符串:

NSString *uuid = [[NSUUID UUID] UUIDString];

跟CFUUID一样,这个值系统也不会存储,每次调用的时候都会获得一个新的唯一标示符。如果要存储的话,你需要自己存储。在我读取NSUUID时,注意到获取到的这个值跟CFUUID完全一样(不过也可能不一样)

三.open UDID

在iOS 5发布时,uniqueIdentifier被弃用了,这引起了广大开发者需要寻找一个可以替代UDID,并且不受苹果控制的方案。由此OpenUDID成为了当时使用最广泛的开源UDID替代方案。OpenUDID在工程中实现起来非常简单,并且还支持一系列的广告提供商。

OpenUDID利用了一个非常巧妙的方法在不同程序间存储标示符 — 在粘贴板中用了一个特殊的名称来存储标示符。通过这种方法,别的程序(同样使用了OpenUDID)知道去什么地方获取已经生成的标示符(而不用再生成一个新的)。而且根据贡献者的代码和方法,和一些开发者的经验,如果把使用了OpenUDID方案的应用全部都删除,再重新获取OpenUDID,此时的OpenUDID就跟以前的不一样。可见,这种方法还是不保险。
但是OpenUDID库早已经弃用了, 在其官方的博客中也指明了, 停止维护OpenUDID的原因是为了更好的向苹果的举措靠拢, 还指明了MAC Address不是一个好的选择。

四.MAC Address

1.这个MAC地址是指什么?有什么用?

MAC(Medium/Media Access Control)地址,用来表示互联网上每一个站点的标识符,采用十六进制数表示,共六个字节(48位)。其中,前三个字节是由IEEE的注册管理机构 RA负责给不同厂家分配的代码(高位24位),也称为“编制上唯一的标识符” (Organizationally Unique Identifier),后三个字节(低位24位)由各厂家自行指派给生产的适配器接口,称为扩展标识符(唯一性)。
MAC地址在网络上用来区分设备的唯一性,接入网络的设备都有一个MAC地址,他们肯定都是不同的,是唯一的。一部iPhone上可能有多个MAC地址,包括WIFI的、SIM的等,但是iTouch和iPad上就有一个WIFI的,因此只需获取WIFI的MAC地址就好了,也就是en0的地址。
形象的说,MAC地址就如同我们身份证上的身份证号码,具有全球唯一性。这样就可以非常好的标识设备唯一性,类似与苹果设备的UDID号,通常的用途有:
1)用于一些统计与分析目的,利用用户的操作习惯和数据更好的规划产品;
2)作为用户ID来唯一识别用户,可以用游客身份使用app又能在服务器端保存相应的信息,省去用户名、密码等注册过程。

2.如何使用Mac地址生成设备的唯一标识呢?

主要分三种:
1、直接使用“MAC Address”
2、使用“MD5(MAC Address)”
3、使用“MD5(Mac Address+bundle_id)”获得“机器+应用”的唯一标识(bundle_id 是应用的唯一标识)

iOS7之前,因为Mac地址是唯一的, 一般app开发者会采取第3种方式来识别安装对应app的设备。为什么会使用它?在iOS5之前,都是使用UDID的,后来被禁用。苹果推荐使用UUID 但是也有诸多问题,从而使用MAC地址。而MAC地址跟UDID一样,存在隐私问题,现在苹果新发布的iOS7上,如果请求Mac地址都会返回一个固定值,那么Mac Address+bundle_id这个值大家的设备都变成一致的啦,跟UDID一样相当于被禁用, 所以Mac Address 是不能够被使用为获取设备唯一标识的。

五.广告标示符(IDFA-identifierForIdentifier)

广告标示符,在同一个设备上的所有App都会取到相同的值,是苹果专门给各广告提供商用来追踪用户而设的。但好在Apple默认是允许追踪的,而且一般用户都不知道有这么个设置,所以基本上用来监测推广效果,是戳戳有余了。
它是iOS 6中另外一个新的方法,提供了一个方法advertisingIdentifier,通过调用该方法会返回一个NSUUID实例,最后可以获得一个UUID,由系统存储着的。

#import <AdSupport/AdSupport.h>
    NSString *adId = [[[ASIdentifierManager sharedManager] advertisingIdentifier] UUIDString];

不过即使这是由系统存储的,但是有几种情况下,会重新生成广告标示符。如果用户完全重置系统((设置程序 -> 通用 -> 还原 -> 还原位置与隐私) ,这个广告标示符会重新生成。另外如果用户明确的还原广告(设置程序-> 通用 -> 关于本机 -> 广告 -> 还原广告标示符) ,那么广告标示符也会重新生成。
关于广告标示符的还原,有一点需要注意:如果程序在后台运行,此时用户“还原广告标示符”,然后再回到程序中,此时获取广 告标示符并不会立即获得还原后的标示符。必须要终止程序,然后再重新启动程序,才能获得还原后的广告标示符。
所以IDFA也不可以作为获取唯一标识的方法,来识别用户。

六.Vendor标示符 (IDFV-identifierForVendor)

Vendor标示符,是给Vendor标识用户用的,每个设备在所属同一个Vender的应用里,都有相同的值。其中的Vender是指应用提供商,但准确点说,是通过BundleID的反转的前两部分进行匹配,如果相同就是同一个Vender,例如对于com.taobao.app1, com.taobao.app2 这两个BundleID来说,就属于同一个Vender,共享同一个IDFV的值。和IDFA不同的是,IDFV的值是一定能取到的,所以非常适合于作为内部用户行为分析的主id,来标识用户,替代OpenUDID。
它是iOS 6中新增的,跟advertisingIdentifier一样,该方法返回的是一个 NSUUID对象,可以获得一个UUID。如果满足条件“相同的一个程序里面-相同的vendor-相同的设备”,那么获取到的这个属性值就不会变。如果是“相同的程序-相同的设备-不同的vendor,或者是相同的程序-不同的设备-无论是否相同的vendor”这样的情况,那么这个值是不会相同的。

    NSString *strIDFV = [[[UIDevice currentDevice] identifierForVendor] UUIDString];

但是如果用户将属于此Vender的所有App卸载,则IDFV的值会被重置,即再重装此Vender的App,IDFV的值和之前不同。

七.推送token+bundle_id

推送token+bundle_id的方法:
1、应用中增加推送用来获取token
2、获取应用bundle_id
3、根据token+bundle_id进行散列运算

apple push token保证设备唯一,但必须有网络情况下才能工作,该方法并不是依赖于设备本身,而是依赖于apple push机制,所以当苹果push做出改变时, 你获取所谓的唯一标识也就随之失效了。所以此方法还是不可取的。

八. NSUUID, CFUUID, IDFA, IDFV获取的标识对比

首次运行

NSUUID:9D820D3A-4429-4918-97F7-A69588B388A4
CFUUID:80F961D0-1E6A-4ECD-A0A9-F58ED858FE20
IDFA:687E6A90-50A3-4424-871C-BE255D050AFD
IDFV:8E740A99-283B-4F6A-87EF-443FB7778488

二次运行

NSUUID:23AB8D3D-4F1D-45E2-8BD7-83B451125326
CFUUID:14DDBFCF-67A6-46B7-BB48-4EF2ADC5429F
IDFA:687E6A90-50A3-4424-871C-BE255D050AFD
IDFV:8E740A99-283B-4F6A-87EF-443FB7778488

卸载后, 重新安装运行

NSUUID:BD934F9C-B7EC-4BD1-B65E-964C66537CAB
CFUUID:29654DE0-AC93-40F9-98AB-1E10A271AF8D
IDFA:687E6A90-50A3-4424-871C-BE255D050AFD
IDFV:8E740A99-283B-4F6A-87EF-443FB7778488

重启后运行

NSUUID:82711557-3A17-4B82-8F18-09AADF9DD37B
CFUUID:FFBC73EC-CFBE-414C-870E-77C0714E0347
IDFA:687E6A90-50A3-4424-871C-BE255D050AFD
IDFV:8E740A99-283B-4F6A-87EF-443FB7778488

总结

说了这么多, 才发现原来没有一种方法是可行的。没错, 其实自从苹果废除UDID后, 就不能达到获取设备真正的唯一标识了。因为这些方法中导致获取的唯一标示产生改变的原因, 或是重新调用方法, 或是重启设备, 或是卸载应用, 或是还原某些标识, 或者刷新系统…
所以, 不能达到从根本上获取唯一标识, 我们只能做到尽可能接近。下面是我用过的方法。

如何正确的获取设备的唯一标识

我用的方法是将获取的UUID永久存储在设备的KeyChain中, 这个方法在应用第一次启动时, 将获取的UUID存储进KeyChain中, 每次取的时候, 检查本地钥匙串中有没有, 如果没有则需要将获取的UUID存储进去。当你重启设备, 卸载应用再次安装,都不影响, 只是当设备刷机时, KeyChain会清空, 才会消失, 才会失效。
不只是这一种方法, 你也可以保存除UUID之外,其他合适的标识, 但利用KeyChain去存储标识的方式应该是最接近的。

利用keyChain和UUID永久获得设备的唯一标识

开发者可以在应用第一次启动时调用一次,然后将该串存储起来,以便以后替代UDID来使用。但是,如果用户删除该应用再次安装时,又会生成新的字符串,所以不能保证唯一识别该设备。这就需要各路高手想出各种解决方案。所以,之前很多应用就采用MAC Address。但是现在如果用户升级到iOS7(及其以后的苹果系统)后,他们机子的MAC Address就是一样的,没办法做区分,只能弃用此方法,重新使用UUID来标识。如果使用UUID,就要考虑应用被删除后再重新安装时的处理。

什么是钥匙串?

一、在应用间利用KeyChain共享数据
我们可以把KeyChain理解为一个Dictionary,所有数据都以key-value的形式存储,可以对这个Dictionary进行add、update、get、delete这四个操作。对于每一个应用来说,KeyChain都有两个访问区,私有区和公共区。私有区是一个sandbox,本程序存储的任何数据都对其他程序不可见。而要想在将存储的内容放在公共区,需要先声明公共区的名称,官方文档管这个名称叫“keychain access group”,声明的方法是新建一个plist文件,名字随便起,内容如下:

这里写图片描述

“yourAppID.com.yourCompany.whatever”就是你要起的公共区名称,除了whatever字段可以随便定之外,其他的都必须如实填写。这个文件的路径要配置在 Project->build setting->Code Signing Entitlements里,否则公共区无效,配置好后,须用你正式的证书签名编译才可通过,否则xcode会弹框告诉你code signing有问题。所以,苹果限制了你只能同公司的产品共享KeyChain数据,别的公司访问不了你公司产品的KeyChain。

二、保存私密信息
iOS的keychain服务提供了一种安全的保存私密信息(密码,序列号,证书等)的方式,每个ios程序都有一个独立的keychain存储。相对于NSUserDefaults、文件保存等一般方式,keychain保存更为安全,而且keychain里保存的信息不会因App被删除而丢失,所以在重装App后,keychain里的数据还能使用。

实现代码

首先, 我先推荐两篇文章,里面介绍了如利用keyChain和UUID永久获得设备的唯一标识, Classes/KeychainItemWrapper.m如何使用KeyChain保存和获取UDID
然后, 再介绍下我使用的方法以及封装的工具类, 在应用里使用使用keyChain,我们需要导入Security.framework。下面介绍下, 我在其他库基础上封装的一个获取唯一标识的工具类:

#import <Foundation/Foundation.h>
#import <Security/Security.h>

NSString * const KEY_UDID_INSTEAD = @"com.myapp.udid.test";

@interface LZKeychain : NSObject

/**
 本方法是得到 UUID 后存入系统中的 keychain 的方法
 不用添加 plist 文件
 程序删除后重装,仍可以得到相同的唯一标示
 但是当系统升级或者刷机后,系统中的钥匙串会被清空,此时本方法失效
 */
+(NSString *)getDeviceIDInKeychain;

@end

#import "LZKeychain.h"

@implementation LZKeychain

+(NSString *)getDeviceIDInKeychain
{
    NSString *getUDIDInKeychain = (NSString *)[LZKeychain load:KEY_UDID_INSTEAD];
    NSLog(@"从keychain中获取到的 UDID_INSTEAD %@",getUDIDInKeychain);
    if (!getUDIDInKeychain ||[getUDIDInKeychain isEqualToString:@""]||[getUDIDInKeychain isKindOfClass:[NSNull class]]) {
        CFUUIDRef puuid = CFUUIDCreate( nil );
        CFStringRef uuidString = CFUUIDCreateString( nil, puuid );
        NSString * result = (NSString *)CFBridgingRelease(CFStringCreateCopy( NULL, uuidString));
        CFRelease(puuid);
        CFRelease(uuidString);
        NSLog(@"\n \n \n _____重新存储 UUID _____\n \n \n  %@",result);
        [LZKeychain save:KEY_UDID_INSTEAD data:result];
        getUDIDInKeychain = (NSString *)[LZKeychain load:KEY_UDID_INSTEAD];
    }
    NSLog(@"最终 ———— UDID_INSTEAD %@",getUDIDInKeychain);
    return getUDIDInKeychain;
}

#pragma mark - private

+ (NSMutableDictionary *)getKeychainQuery:(NSString *)service {
    return [NSMutableDictionary dictionaryWithObjectsAndKeys:
            (id)kSecClassGenericPassword,(id)kSecClass,
            service, (id)kSecAttrService,
            service, (id)kSecAttrAccount,
            (id)kSecAttrAccessibleAfterFirstUnlock,(id)kSecAttrAccessible,
            nil];
}

+ (void)save:(NSString *)service data:(id)data {
    //Get search dictionary
    NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
    //Delete old item before add new item
    SecItemDelete((CFDictionaryRef)keychainQuery);
    //Add new object to search dictionary(Attention:the data format)
    [keychainQuery setObject:[NSKeyedArchiver archivedDataWithRootObject:data] forKey:(id)kSecValueData];
    //Add item to keychain with the search dictionary
    SecItemAdd((CFDictionaryRef)keychainQuery, NULL);
}

+ (id)load:(NSString *)service {
    id ret = nil;
    NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
    //Configure the search setting
    //Since in our simple case we are expecting only a single attribute to be returned (the password) we can set the attribute kSecReturnData to kCFBooleanTrue
    [keychainQuery setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];
    [keychainQuery setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit];
    CFDataRef keyData = NULL;
    if (SecItemCopyMatching((CFDictionaryRef)keychainQuery, (CFTypeRef *)&keyData) == noErr) {
        @try {
            ret = [NSKeyedUnarchiver unarchiveObjectWithData:(NSData *)keyData];
        } @catch (NSException *e) {
            NSLog(@"Unarchive of %@ failed: %@", service, e);
        } @finally {
        }
    }
    if (keyData)
        CFRelease(keyData);
    return ret;
}

+ (void)delete:(NSString *)service {
    NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
    SecItemDelete((CFDictionaryRef)keychainQuery);
}
@end

参考资料:
The Developer’s Guide to Unique Identifiers
获取设备的唯一标识

作者:wangyanchang21 发表于2016/11/7 17:38:23 原文链接
阅读:64 评论:0 查看评论

洲新城 IT教育 李赞红老师 第一章节

$
0
0

摘要 : 初学者一枚,每一次写的时候,都是从网上直接 复制+粘贴。。完全不为什么要这样写,为什么通过这样就可以实现,也想着要去看懂。可是看一下就不知道怎么下手。完全是一个球形,无法下手。有一些通过各种搜索后,能够知道了表皮意思,可以接下来自己动手的时。就又忘记了怎么下手。希望各位大大介绍一下大致的方向,再此感激不尽。后面考虑一下还是直接做笔记吧!每一去搜索慢慢的收藏也多了。完全找不到自己所需要的在哪里去了,又得重新搜索…

文摘摘抄至:株洲新城 IT教育 李赞红老师。非常感谢老师。想过摘抄一边的方式去让自己记住一些知识点。如果不适合,我会立刻删除

第一章 View的绘图流程


1.1、概述

  Android中组件必须是View直接子类或间接子类,其中View有一个名为ViewGroup的子类。用于定义容器组件类(FrameLayout、LinearLayout都是是ViewGroup的子类)。二者的职责定义非常清晰,如果组件中还有子组件。就一定是从ViewGroup类继承,否则从View类继承。

  View 类定义了组件相关的通用功能,并打通了组件在Activity整个活动周期中的绘制流程和效果等任督二脉。通过 OOP构建出基本的运行框架。

1.2、Activity 的组成结构

  Activity 代表一个窗口,事实上,这里的“窗口”是由 Activity的成员变量 mWindow来表示的,mWindow本质上是一个PhoneWindow 对象。PhoneWindow继承至 Window抽象类。负责窗口的管理。但是,PhoneWindow 并不是用来呈现界面效果,呈现界面是由 PhoneWindow 管理的 DecorView对象来完成的。DecorView 类是 FrameLayout的子类,也是整个 View树的“根”。DecorView由三部分构成:ActionBar、标题区和内容区。在 源码 platforms/android-21/data/res/layout 的目录下有一个名为 screen_title.xml 的布局文件,该布局文件是常见的窗口风格定义文件,打开后可以看到如下定义:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:fitsSystemWindows="true">
    <!-- Popout bar for action modes -->
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
    <FrameLayout
        android:layout_width="match_parent" 
        android:layout_height="?android:attr/windowTitleSize"
        style="?android:attr/windowTitleBackgroundStyle">
        <TextView android:id="@android:id/title" 
            style="?android:attr/windowTitleStyle"
            android:background="@null"
            android:fadingEdge="horizontal"
            android:gravity="center_vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </FrameLayout>
    <FrameLayout android:id="@android:id/content"
        android:layout_width="match_parent" 
        android:layout_height="0dip"
        android:layout_weight="1"
        android:foregroundGravity="fill_horizontal|top"
        android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
  从代码真看出,ActionBar 由 ViewStub标签定义,内容区包含了两个 FrameLayout标签,分别代表标题栏和正文区。id为 @android:id/content 的FrameLayout被 inflate 成名为 mContentParent 的FrameLayout对象。在 Activity的 onCreate() 方法中调用 setContentView方法加载的布局终将转化成 mContentParent 的子 View。
下图所示描述上面各组件之间的关系:

从上图可以看出:

  • Activity 类似于一个框架,负责容器的生命周期及活动,窗口通过 Window 来管理
  • Window 负责窗口管理(实际是子类 PhoneWindow),窗口的绘制和渲染交给 DecorView完成
  • DecorView 是 View的数的根,我们为 Activity定义的 layout 将转成 DecorView的子类视图 ContentParent 的子视图
  • layout.xml是我们定义的布局文件,最终 inflate为 DecorView的子组件

  需要说明的是,PhoneWindow 类还关联一个 名为 mWindowManager 的 windowmanager对象,windowmanager 会创建一个 ViewRootImpl 对象来和 WindowManagerService 进行沟通,windowmanagerservice 能获取触摸事件、键盘事件和轨迹球事件,并通过 ViewRootImpl 将事件分发给各个 Activity。另外,ViewRootImpl 还负责Activity 整个 GUI的绘制。

下图所示是 Activity涉及到各个组件的关系图(来源于网络)

1.3、View 树的绘制流程

  上文提到,ViewRootImpl负责 Activity整个 GUI的绘制,而绘制是从 ViewRootImpl的 performTraversals()方法开始。该方法是由 private 修饰,控制着 View树的绘制流程,禁止被重写。

  通过查看 ViewRootImpl类,在performTraversals()中,提取出三行关键代码

private void performTraversals(){
......
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
......
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
......
performDraw();
......

}
  • performMeasure()方法测量组件的大小
  • performLayout()方法用于子组件的定位(放在窗口的什么位置)
  • performDraw()方法就是将组件的外观绘制出来

1.3.1 测量组件大小

  performMeasure() 方法负责组件的自身尺寸的测量,在layout 布局文件中,每一个组件都必须设置 layout_width 和 layout_height属性,属性值有三种可选模式:wrap_content、match_parent 和 数值。preformMeasure() 方法根矩设置的模式计算出组件的宽度和高度。事实上,大多数情况下模式为 match_parent 和 数值的时候不需要计算的,传过来的就是父容器自己计算好的尺寸或是一个指定的精确值,只有当模式是 wrap_content 的时候,才需要根矩内容进行尺寸的测量。

prefromMeasure()方法的源码摘录如下:

    private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

  对象 mView是 View树的根视图,代码中调用了 mView的 measure()方法,我们进入该方法的源代码如下:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
......
onMeasure(widthMeasureSpec, heightMeasureSpec);
......
}

  忽略了其与代码,只剩下了 onMeasure()这一行,onMeasure()方法是为组件尺寸的测量留的功能接口。当然,也定义了默认的实现,默认的实现并没有太多的意义,在绝大部分情况下,onMEasure()方法必须重写。

  如果测量的是容器的尺寸,而容器的尺寸有依赖于子组件的大小,所以必须先测量容器中子类组件的大小。不然,测量出来的宽度和高度永远为 0.编程的时候往往容易忽略。

  Android 中使用的单词 measure来计算组件的大小,背后其实颇有讲究。measure是“测量、评定”之意。说明其结果只能起参考作用,并不是一定非使用该值不可。组件真正的大小最终是由setFrame()方法决定的,该方法一般情况下回参考 measure出来的尺寸。

1.3.2 确定子组件的位置

preformLayout()方法用于确定子组件的位置。所以,该方法只针对 ViewGroup容器类。作为容器,必须为容器中的子类 View精确定义位置和大小。该方法源码如下:

private void performLayout(WindowManager.LayoutParams lp,
int desiredWindowWidth, int desiredWindowHeight){

......
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
......
for (int i = 0; i < numValidRequests; ++i) {
    final View view = validLayoutRequesters.get(i);
    view.requestLayout();
    }
}

  代码中的 host是 View树中根视图(DecorView),也就是最外层容器,容器的位置安排在左上角(0,0),其大小默认会填满 mContentParent容器。我们重点来看一下 layout()方法源码:

    public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;

        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);


            onLayout(changed, l, t, r, b);

            ......
        }
    ......
    }

  在layout()方法中,在定位之前如果需要重新测量组件的大小,则先调用 onMeasure()方法,接下来执行 setOpticalFrame()或 setFrame()方法确定自身的位置与大小,此时只是保存了相关的值,与具体的绘制无关。随后,onLayout()方法被调用,该方法是空方法,如下:

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }

  onLayout()方法在这里的作用是当前组件为容器时,负责定位容器中的子组件。这其实是一个递归的过程,如果子组件也是一个容器,该容器依然负责它的子组件的定位。依此类推,直到所有的组件都定位完成为止。也就是说:“从顶层的DecorView开始定位,像多米罗骨牌一样从上往下驱动,最后每一个组件都放到它对应该出现的位置上。”onLayout()方法和上节的 onMeasure()方法一样,是为开发人员预留的功能扩展接口,自定义容器时,该方法必须重写。

1.3.3 绘制组件

  preformDraw()方法执行执行组件的绘制功能,组件的绘制是一个十分复杂的过程。不仅仅绘制组件本身,还要绘制背景、滚动条,好消息是每一个组件只需要负责自身的绘制。而且一般来说,容器组件不需要绘制,ViewGroup已经做了大量的工作。通过源码整理出的绘图流程如下:

   private void performDraw() {

        ......
        final boolean fullRedrawNeeded = mFullRedrawNeeded;
        mFullRedrawNeeded = false;

        mIsDrawing = true;
         .......
        try {
            draw(fullRedrawNeeded);
        } finally {
            mIsDrawing = false;
          ......
        }
    ......
    }

  在performDraw()方法中调用 draw()方法

private void draw(boolean fullRedrawNeeded) {

    ......
    if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
                    return;
                }
    ......
}

  draw()方法又调用了 drawSoftware()方法

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty) {

    final Canvas canvas;
    try {
            final int left = dirty.left;
            final int top = dirty.top;
            final int right = dirty.right;
            final int bottom = dirty.bottom;

            canvas = mSurface.lockCanvas(dirty);
            ........


    if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {
                canvas.drawColor(0, PorterDuff.Mode.CLEAR);
            }
            ......

    mView.draw(canvas);
    ......
    surface.unlockCanvasAndPost(canvas);
    ......
}

  如果说前面的代码倍感陌生,那么从 drawSoftware()开始,代码似乎越来越平易近人越来越接地气了。绘制组件是通过 Canvas类完成的,该类定义了若干个绘制图像的方案。通过 Paint类配置绘制参数,便能绘制出各种图案效果。为了提高绘图的性能,使用了 Surface技术,sureface提供了一套双缓存机制,能大大的加快绘图效率。而我们绘图是需要的 Canvas对象也由是 Surface创建的。

  drawSoftware()方法中调用了 mView的 draw()方法。前面说过,mView是 ACtivity界面中 View树的根(DecorView),也是一个容器(具体来说就是一个FrameLayout布局容器)。所以,我们来看看 FrameLayout类的 draw()方法源码:

public void draw(Canvas canvas) {
    super.draw(canvas);
    ......
    final Drawable foreground = mForeground;
    ......
    foreground.draw(canvas);
}

  FrameLayout类的 draw()方法做了两件事情,一是调用谷类的 draw()方法绘制自己;二是将前景位图画在 Canvas上,自然,super.draw(canvas)语句是我们关注重点,FrameLayout继承自 ViewGroup,遗憾的是 ViewGroup并没有重写 draw()方法,也就是说,ViewGroup的绘制完全重用了它的父类 View的 draw()方法。不过,ViewGroup中定义了一个名为 dispatchDraw()的方法。该方法在 View中定义,在 ViewGroup中实现。至于有什么用? 暂且不说,我们先扒开 View的 drwa()方法源码看看:

public void draw(Canvas canvas) {

    ......
    drawBackground(Canvas canvas)

    ......
    if (!dirtyOpaque) onDraw(canvas);

    ......
    dispatchDraw(canvas);

    onDrawScrollBars(canvas);

    ......

}

View 类的 draw()方法是组件绘制的核心方法,主要做了下面几件事情:

  • 绘制背景:background.draw(canvas)
  • 绘制自己 :onDraw(canvas)
  • 绘制子视图 :dispatchDraw(canvas);
  • 绘制滚动条 :onDrawScrollBars(canvas);

  backgroud是一个 Drawable对象,直接绘制在 Canvas上,并且与组件要绘制的内容互补干扰。跟多时候,这个特征能被某些场景利用。比如后面的“刮刮乐”就是一个很好的范例。

  View 只是组件的抽象定义,它自己并不知道自己长神马样子。所以,View定义了一个空 onDraw(),如下 :

/**
     * Implement this to do your drawing.
     *
     * @param canvas the canvas on which the background will be drawn
     */
    protected void onDraw(Canvas canvas) {
    }

  和前面的 onMeasure() 与 onLayout()一样,onDraw()方法同样是预留给子类扩展的功能接口。用于绘制组件自身,组件的外观有该方法来决定。

dispatchDraw()方法也是一个空方法,如下:

/**
     * Called by draw to draw the child views. This may be overridden
     * by derived classes to gain control just before its children are drawn
     * (but after its own view has been drawn).
     * @param canvas the canvas on which to draw the view
     */
    protected void dispatchDraw(Canvas canvas) {

    }

  该方法服务容器组件,容器中的子组件必须通过 dispatchDraw()方法进行绘制。所以,View虽然没有实现该方法但是它的子类 ViewGroup实现了该方法。

protected void dispatchDraw(Canvas canvas) {
......
    final int count = mChildrenCount;
    final View[] children = mChildren;
        for (int i = 0; i < count; i++) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
        }
    }
......
}

  在 dispatchDraw()方法中,循环遍历每一个子组件,并用 drawChild()方法绘制子组件。而子组件有调用 View的 draw()方法绘制自己。

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }

  组件的绘制委员会寺一个递归的过程,说到底 Activity的 UI界面的根一定是容器,根容器绘制结束后开始绘制子组件。子组件如果是容器继续往下递归绘制,直到说有的组件正确绘制为止。否则直接将子组件绘制出来。

总体来说,UI界面的绘制从开始到结束要经历的几个过程:

  • 测量组件大小,回调 onMeasure()方法
  • 组件定位,回调 onLayout()方法
  • 组件绘制,回调 onDraw()方法
作者:guyuelin123 发表于2016/11/7 17:40:11 原文链接
阅读:25 评论:0 查看评论

android 自定义view之选座功能

$
0
0

效果图:
这里写图片描述

界面比较粗糙,主要看原理。

这个界面主要包括以下几部分
1、座位
2、左边的排数
3、左上方的缩略图
4、缩略图中的红色区域
5、手指移动时跟随移动
6、两个手指缩放时跟随缩放

主要技术点
1、矩阵Matrix
2、GestureDetector与ScaleGestureDetector
3、Bitmap的一下基本用法
4、这里只需要重写view的onDraw就可实现全部功能

可以发现这个其实没什么难度,主要就是一些位置的计算。

为了能便于理解首先把要用到的知识点进行一下梳理

1、矩阵Matrix

Matrix由3*3矩阵中9个值来决定,我们对Matrix的所有设置, 就是对这9个值的操作。
{MSCALE_X,MSKEW_X,MTRANS_X,
MSKEW_Y,MSCALE_Y,MTRANS_Y,
MPERSP_0,MPERSP_1,MPERSP_2}

这是矩阵的9个值,看名字也知道他们是什么意思了。

这里主要用到缩放和平移,下面以缩放为例来了解一下缩放的控制
通过android提供的api我们可以调用setScale、preScale、postScale来改变MSCALE_X和MSCALE_Y的值达到缩放的效果

所以只要理解setScale、preScale、postScale这三个方法的区别我们就可以简单的进行缩放控制了

1、setScale(sx,sy),首先会将该Matrix设置为对角矩阵,即相当于调用reset()方法,然后在设置该Matrix的MSCALE_X和MSCALE_Y直接设置为sx,sy的值
2、preScale(sx,sy),不会重置Matrix,而是直接与Matrix之前的MSCALE_X和MSCALE_Y值结合起来(相乘),M’ = M * S(sx, sy)。
3、postScale(sx,sy),不会重置Matrix,而是直接与Matrix之前的MSCALE_X和MSCALE_Y值结合起来(相乘),M’ = S(sx, sy) * M。

这么说其实也有些不好理解,举个栗子一看就明白

1、pre….的执行顺序

        Matrix matrix=new Matrix();
        float[] points=new float[]{10.0f,10.0f};
        matrix.preScale(2.0f, 3.0f);
        matrix.preTranslate(8.0f,7.0f);
        matrix.mapPoints(points);
        Log.i("test", points[0]+"");
        Log.i("test", points[1]+"");

结果为点坐标为(36.0,51.0)
可以得出结论,进行变换的顺序是先执行preTranslate(8.0f,7.0f),在执行的preScale(2.0f,3.0f)。即对于一个Matrix的设置中,所有pre….是倒着向后执行的。

2、post…的执行顺序

        Matrix matrix=new Matrix();
        float[] points=new float[]{10.0f,10.0f};
        matrix.postScale(2.0f, 3.0f);
        matrix.postTranslate(8.0f,7.0f);
        matrix.mapPoints(points);
        Log.i("test", points[0]+"");
        Log.i("test", points[1]+"");

结果为点坐标为(28.0,37.0)
可以得出结论,进行变换的顺序是先执行postScale(2.0f,3.0f),在执行的postTranslate(8.0f,7.0f)。即对于一个Matrix的设置中,所有post….是顺着向前执行的。

这里主要知道set…和post…方法就行,因为只用到了这两个。
自我理解其实和scrollTo、scrollBy类似。

2、GestureDetector与ScaleGestureDetector

GestureDetector主要用于识别一些特定手势,只要调用GestureDetector.onTouchEvent()把MotionEvent传递进去就可以了
ScaleGestureDetector用于处理缩放的攻击类用法和GestureDetector类似

3、Bitmap的一下基本用法
参考: 关于bitmap你不知道的一些事

了解一下bitmap的注意事项即可

下面开始正式画这个选座的功能了

1、画座位:

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        /**
         * 如果第一次进入 使座位图居中
         */
        if (mViewH != 0 && mViewW != 0&&isFrist) {
            isFrist = false;
            matrix.setTranslate(-(mViewW-getMeasuredWidth())/2, 0);
        }
        /**
         * 画座位
         */
        drawSeat(canvas);
        /**
         * 画排数
         */
        drawText(canvas);
        /**
         * 画缩略图
         */
        drawOverView(canvas);
        /**
         * 画缩略图选择区域
         */
        drawOvewRect(canvas);

    }
private void drawSeat(Canvas canvas) {
        float zoom = getMatrixScaleX();
        scale1 = zoom;
        tranlateX = getTranslateX();
        tranlateY = getTranslateY();
        /**
         * 使用两层for循环来画出所有座位
         */
        for (int i = 0; i < row; i++) {
            float top = i * SeatHight * scale * scale1 + i * mSpaceY * scale1
                    + tranlateY;
            for (int j = 0; j < column; j++) {

                float left = j * SeatWidth * scale * scale1 + j * mSpaceX
                        * scale1 + tranlateX;

                tempMatrix.setTranslate(left, top);
                tempMatrix.postScale(scale, scale, left, top);
                tempMatrix.postScale(scale1, scale1, left, top);


                /**
                 * 获取每个位置的信息
                 */
                int state = getSeatType(i, j);
                /**
                 * 根据位置信息画不同的位置图片
                 */
                switch (state) {
                case SEAT_TYPE_SOLD:
                    canvas.drawBitmap(SeatLock, tempMatrix, null);
                    break;
                case SEAT_TYPE_SELECTED:
                    canvas.drawBitmap(SeatChecked, tempMatrix, null);
                    break;
                case SEAT_TYPE_AVAILABLE:
                    canvas.drawBitmap(SeatNormal, tempMatrix, null);
                    break;
                case SEAT_TYPE_NOT_AVAILABLE:
                    break;

                }
            }
        }

    }

这里其实没什么难度,主要就是使用两层for循环,一层画行,一层画列

另外要注意的就是当前位置的计算 top = (当前位置i)(座位图标大小SeatHight 它本身的缩放比scale*缩放时的缩放比scale1)+(当前位置i* 垂直方向的间距mSpaceY*缩放时的缩放比scale1)+垂直方向移动是的移动距离

2、画排数

private void drawText(Canvas canvas) {
        mTextPaint.setColor(bacColor);
        RectF rectF = new RectF();
        rectF.top = getTranslateY() - mNumberHeight/2;
        rectF.bottom = getTranslateY()+ mViewH* getMatrixScaleX() + mNumberHeight/2;
        rectF.left = 0;
        rectF.right = mTextWidth;


        canvas.drawRoundRect(rectF, mTextWidth/2, mTextWidth/2, mTextPaint);
        mTextPaint.setColor(Color.WHITE);
         for (int i = 0; i < row; i++) {
             float top = (i *SeatHight*scale + i * mSpaceY) * getMatrixScaleX() + getTranslateY();
             float bottom = (i * SeatHight*scale + i * mSpaceY + SeatHight) * getMatrixScaleX() + getTranslateY();
             float baseline = (bottom + top  - lineNumberPaintFontMetrics.bottom - lineNumberPaintFontMetrics.top ) / 2-6;
             canvas.drawText(lineNumbers.get(i), mTextWidth / 2, baseline, mTextPaint);
          }     
    }

3、画缩略图

private void drawOverView(Canvas canvas) {
        /**
         * 1、先画张背景图片
         */
        mBitMapOverView = Bitmap.createBitmap((int)mOverViewWidth,(int)mOverViewHight,Bitmap.Config.ARGB_8888);
        Canvas OverViewCanvas = new Canvas(mBitMapOverView);
        Paint paint = new Paint();
        paint.setColor(bacColor);
        scaleoverX = mOverViewWidth / mViewW;
        scaleoverY = mOverViewHight / mViewH;
        float tempX = mViewW * scaleoverX;
        float tempY = mViewH * scaleoverY;
        OverViewCanvas.drawRect(0, 0, (float)tempX, (float)tempY, paint);

        Matrix tempoverMatrix = new Matrix();
        /**
         * 2、和画座位图一样在缩略图中画座位
         */
        for (int i = 0; i < row; i++) {
            float top =  i * SeatHight * scale * scaleoverY+ i * mSpaceY * scaleoverY;
            for (int j = 0; j < column; j++) {
                float left = j * SeatWidth * scale * scaleoverX + j * mSpaceX * scaleoverX+mTextWidth*scaleoverX;
                tempoverMatrix.setTranslate(left, top);
                tempoverMatrix.postScale(scale*scaleoverX, scale*scaleoverY, left, top);

                int state = getSeatType(i, j);
                switch (state) {
                case SEAT_TYPE_SOLD:
                    OverViewCanvas.drawBitmap(SeatLock, tempoverMatrix, null);
                    break;
                case SEAT_TYPE_SELECTED:
                    OverViewCanvas.drawBitmap(SeatChecked, tempoverMatrix, null);
                    break;
                case SEAT_TYPE_AVAILABLE:
                    OverViewCanvas.drawBitmap(SeatNormal, tempoverMatrix, null);
                    break;
                case SEAT_TYPE_NOT_AVAILABLE:
                    break;

                }


            }
        }

        canvas.drawBitmap(mBitMapOverView,0,0,null);

    }

4、缩略图中的红色区域

private void drawOvewRect(Canvas canvas) {

        OverRectPaint = new Paint();
        OverRectPaint.setColor(Color.RED);
        OverRectPaint.setStyle(Paint.Style.STROKE);
        OverRectPaint.setStrokeWidth(overRectLineWidth);
        int tempViewW ;
        int tempViewH;
        if(getMeasuredWidth()<mViewW){
            tempViewW = getMeasuredWidth();
        }else{
            tempViewW = mViewW;
        }
        if(getMeasuredHeight()<mViewH){
            tempViewH = getMeasuredHeight();
        }else{
            tempViewH = mViewH;
        }

        try{
            Rect rect ;
            if(getMatrixScaleX()>= 1.0f){
                 rect = new Rect((int)(scaleoverX*Math.abs(getTranslateX())/getMatrixScaleX()), 
                                     (int)(scaleoverY*Math.abs(getTranslateY())/getMatrixScaleX()),
                                     (int)(scaleoverX*Math.abs(getTranslateX())/getMatrixScaleX()+tempViewW*scaleoverX/getMatrixScaleX()),
                                     (int)(scaleoverY*Math.abs(getTranslateY())/getMatrixScaleX()+tempViewH*scaleoverY/getMatrixScaleX()));
            }else{
                 rect = new Rect((int)(scaleoverX*Math.abs(getTranslateX())), 
                         (int)(scaleoverY*Math.abs(getTranslateY())),
                         (int)(scaleoverX*Math.abs(getTranslateX())+tempViewW*scaleoverX),
                         (int)(scaleoverY*Math.abs(getTranslateY())+tempViewH*scaleoverY));
            }
        canvas.drawRect(rect, OverRectPaint);
        }catch(Exception e){
            e.printStackTrace();
        }   
    }

5、手指移动时跟随移动

@Override
    public boolean onTouchEvent(MotionEvent event) {

        /**
         * 缩放事件交由ScaleGestureDetector处理
         */
        scaleGestureDetector.onTouchEvent(event);
        /**
         * 移动和点击事件交由GestureDetector处理
         */
        gestureDetector.onTouchEvent(event);

        return true;
    }
/**
                 * 移动事件 这里只是简单判断了一下,需要更细致的进行条件判断
                 */
                public boolean onScroll(MotionEvent e1, MotionEvent e2,
                        float distanceX, float distanceY) {
                    float tempMViewW = column * SeatWidth*scale*scale1+(column -1)*mSpaceX*scale1+mTextWidth-getWidth();
                    float tempmViewH = row * SeatHight * scale * scale1 + (row -1) * mSpaceY * scale1 - getHeight();


                    if((getTranslateX()>mTextWidth+mSpaceX)&& distanceX<0){
                        distanceX = 0.0f;
                    }
                    if((Math.abs(getTranslateX())>tempMViewW)&&(distanceX>0)){
                        distanceX = 0.0f;
                    }


                    if((getTranslateY()>0)&&distanceY<0){
                        distanceY=0.0f;
                    }
                    if((Math.abs(getTranslateY())>tempmViewH)&&(distanceY>0)){
                        distanceY = 0.0f;
                    }                   
                    matrix.postTranslate(-distanceX, -distanceY);               
                    invalidate();
                    return false;

                }
                /**
                 * 单击事件
                 */
                public boolean onSingleTapConfirmed(MotionEvent e) {
                    int x = (int) e.getX();
                    int y = (int) e.getY();

                    for (int i = 0; i < row; i++) {
                        for (int j = 0; j < column; j++) {
                            int tempX = (int) ((j * SeatWidth * scale + j * mSpaceX) * getMatrixScaleX() + getTranslateX());
                            int maxTemX = (int) (tempX + SeatWidth * scale * getMatrixScaleX());

                            int tempY = (int) ((i * SeatHight * scale + i * mSpaceX) * getMatrixScaleY() + getTranslateY());
                            int maxTempY = (int) (tempY + SeatHight * scale * getMatrixScaleY());


                            if (x >= tempX && x <= maxTemX && y >= tempY
                                    && y <= maxTempY) {
                                int id = getID(i, j);
                                int index = isHave(id);
                                if (index >= 0) {
                                    remove(index);
                                } else {
                                    addChooseSeat(i, j);


                                }
                                float currentScaleY = getMatrixScaleY();
                                if (currentScaleY < 1.7f) {
                                    scaleX = x;
                                    scaleY = y;
                                    /**
                                     * 选中时进行缩放操作
                                     */
                                    zoomAnimate(currentScaleY, 1.9f);
                                }
                                invalidate();
                                break;

                            }
                        }
                    }

                    return super.onSingleTapConfirmed(e);
                }
            });

6、两个手指缩放时跟随缩放

public boolean onScale(ScaleGestureDetector detector) {
                    float scaleFactor = detector.getScaleFactor();
                    //scaleX = detector.getCurrentSpanX();
                    //scaleY = detector.getCurrentSpanY();
                    //直接判断大于2会导致获取的matrix缩放比例继续执行一次从而导致变成2.000001之类的数从而使
                    //判断条件一直为真从而不会执行缩小动作
                    //判断相乘大于2 可以是当前获得的缩放比例即使是1.9999之类的数如果继续放大即使乘以1.0001也会比2大从而
                    //避免上述问题。

                     if (getMatrixScaleY() * scaleFactor > 2) {
                            scaleFactor = 2 / getMatrixScaleY();
                      }
                      if (getMatrixScaleY() * scaleFactor < 0.8) {
                            scaleFactor = 0.8f / getMatrixScaleY();
                      }
                    matrix.postScale(scaleFactor, scaleFactor);


                    invalidate();

                    return true;
                }

至此这个比较粗糙的选座功能就实现了,有时间会继续优化下细节问题。

下面两个demo都比较给力我就不上传demo了

主要参考:
Android例子源码高仿QQ电影票选座功能例子

andriod 打造炫酷的电影票在线选座控件,1比1还原淘宝电影在线选座功能

作者:asd1031 发表于2016/11/7 17:46:52 原文链接
阅读:50 评论:0 查看评论

OkHttp框架二次封装,post json格式的参数(上)

$
0
0

OkHttp框架二次封装,post json格式的参数(上)

请求的封装

本篇主要是针对后台数据格式,进行请求参数的封装。封装的目的,是为了配合后台的数据结构,方便客户端进行数据的请求和响应数据的处理。

数据请求的格式

数据请求的请求以Json格式传递参数到服务器,在本例中,参数分为了公参和私参,请求体结构如下:

{
    "args": {
        "pri_args": {
            "username": "xxxxxx",  
            "password": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 
        },
        "pub_args": {        
            "appversion": "xxx",   
            "imei": "xxxxxxxx",  
            "token": "xxxxxxxx",   
            "uid": xxxxxx 
        }
    }
}

在使用过程时,首先要做的,是对参数的拼接,将私有参数和公参分开来,对于整个APP来说,只要登录了,每次请求接口时,公参都是相对固定的,因此可以把公参隐藏起来,让用户不必每次请求都自己写公参传进去,只需要在app打开的时候给公参赋值,然后对于每个接口,只需要传私参即可。

说在前面的话

项目使用的是鸿洋大神封装好的Okhttp-utils框架,针对Json格式的数据请求,进行了二次封装,膜拜大神!下面是原来的框架地址:
博客地址:http://blog.csdn.net/lmj623565791/article/details/47911083

github:https://github.com/hongyangAndroid/okhttp-utils

遵循开闭原则,二次封装并没有对原来的框架动刀子,只是把原来的框架作为依赖,在此基础上,进行了二次封装而已。

【Look here】:由于后台定义的请求格式各有不同,返回字段也各有差异,args、pri_args、pub_args等这些字段都不会一样,所以本文章封装的框架可能不能直接拿来就用,但是可以参照我这边封装的,照葫芦画瓢,本篇只介绍自己封装时的想法和写过之后的经验,谈不上技术分享,大家酌情参考。

参数的拼接

虽然原来的Okhttp-utils里并没有直接对json格式参数的封装,但是有对String类型参数的封装方法postString,我们可以借用这个方法,然后在这个方法里再封装一层,达到的效果是,使用时,传进去的是Map

    OkHttpUtils.postString().url(THost.HOST).content(paramContent).build().execute(callback);

使用时传入url和content,然后可以看到postString实际上是返回了一个PostStringBuilder类型的对象实例.

OkHttpUtils.java

public static PostStringBuilder postString() {
    return new PostStringBuilder();
}

然后通过这个对象的build方法,传入url和content参数,生成了一个RequestCall对象。

PostStingBuilder.java

 public RequestCall build() {
    return (new PostStringRequest(this.url, this.tag, this.params, this.headers, this.content, this.mediaType, this.id)).build();
}

OkHttpRequest.java

public RequestCall build() {
    return new RequestCall(this);
}

最后调用execute方法来发送请求,并且给这个方法传了两个参数,一个是刚刚生成的RequestCall对象,另一个就是自己定义的Callback了。

 public void execute(final RequestCall requestCall, Callback callback)
{
    ......
}

我们要做的工作是:
1、给框架使用者提供方法,来传入Map类型的参数;
2、转换传入的参数,将其变成json string
3、将得到的json string 通过postString方法发送出去

无论是传参还是转换,都可以在Builder里去写:

我们可以以PostStringBuilder类为模板新建一个PostJsonBuilder类:

public class TPostJsonBuilder extends OkHttpRequestBuilder<TPostJsonBuilder> {

private String paramContent;
private MediaType mediaType;

private Map<String, Object> pri_args;
private Map<String, Object> pub_args;

public TPostJsonBuilder() {
    this.pri_args = new HashMap<>() ;
    this.pub_args = new HashMap<>() ;
}

public TPostJsonBuilder(String paramContent, Map<String, Object> pri_args, MediaType mediaType, Map<String, Object> pub_args) {
    this.paramContent = paramContent;
    this.pri_args = pri_args;
    this.mediaType = mediaType;
    this.pub_args = pub_args;
}

public TPostJsonBuilder pri_args(Map<String, Object> args) {
    this.pri_args = args  ;
    return  this;
}
public TPostJsonBuilder pub_args(Map<String, Object> args) {
    this.pub_args = args  ;
    return  this;
}

public TPostJsonBuilder paramContent(String content) {
    if (content != null) {
        this.paramContent = content;
    } else if (this.pri_args.size() != 0 && this.pub_args.size() != 0){
        this.paramContent = initParams(this.pri_args,this.pub_args) ;
    } else {
        Exceptions.illegalArgument("args cannot be null !");
    }
    return this;
}
//这个方法就是拼接参数的关键了,将传入的公参和私参拼接成json string
private String initParams(Map<String, Object> pri_args, Map<String, Object> pub_args) {
    Map<String,Map<String, Object>> priArgs = new HashMap<>();
    priArgs.put("pri_args",pri_args);

    Map<String,Map<String, Object>> pubArgs = new HashMap<>();
    pubArgs.put("pub_args",pub_args);

    Map<String,Map<String,Map<String, Object>>> args = new HashMap<>() ;
    //将pub_args追加到pri_args里
    priArgs.putAll(pubArgs);
    args.put("args",priArgs);
    Gson g = new Gson();
    String argsStr = g.toJson(args) ;

    return argsStr ;
}


public TPostJsonBuilder mediaType(MediaType mediaType)
{
    this.mediaType = mediaType;
    return this;
}
@Override
public RequestCall build() {
    paramContent = initParams(this.pri_args,this.pub_args) ;
    mediaType = MediaType.parse("application/json;charset=utf-8") ;
    return new PostStringRequest(url,tag,params,headers,paramContent,mediaType,id).build();
}
}

里面将参数分为私参pri_args和公参pub_args两部分,并且给这个两个参数暴露了返回值为TPostJsonBuilder对象的方法,这样使用者就可以通过.pri_args(pri).pub_args(pub)来传入参数;
然后在关键的initParams方法里,将私参和公参拼接成服务器所需要的形式,然后再转成json string,在build方法里通过PostStringRequest构建RequestCall。这样参数的封装就基本完成了。

然后就是给使用者暴露postJson方法了,我们写一个类继承自OkHttpUtils:

TOkhttpUtil.java

public class TOkhttpUtil extends OkHttpUtils {


public static void initPubParams(Map<String, Object> pub_args) {
    TPublicParam.publicparam = pub_args ;
}
public static void initHeadS(String s) {
    TPublicParam.headparam.put("s",s) ;
}

public TOkhttpUtil(OkHttpClient okHttpClient) {
    super(okHttpClient);
}

public static TPostJsonBuilder postJson() { return  new TPostJsonBuilder(); }

}

里面有一个方法postJson,用来返回一个TPostJsonBuilder对象。
还有两个方法,initPubParams和initHeadS,这两个方法就是对于公参和请求头的封装了。本例中,请求头里有2个字段,s字段相对不变,m字段每个接口都不同,所以可以先封装好s,下面会说到。

这样TOkhttUtil就可以这么使用了:

TOkhttpUtil.postJson().url(THost.HOST).pri_args(pri_args).pub_args(pub_args).build().execute(callback)

其中的pri_args和pub_args分别是要传的公参和私参,例如:

Map<String,Object> pri_args = new HashMap<>();
pri_args.put("username","张三");
pri_args.put("password","4f32d39b42v322vkj43");

Map<String,Object> pub_args = new HashMap<>();
pub_args.put("appversion","1.0.1");
pub_args.put("imei","0000-0033-3300-3232-9909-3333");
pub_args.put("token","4f32d39b42v322vkj43");
pub_args.put("uid",9527);

就可以得到文章开头所要传的参数格式。

参数的封装

虽然能请求了,但是公参里面的值大多不变,每次调用方法都要传pub_args也不好,所以还是可以通过封装参数,然后暴露多个重载方法,
让用户用的更省心:

TBaseApi.java

public class TBaseApi {

//最原始的方法,传url,请求头,私参,公参,回调
public static void createPostRequest(String url, Map<String,String> heads, Map<String,Object> pri_args, Map<String,Object> pub_args, TBaseCallback callback){
    TOkhttpUtil.postJson()
        .url(url)
        .headers(heads)
        .pri_args(pri_args)
        .pub_args(pub_args)
        .build()
        .execute(callback) ;
}
//封装了公参,使用时不用传公参了。
public static void createPostRequest(String url, Map<String,String> heads, Map<String,Object> pri_args, TBaseCallback callback){
    TOkhttpUtil.postJson()
            .url(url)
            .headers(heads)
            .pri_args(pri_args)
            .pub_args(TPublicParam.publicparam)
            .build()
            .execute(callback) ;
}
//使用固定的HOST
public static void createPostRequest(Map<String,String> heads, Map<String,Object> pri_args, TBaseCallback callback){
    TOkhttpUtil.postJson()
            .url(THost.HOST)
            .headers(heads)
            .pri_args(pri_args)
            .pub_args(TPublicParam.publicparam)
            .build()
            .execute(callback) ;
}
//封装请求头里面的s字段,只需传m字段就行了。
public static void createPostRequest(String headM, Map<String,Object> pri_args, TBaseCallback callback){
    TOkhttpUtil.postJson()
            .url(THost.HOST)
            .headers(TPublicParam.addHeadM(headM))
            .pri_args(pri_args)
            .pub_args(TPublicParam.publicparam)
            .build()
            .execute(callback);

}
}

上面的方法里,提供了多个选择,TPublicParam里封装好了公参。在使用的时候,只需要在初始化的时候,同时把固定的公参和私参也加进去:

比如在TApplication.java里初始化:

OkHttpClient client = new OkHttpClient.Builder()
            .connectTimeout(10*1000L, TimeUnit.MILLISECONDS)
            .readTimeout(10*1000L, TimeUnit.MILLISECONDS)
            .build();
TOkhttpUtil.initClient(client) ;
  TOkhttpUtil.initPubParams(TPublicParam.addPublicParams(pubParams));
TOkhttpUtil.initHeadS(TConstants.getS());

这样,以后在调用时,可以只传最低限度的参数,比如请求头的m字段,私参,和自定义的callback,已经封装好的头部的s字段和公参就不需要操心了。

比如:

Map<String,Object> pri_args = new HashMap();
TBaseCallback callback = new ....
TBaseApi.createPostRequest("getUserInfo",pri_args,callback) ;

下篇里会介绍响应的封装,以及TBaseCallback是什么。有兴趣可以看看下篇:
http://blog.csdn.net/black_dreamer/article/details/53068627

github地址:https://github.com/herdotage/Android_sample/tree/master/LOkhttpUtils

作者:black_dreamer 发表于2016/11/7 18:05:56 原文链接
阅读:8 评论:0 查看评论

一步步实现一个城市选择器

$
0
0

城市选择器

今天我们一起实现一个城市选择器。O.O

代码下载:
城市选择器 - 下载频道 - CSDN.NET
http://download.csdn.net/detail/baidu_31093133/9675482

效果图预览

主要包含以下内容:

1、自动定位所在城市
2、热门城市列表展示
3、所有城市列表的展示
4、输入城市名或者城市拼音搜索对应城市
5、右侧的slidebar城市列表导航栏

请大家先下载Demo然后再一边看demo一边看博客。因为博客里很多代码因为比较简单就不贴了。

首先我们先搭建基本的UI:

分析效果图,我们需要一个顶部title view,一个搜索框,一个定位功能的view,一个展示热门城市的view,一个侧边栏view和一个listview。

顶部title View:

这里有一些需要注意的地方:
我们在新建工程的时候,android studio会自动生成一个style作为我们的主题:

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <!-- Customize your theme here. -->
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="colorAccent">@color/colorAccent</item>
</style>    

android:theme="@style/AppTheme"  

这个默认的主题是带有actionbar的,如果我们要去掉这个actionbar,首先需要把DarkActionBar改为NoActionBar,因为使用AppCompatActivity的时候,Activity必须使用Theme.AppCompat主题及其子主题,所以我们的自定义的HD_NoActionBar样式必须继承这个主题:

 <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
    <!-- Customize your theme here. -->
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="colorAccent">@color/colorAccent</item>
</style>
<style name="HD_NoActionBar" parent="AppTheme">
    <item name="android:windowNoTitle">true</item>
    <item name="android:windowActionBar">false</item>
</style>

然后引用这个style:

android:theme="@style/AppTheme.NoActionBar"

接下来写我们的头布局 title_view.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">

<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="?attr/actionBarSize"
    android:background="@color/light_blue">

    <ImageView
        android:id="@+id/back"
        style="@style/Widget.AppCompat.ActionButton"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:paddingLeft="16dp"
        android:paddingRight="16dp"
        android:scaleType="center"
        android:src="@mipmap/ic_back"
        tools:ignore="ContentDescription" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="@string/select_city"
        android:textSize="20sp"
        android:textColor="@color/white" />
</RelativeLayout>

<View
    android:layout_width="match_parent"
    android:layout_height="1dp"
    android:background="@color/white"/>

布局返回按钮用一个ImageView,title用一个Textview。

然后在我们的主布局里使用标签引入头布局:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/light_blue"
android:orientation="vertical">

<include layout="@layout/title_view" />

现在的效果是这样的:

title

搜索框布局 search view

search_view/xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="36dp"
android:layout_marginBottom="8dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="8dp"
android:gravity="center_vertical"
android:background="@drawable/search_box_bg">

<ImageView
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:paddingLeft="8dp"
    android:paddingRight="8dp"
    android:src="@mipmap/ic_search"
    android:scaleType="center"
    tools:ignore="ContentDescription" />

<EditText
    android:id="@+id/et_search"
    android:layout_weight="1"
    android:layout_width="0dp"
    android:layout_height="match_parent"
    android:background="@null"
    android:gravity="center_vertical"
    android:hint="@string/hint_search_box"
    android:textColorHint="@color/deep_blue"
    android:inputType="text"
    android:singleLine="true"
    android:textColor="@color/deep_blue"
    android:textSize="14sp"
    tools:ignore="RtlHardcoded" />

<ImageView
    android:id="@+id/iv_search_clear"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:paddingLeft="8dp"
    android:paddingRight="8dp"
    android:src="@mipmap/ic_search_clear"
    android:visibility="gone"
    tools:ignore="ContentDescription" />

然后在主布局里引入这个布局:

<include layout="@layout/search_view"/>

搜索框的布局也非常简单,就不说明了。

现在的效果:

城市列表

接下来的定位城市、热门城市、以及所有城市的列表我们使用一个Listview搞定,让Listview加载三种不同的布局来展示。

定位城市和所有城市列表好说,这个热门城市的UI该怎么做呢?我们准备使用gridview来做,在listview里嵌套gridview会遇到gridview只能显示一行的问题,我们先重现这个问题,然后再分析怎么解决。

listview需要一个adapter适配器,adapter需要一个数据源,我们的数据源存放在一个db数据库里,所以我们要构建一个数据库操作类,从数据库中取出这些城市然后展示出来。这一段的代码比较多,前方高能预警(^__^)

我们把要做的事情按步骤划分:

1、导入数据库文件
2、构建City对象,用户存储城市信息
3、创建DBManager用来操作数据库,将查询到的数据传递给adapter
4、编写定位城市、热门城市、所有城市三种不同的item布局
5、编写adapter,在adapter里加载三种item布局
6、编写gridview热门城市的item布局
7、实现gridview的adapter

1、建立assets文件,并把db文件放在assets目录下:

2、City对象

City.java:

public class City {
private String name;
private String pinyin;

public City() {}

public City(String name, String pinyin) {
    this.name = name;
    this.pinyin = pinyin;
}

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

public String getPinyin() {
    return pinyin;
}

public void setPinyin(String pinyin) {
    this.pinyin = pinyin;
}

}

数据库操作类:

DBManager.java:

public class DBManager {
private static final String ASSETS_NAME = "china_cities.db";
private static final String DB_NAME = "china_cities.db";
private static final String TABLE_NAME = "city";
private static final String NAME = "name";
private static final String PINYIN = "pinyin";
private static final int BUFFER_SIZE = 1024;
private String DB_PATH;
private Context mContext;

//初始化
public DBManager(Context context) {
    this.mContext = context;
    DB_PATH = File.separator + "data"
            + Environment.getDataDirectory().getAbsolutePath() + File.separator
            + context.getPackageName() + File.separator + "databases" + File.separator;
}
//保存数据库到本地
@SuppressWarnings("ResultOfMethodCallIgnored")
public void copyDBFile(){
    File dir = new File(DB_PATH);
    if (!dir.exists()){
        dir.mkdirs();
    }
    File dbFile = new File(DB_PATH + DB_NAME);
    if (!dbFile.exists()){
        InputStream is;
        OutputStream os;
        try {
            is = mContext.getResources().getAssets().open(ASSETS_NAME);
            os = new FileOutputStream(dbFile);
            byte[] buffer = new byte[BUFFER_SIZE];
            int length;
            while ((length = is.read(buffer, 0, buffer.length)) > 0){
                os.write(buffer, 0, length);
            }
            os.flush();
            os.close();
            is.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
/**
 * 读取所有城市
 * @return
 */
public List<City> getAllCities(){
    SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(DB_PATH + DB_NAME, null);
    Cursor cursor = db.rawQuery("select * from " + TABLE_NAME, null);
    List<City> result = new ArrayList<>();
    City city;
    while (cursor.moveToNext()){
        String name = cursor.getString(cursor.getColumnIndex(NAME));
        String pinyin = cursor.getString(cursor.getColumnIndex(PINYIN));
        city = new City(name, pinyin);
        result.add(city);
    }
    cursor.close();
    db.close();
    Collections.sort(result, new CityComparator());
    return result;
}

/**
 * 通过名字或者拼音搜索
 * @param keyword
 * @return
 */
public List<City> searchCity(final String keyword){
    SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(DB_PATH + DB_NAME, null);
    Cursor cursor = db.rawQuery("select * from " + TABLE_NAME +" where name like \"%" + keyword
            + "%\" or pinyin like \"%" + keyword + "%\"", null);
    List<City> result = new ArrayList<>();
    City city;
    while (cursor.moveToNext()){
        String name = cursor.getString(cursor.getColumnIndex(NAME));
        String pinyin = cursor.getString(cursor.getColumnIndex(PINYIN));
        city = new City(name, pinyin);
        result.add(city);
    }
    cursor.close();
    db.close();
    Collections.sort(result, new CityComparator());
    return result;
}

/**
 * a-z排序
 */
private class CityComparator implements Comparator<City> {
    @Override
    public int compare(City lhs, City rhs) {
        String a = lhs.getPinyin().substring(0, 1);
        String b = rhs.getPinyin().substring(0, 1);
        return a.compareTo(b);
    }
}

}

这个类使用SQLiteDatabase来管理数据库,同事写了一个排序类CityComparator用来对城市按照首字母进行排序

定位城市的布局:

view_locate_city.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="8dp"
tools:ignore="RtlHardcoded">

<TextView
    style="@style/LetterIndexTextViewStyle"
    android:text="@string/located_city"/>

<LinearLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginLeft="16dp"
    android:background="@color/content_bg">

    <LinearLayout
        android:id="@+id/layout_locate"
        android:layout_width="wrap_content"
        android:layout_height="40dp"
        android:minWidth="96dp"
        android:paddingLeft="8dp"
        android:paddingRight="8dp"
        android:gravity="center"
        android:clickable="true"
        android:background="@drawable/overlay_bg">

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@mipmap/ic_locate"
            tools:ignore="ContentDescription" />

        <TextView
            android:id="@+id/tv_located_city"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="8dp"
            android:text="@string/locating"
            android:textSize="16sp"
            android:textColor="@color/white"/>
    </LinearLayout>
</LinearLayout>

效果图

然后是所有城市的布局:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
tools:ignore="RtlHardcoded">

<TextView
    android:id="@+id/tv_item_city_listview_letter"
    style="@style/LetterIndexTextViewStyle"
    android:textSize="18sp"
    android:clickable="false"/>

<TextView
    android:id="@+id/tv_item_city_listview_name"
    android:layout_width="match_parent"
    android:layout_height="48dp"
    android:paddingLeft="16dp"
    android:paddingRight="@dimen/side_letter_bar_width"
    android:background="?android:attr/selectableItemBackground"
    android:clickable="true"
    android:gravity="center_vertical"
    android:textSize="@dimen/city_text_size"
    android:textColor="@color/light_blue"/>
<View
    android:layout_width="match_parent"
    android:layout_height="1px"
    android:layout_marginLeft="16dp"
    android:layout_marginRight="@dimen/side_letter_bar_width"
    android:background="@color/divider"/>

使用两个TextView一个用来显示城市的首字母,一个用来显示城市名字

上面的布局都很简单,接下来就是热门城市了:

如果我们使用gridview不做任何处理的话,最终效果是这样的:

不止在listview,gridview在其它任何可以滚动的view里都会出现这个问题,解决这个问题我们有固定的方案,那就是自定义一个gridview然后重写onMeasure方法,在onMeasure方法里,让gridview测量子view的高度,并全部显示出来。

代码其实非常简单:

public class WrapHeightGridView extends GridView {
public WrapHeightGridView(Context context) {
    this(context, null);
}

public WrapHeightGridView(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
}

public WrapHeightGridView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
}

//如果把GridView放到一个垂直方向滚动的布局中,设置其高度属性为 wrap_content ,
// 则该GridView的高度只有一行内容,其他内容通过滚动来显示。
// 如果你想让该GridView的高度为所有行内容所占用的实际高度,则可以通过覆写GridView的 onMeasure 函数来修改布局参数
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int heightSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
    super.onMeasure(widthMeasureSpec, heightSpec);
}

}

这里重点理解makeMeasureSpec这个方法,public static int makeMeasureSpec(int size, int mode)
这个是由我们给出的尺寸大小和模式生成一个包含这两个信息的int变量。这个int值有32位,其中高2位表示模式,低30位表示值。我们把Integer.MAX_VALUE >> 2右移两位然后和MeasureSpec.AT_MOST合成一个新的int值。Integer.MAX_VALUE是INT类型的最大值是0xFFFFFFFF,所以这个值右移两位就表示新的合成的int值得低30位都是1。也就是说我们取最大值作为控件高度的最大值。

这样就可以了,效果:

接下来看一下adapter是怎么实现的:

CityListAdapter.java:

package test.study.select_city.adapter;

public class CityListAdapter extends BaseAdapter{
private static final int VIEW_TYPE_COUNT = 3;

private Context mContext;
private LayoutInflater inflater;
private List<City> mCities;
private HashMap<String, Integer> letterIndexes;
private String[] sections;
private OnCityClickListener onCityClickListener;
private int locateState = LocateState.LOCATING;
private String locatedCity;

public CityListAdapter(Context mContext, List<City> mCities) {
    this.mContext = mContext;
    this.mCities = mCities;
    this.inflater = LayoutInflater.from(mContext);
    if (mCities == null){
        mCities = new ArrayList<>();
    }
    mCities.add(0, new City("定位", "0"));
    mCities.add(1, new City("热门", "1"));
    int size = mCities.size();
    letterIndexes = new HashMap<>();
    sections = new String[size];
    for (int index = 0; index < size; index++){
        //当前城市拼音首字母
        String currentLetter = PinyinUtils.getFirstLetter(mCities.get(index).getPinyin());
        //上个首字母,如果不存在设为""
        String previousLetter = index >= 1 ? PinyinUtils.getFirstLetter(mCities.get(index - 1).getPinyin()) : "";
        if (!TextUtils.equals(currentLetter, previousLetter)){
            letterIndexes.put(currentLetter, index);
            sections[index] = currentLetter;
        }
    }
}

/**
 * 更新定位状态
 * @param state
 */
public void updateLocateState(int state, String city){
    this.locateState = state;
    this.locatedCity = city;
    notifyDataSetChanged();
}

/**
 * 获取字母索引的位置
 * @param letter
 * @return
 */
public int getLetterPosition(String letter){
    Integer integer = letterIndexes.get(letter);
    return integer == null ? -1 : integer;
}

@Override
public int getViewTypeCount() {
    return VIEW_TYPE_COUNT;
}

@Override
public int getItemViewType(int position) {
    return position < VIEW_TYPE_COUNT - 1 ? position : VIEW_TYPE_COUNT - 1;
}

@Override
public int getCount() {
    return mCities == null ? 0: mCities.size();
}

@Override
public City getItem(int position) {
    return mCities == null ? null : mCities.get(position);
}

@Override
public long getItemId(int position) {
    return position;
}

@Override
public View getView(final int position, View view, ViewGroup parent) {
    CityViewHolder holder;
    int viewType = getItemViewType(position);
    switch (viewType){
        case 0:     //定位
            view = inflater.inflate(R.layout.view_locate_city, parent, false);
            ViewGroup container = (ViewGroup) view.findViewById(R.id.layout_locate);
            TextView state = (TextView) view.findViewById(R.id.tv_located_city);
            switch (locateState){
                case LocateState.LOCATING:
                    state.setText(mContext.getString(R.string.locating));
                    break;
                case LocateState.FAILED:
                    state.setText(R.string.located_failed);
                    break;
                case LocateState.SUCCESS:
                    state.setText(locatedCity);
                    break;
            }
            container.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (locateState == LocateState.FAILED){
                        //重新定位
                        if (onCityClickListener != null){
                            onCityClickListener.onLocateClick();
                        }
                    }else if (locateState == LocateState.SUCCESS){
                        //返回定位城市
                        if (onCityClickListener != null){
                            onCityClickListener.onCityClick(locatedCity);
                        }
                    }
                }
            });
            break;
        case 1:     //热门
            view = inflater.inflate(R.layout.view_hot_city, parent, false);
            WrapHeightGridView gridView = (WrapHeightGridView) view.findViewById(R.id.gridview_hot_city);
            final HotCityGridAdapter hotCityGridAdapter = new HotCityGridAdapter(mContext);
            gridView.setAdapter(hotCityGridAdapter);
            gridView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
                @Override
                public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                    if (onCityClickListener != null){
                        onCityClickListener.onCityClick(hotCityGridAdapter.getItem(position));
                    }
                }
            });
            break;
        case 2:     //所有
            if (view == null){
                view = inflater.inflate(R.layout.item_city_listview, parent, false);
                holder = new CityViewHolder();
                holder.letter = (TextView) view.findViewById(R.id.tv_item_city_listview_letter);
                holder.name = (TextView) view.findViewById(R.id.tv_item_city_listview_name);
                view.setTag(holder);
            }else{
                holder = (CityViewHolder) view.getTag();
            }
            if (position >= 1){
                final String city = mCities.get(position).getName();
                holder.name.setText(city);
//如果当前的item的城市的首字母和上一个城市的首字母相同,就不显示首字母否则就显示。  
//这样就可以实现让所有城市根据首字母分类的效果了。
                String currentLetter = PinyinUtils.getFirstLetter(mCities.get(position).getPinyin());
                String previousLetter = position >= 1 ? PinyinUtils.getFirstLetter(mCities.get(position - 1).getPinyin()) : "";
                if (!TextUtils.equals(currentLetter, previousLetter)){
                    holder.letter.setVisibility(View.VISIBLE);
                    holder.letter.setText(currentLetter);
                }else{
                    holder.letter.setVisibility(View.GONE);
                }
                holder.name.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        if (onCityClickListener != null){
                            onCityClickListener.onCityClick(city);
                        }
                    }
                });
            }
            break;
    }
    return view;
}

public static class CityViewHolder{
    TextView letter;
    TextView name;
}

public void setOnCityClickListener(OnCityClickListener listener){
    this.onCityClickListener = listener;
}

public interface OnCityClickListener{
    void onCityClick(String name);
    void onLocateClick();
}

}

Listview加载不同的布局,请参考我的另一篇博客:
最主要的就是getItemViewType方法。

listview加载不同布局 - 秦时明月 - 博客频道 - CSDN.NET
http://blog.csdn.net/baidu_31093133/article/details/51804923

在adapter里使用LocateState类来标示不同的定位状态。代码比较多,但是都很好理解就不再解释了(其实是懒。。。)

gridview热门城市的item布局

就是一个Textview而已

gridview热门城市的adapter

请参考demo,就是一个简单的adapter

然后是侧边导航栏的实现:

写一个自定义view,
SideBar.java

public class SideBar extends View {
private static final String[] b = {"定位", "热门", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"};
private int choose = -1;
private Paint paint = new Paint();
private boolean showBg = false;
private OnLetterChangedListener onLetterChangedListener;
private TextView overlay;

public SideBar(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
}

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

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

/**
 * 设置悬浮的textview
 * @param overlay
 */
public void setOverlay(TextView overlay){
    this.overlay = overlay;
}

@SuppressWarnings("deprecation")
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    if (showBg) {
        canvas.drawColor(Color.TRANSPARENT);
    }

    int height = getHeight();
    int width = getWidth();
    int singleHeight = height / b.length;//单行字母高度
    for (int i = 0; i < b.length; i++) {
        paint.setTextSize(getResources().getDimension(R.dimen.side_letter_bar_letter_size));
        paint.setColor(getResources().getColor(R.color.deep_blue));
        paint.setAntiAlias(true);
        if (i == choose) {
            paint.setColor(getResources().getColor(R.color.gray_deep));

// paint.setFakeBoldText(true); //加粗
}
float xPos = width / 2 - paint.measureText(b[i]) / 2;
float yPos = singleHeight * i + singleHeight;
canvas.drawText(b[i], xPos, yPos, paint);
paint.reset();
}

}

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    final int action = event.getAction();
    final float y = event.getY();
    final int oldChoose = choose;
    final OnLetterChangedListener listener = onLetterChangedListener;
    final int c = (int) (y / getHeight() * b.length);//获取字母的index

    switch (action) {
        case MotionEvent.ACTION_DOWN:
            showBg = true;
            if (oldChoose != c && listener != null) {
                if (c >= 0 && c < b.length) {
                    listener.onLetterChanged(b[c]);
                    choose = c;
                    invalidate();
                    if (overlay != null){
                        overlay.setVisibility(VISIBLE);
                        overlay.setText(b[c]);
                    }
                }
            }

            break;
        case MotionEvent.ACTION_MOVE:
            if (oldChoose != c && listener != null) {
                if (c >= 0 && c < b.length) {
                    listener.onLetterChanged(b[c]);
                    choose = c;
                    invalidate();
                    if (overlay != null){
                        overlay.setVisibility(VISIBLE);
                        overlay.setText(b[c]);
                    }
                }
            }
            break;
        case MotionEvent.ACTION_UP:
            showBg = false;
            choose = -1;
            invalidate();
            if (overlay != null){
                overlay.setVisibility(GONE);
            }
            break;
    }
    return true;
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    return super.onTouchEvent(event);
}

public void setOnLetterChangedListener(OnLetterChangedListener onLetterChangedListener) {
    this.onLetterChangedListener = onLetterChangedListener;
}

public interface OnLetterChangedListener {
    void onLetterChanged(String letter);
}

}

我们绘制了slidebar,并且重写了它的ontouch事件。当手指摁下和滑动的时候在布局中央显示当前城市的首字母,显示首字母的控件是一个Textview,这个Textview通过setOverlay方法传递进来。抬起的时候不显示。

现在的效果是这样:

UI总算实现的差不多了,接下来还有城市定位功能,搜索功能以及右侧导航功能的要实现。

搜索功能:

主要就是给edittext设置一个TextChangedListener,让它根据输入去数据库中查找数据并将数据传递给adapter,然后通过notifyDataSetChanged方法来更新UI。

代码请参照demo

右侧导航功能

当我们的手指在slidebar上滑动的时候会触发它的ontouch事件,然后通过回调将当前的字母传递回来,接着我们将点击的字母传递给mCityAdapter的getLetterPosition方法,来得到当前字母的位置,并通过mListview.setSelection(position);方法来改变listview的显示位置

代码参考demo

定位功能

这个需要接入百度地图的sdk

这个大家根据百度地图开发者中心的手册一点点来就可以了。

传送门:

android-locsdk/guide/key - Wiki
http://lbsyun.baidu.com/index.php?title=android-locsdk/guide/key

谢谢大家。(^__^)

作者:baidu_31093133 发表于2016/11/7 18:20:46 原文链接
阅读:216 评论:0 查看评论

Android"挂逼"修炼之行---微信摇骰子和猜拳作弊器原理解析

$
0
0

一、前言

在之前的一篇文章中我们已经详细介绍了Android中Hook工作的一款神器Xposed工具:Xposed框架原理解析和使用案例分析 在那一篇文章中我们介绍了如何安装Xposed框架,以及如何使用Xposed库编写第一个模块来做到修改系统方法功能的效果,同时也说到了一个非常重要的知识点就是:在Hook过程中最重要的一点就是如何找到Hook点,而对于这一点很多同学都会感觉到非常的困难,因为对于修改系统方法还好,因为可以简单的去查看具体的Android源码即可,但是如果说要去编写一些应用和游戏的外挂的话,那么第一步就得先去破解对应的App找到指定Hook点,这一步是非常困难的。所以只能多尝试多破解慢慢长点经验即可。


二、猜想假设

本文就借助之前的Xposed框架来介绍如何编写微信的一个外挂功能,这个功能就是微信摇色子和剪刀石头布的作弊器,我们用过微信这个功能都知道,是一个比较常用的功能,因为在一个群聊中会很无聊就来这种简单的比赛,谁输了发红吧啥的。那么这个功能肯定伴有随机性,而且我们应该相信这个随机方法肯定是在微信代码中的某个地方,所以如果我们找到这个方法了,那么就可以进行Hook他,然后拦截返回最大的数值即可,也就是我们想要得数值。


三、准备工作

上面大致分析了这个功能的原理使用,下面咋们就不多说了,直接进行操作,本文用了一个微信的老版本做测试的:微信6.3.9.apk ;那么第一步咋们得逆向微信,需要做两个工作:

第一先使用apktools反编译apk,这个不用多说了,微信没有对应用进行加固,所以反编译过程很正常。

第二需要借助可视化反编译工具Jadx来打开微信apk,因为微信应用很大,所以得慢慢等待一会才可以打开。


四、逆向分析

下面在来冷静分析一下如何我们该怎么入手?上面反编译成功了之后,还得寻求入口,这个入口也很简单的,因为我们想得到这个随机函数,那么在我们能看到的效果是色子,我们可以选择一个微信聊天对话框,然后点击一个色子功能:


看到了,我们点击色子之后肯定会调用这个随机函数获取随机值,那么这里就是我们的入口,所以第一步肯定先找到这个点击事件,如果要找到点击事件,那么就得先找到这个控件的定义,那么问题来了,如果能够快速的找到这个控件的定义呢?这个技术在我之前的一篇文章中已经介绍了,就是 微信的自动抢红包功能 当时因为要找到那个红包的点击事件,所以就用同样的方法来得到那个红包的控件定义的地方,而这个方法就是借助AndroidSDK提供的一个工具:uiautomatorviewer.bat 这个工具位于SDK目录的tools目录下,我们可以点击运行,然后就可以看到这个界面了:


我们把设备停在聊天对话框中,然后使用这个工具,点击左上角那个按钮,可以进行当前桌面的界面布局分析,分析结果我们可以看到,这个色子是一个自定义控件:com.tencent.mm.ui.MMImageView,然后他的id是ae7,而这个值非常关键,后面就是用这个值来进行一步一步的跟踪,这里我们再多看一眼,就是这个表情区域的详细布局:


猜也猜到了,表情区域外面应该是一个ViewPager控件可以进行滑动切换,然后是每一页的控件用的是GridView进行卡片分割,那么这里就会给我们一个提醒了:后面的点击事件要么是在GridView的适配器类的getView方法中对view进行setOnClickListener添加的,要么是对GridView添加onItemClick事件的。


好了下面继续跟踪,因为有了那个色子控件的id了,下面咋们可以直接用这个id去全局搜索这个值了,不过这里有个问题就是微信其实本身做了资源的混淆,而这个混淆一方面是加大安全工作,一方面是减小包的大小功能,我们可以使用解压工具简单的查看他的apk文件中的res目录,会发现全是字母文件夹。而且从上面的id命名也可以看到,我相信微信工程师不可能会傻逼的把一个id命名成ae7了吧?到这里我们又要借助一个知识点了,而这个知识点在我之前的一篇文章中介绍了:Android中的应用攻防之战 在这篇文章中介绍了,我们在反编译apk之后,其实apk所有的资源id值都会保存在一个public.xml文件中,而这个文件是放在values目录下的,这个文件主要存放的就是资源的id值和name,type之间的对应关系,而反编译之后的代码中一般不会用R.id.xxx这种样式来访问控件,而是用转化之后的id值,这个值是十进制的,所以我们得先用上面那个ae7的id值去public.xml文件中找到对应的十六进制值:


注意:

这里在查找ae7的时候,会发现多个匹配项,而我们需要用额外的信息来作区分,那就是type字段了,type字段有很多种值,比如layout,drawable,string,attr,id等,我们这里因为是定义控件的所以type=id。


找到这个项之后,就把后面id的十六进制值转化成对应的十进制值吧:0x7f07060e=2131166734

有了这个值,就好办了,咋们直接在Jadx中打开的微信apk中全局搜索这个值:


哎,可惜的是没有找到,所以到这里就开始蛋疼了,也是这次逆向的最大阻碍了,想了好长时间,最后才突然想起来以前 逆向微信的本地通讯录信息 的时候发现微信采用了分包技术,也就是微信包太大,因为Android系统中有方法数的限制所以需要对apk进行拆包操作,具体可以查看这篇文章:Android中应用拆包技术详解 那么我们会发现反编译之后也没有看到多个dex文件,所以这时候还要猜想他应该是存在本地目录的,在应用启动的时候主dex功能再去加载这个次dex文件,通过查看反编译之后的目录,结果在assets目录下找到了他:


而这个是两个从dex文件,这里他做了dex文件转成jar文件的操作了,所以我们先用解压工具解压这个jar文件,得到对应的dex文件即可。然后咋们在开启一个Jadx窗口打开这个从dex文件,然后在全局搜索上面的那个值:


看到了,终于找到了这个控件的定义的地方了,点击第一个进去查看:


我们在往下面看这个类的信息,会发现到有一个getView方法:


看到这个就可以判定了,这个类其实是一个BaseAdapter类型了,而在getView中没有看到控件的点击事件,所以猜想应该是外部给GridView添加的onItemClick事件了,咋们继续选中这个类名,然后右击进行跟踪这个类在哪些地方被调用了,这个功能的确很实用的:

点击查找之后,会有很多地方调用:


这里从第一项的那个参数命令可以猜想到了应该就是GridView类型,而到这里我们貌似看到了胜利的曙光了:


然后查看这个smileyGrid变量的定义:


这里是一个SmileyGrid类型的,咋们可以全局搜索这个类,看看他的定义:


果然不出所料,这里是一个GridView类型,然后也看到了我们非常期待的onItemClick方法了,下面就开始分析这个onItemClick方法的逻辑了:


这里我没有找到一个好的办法,也不想去深入跟踪了,因为这里的判断分之不是很多,所以就顺序的尝试了每个方法,结果找到了最后一个a方法,然后点进去查看逻辑:


这里我们就需要多想点了,从我们点击色子之后的效果看,没有对话框和toast提示,那么这里就只有两个分支是最有可能执行的了,通过顺序尝试之后,发现是第一处分支的逻辑了,也就是h.a.aMZ.b()这个方法执行了,通过import导入的类,得到这个类的路径定义:


但是在这个dex中没有找到这个类,所以猜想应该是在主dex中,果然找到了这个类定义,然后进入这个类进行想详细查看:


继续进入看看b方法的定义:


擦,这里的b方法尽然返回的是null,那么到这里就要思考了,如果返回null的话,之前的点击事件肯定是无效的,而这个又不符合实际情况,所以猜想还有哪个地方对这个iop进行赋值操作了。我们看看这个iop定义:


从定义上可以看到,极大可能在其他地方进行了赋值操作,所以咋们全局搜索这个变量值iop:


发现搜索结果还是我们刚刚看到的返回null的代码,所以咋们又得继续去另外一个dex中进行搜索了:


这里搜到了一个赋值操作,立马点击进入查看:


然后查看PD方法的定义:


继续查看这个g类定义:


看到他的b方法,这里有一个bb.pu这个方法比较可疑,因为最终返回的值都是和他相关的,而这个方法在这个dex中又没有找到,所以咋们看一下他的import找到全局定义路径,然后去另外一个dex中查看定义:


到这里,就会非常激动了,因为我们看到了胜利了,这个非常明显的随机方法终于找到了,而通过这个随机公式可以看到,这个方法的功能是返回一个0-i之间的随机值,而对于色子应该是0-6之间的值,猜拳是0-3之间的值。所以这个方法百分百是用于随机功能的。


五、掌握逆向技能

到这里我们就成功的找到了我们想要Hook的地方了,从上面的逆向过程会发现微信的工程非常庞大,而对于我们逆向来说工作是非常艰难的,但是还好我们有一些强大的工具可以快速的定位问题,下面就来总结本文逆向收获的知识:

第一、对于逆向中想得到控件点击事件入口,可以通过以下步骤来进行

1、使用界面分析工具得到指定控件的id名称

2、通过id名称去反编译之后的values/public.xml中查找到指定的值,转化成十进制

3、通过Jadx自带的全局所有功能,查找这个十进制值即可

对于这个步骤将适用于想得到一个应用中某个控件的点击事件逻辑入口是非常有效的,而且也是通用的方法。

第二、对于微信来说,因为工程的庞大,所以肯定会存在拆包现象,所以他不止一个dex文件。肯定是包含多个dex文件的,所以后面还需继续写微信的外挂,到时候分析都是要注意这一点。

第三、对于Jadx的强大工具在本文中起到了非常重要的功能:跟踪方法的调用,全局查找功能。


额外说明:

在本文中我们会发现有一个难受的地方就是多个dex文件之间查找相应的方法的地方,所以其实我们可以这么干,我们如果得到了应用所有的dex文件之后,可以将其先转化成对应的java类,然后进行类合并,这里我们可以使用Beyond Compare比较工具将多个不同文件夹合并到一起:


合并之后咋们在用工具将其转化成一个dex文件,也就完成了多个dex文件合并工作了。在这个工程中,我们可以非常巧妙的借助Jadx的另外一个强大功能,就是可以把反编译之后的内容保存到本地:


他有个好处就是,把apk中的dex保存成java文件,资源全部解码保存指定xml文件,而最终的保存样式是一个gradle工程,而这个工程咋们就可以直接导入到一个开发工具中了非常方便了。特别是资源文件,我们在之前会发现apktools工具并没有反编译xml文件出来。而在这里就可以了。


六、开始拦截操作

下面咋们就来进行Hook操作了,上面已经得到了这个随机函数的名称了:

com.tencent.mm.sdk.platformtools.bb.pu(int i)

Hook工作就非常简单了,咋们拦截这个方法之后根据传入的值做一次判断是摇骰子还是猜拳操作:


首先咋们得做一次过滤操作,就是只会Hook微信应用,然后到拦截操作中,通过传递的参数做判断是摇色子还是猜拳,如果是摇色子就返回一点,猜拳就返回剪刀。

编写成功之后,就进行编译成模块,然后重启设备生效,点开微信打开一个聊天框,开始摇色子:


通过打印值可以看到,我们的猜想是正确的,看一下实际效果:



哈哈哈,所有的操作都在掌控之中,到这里我们也成功的编写了一个微信外挂功能,而这个外挂可能有的同学感觉用途不是那么大,而现在主流的外挂是可以防止撤销功能,分享视频到朋友圈,不过这些功能都会慢慢讲解的,不要着急,而对于本文以及后续的外挂内容文章,我想表达的是,结果并不重要,重要的是整个逆向过程,在每次一逆向之后我们学到了什么,是否涨了逆向经验,这才是我们需要得到的。


补充说明:

第一点:有的同学会发现咋们上面得到的那个方法貌似是混淆之后的,也就说如果哪天微信改了混淆机制,那么这个Hook就失效了,本文中用到的是微信6.3.9的版本,对于其他版本我想这个方法类名应该是变了,也就说如果微信每次出一个版本咋们都得去逆向一次得到方法名,但是这个都不是难事,以为本文已经将整个逆向过程讲解了,所以以后只要按照这个步骤来,查找方法也是分分钟的事!

第二点:本文的用意是分享逆向技巧知识点,所以也请各位同学能够知道结果不重要,重要的是整个逆向的过程,也就是如何寻找到Hook点


严重声明:本文介绍的知识点完全是从一个技术分享角度出发,绝非用于任何商业活动和用途,如果涉及到任何法律问题将由操作者本人负责。本文作者将不负责任何法律责任!也请各位同学秉着技术角度出发的原则,切勿用于商业中!


七、总结

本文主要介绍了一个微信的简单外挂功能,就是摇色子和猜拳作弊器功能,而在整个过程中,我们可以看到找到Hook点才是最关键的,而最难的也是找到这个点。一般都是需要进行逆向分析微信应用找到这个点。在这个过程中我们又学习到了一些新的逆向应用技巧知识点,后续将会继续带大家一起分享其他的外挂功能知识点,敬请期待,同时也要多多支持扩散分享啦,也要多多点赞呢?写这篇文章实属不易呀,因为逆向的我头都炸了!

更多内容:点击这里

关注微信公众号,最新技术干货实时推送



作者:jiangwei0910410003 发表于2016/11/7 18:54:33 原文链接
阅读:159 评论:0 查看评论

Qt之图形视图(常见图元)

$
0
0

简述

QGraphicsItem 类是 QGraphicsScene 中所有 item(图元)的基类。

它提供了一个轻量级的基础,用于编写自己的自定义图元。其中包括:定义图元的几何形状、碰撞检测、绘制实现、以及通过其事件处理程序进行的图元交互,QGraphicsItem 是 Qt之图形视图框架 的一部分。

常见图元

为方便起见,Qt 为最常见的形状提供了一组标准图元。它们是:

  • QGraphicsSimpleTextItem:提供了一个简单的文本标签项
  • QGraphicsTextItem:提供了一个高级文本浏览器项
  • QGraphicsLineItem:提供了一个直线项
  • QGraphicsPixmapItem:提供了一个图像项
  • QGraphicsRectItem:提供了一个矩形项
  • QGraphicsEllipseItem:提供了一个椭圆项
  • QGraphicsPathItem:提供了一个路径项
  • QGraphicsPolygonItem:提供了一个多边形项

QGraphicsSimpleTextItem

详细描述

QGraphicsSimpleTextItem 提供了一个简单的文本标签项,可以添加到 QGraphicsScene 中。

要设置 item 的文本,可以传递 QString 到 QGraphicsSimpleTextItem 的构造函数,或在之后调用 setText() 来更改文本。要设置文本填充色,调用 setBrush()。

简单文本项可以包含填充和轮廓,setBrush() 用于设置文本填充(即,文本色),setPen() 用于设置绘制文本轮廓的画笔,(后者可能很慢,特别是对于复杂的画笔以及具有长文本内容的 item)。如果只想绘制一行简单的文本,只需要调用 setBrush(),不需要设置画笔,QGraphicsSimpleTextItem 的画笔默认是 Qt::NoPen。

QGraphicsSimpleTextItem 使用文本的格式化大小和相关联的字体,为 boundingRect()、shape() 和 contains() 提供了一个合理的实现。可以通过调用 setFont() 设置字体。

QGraphicsSimpleText 不显示富文本,相反,可以使用 QGraphicsTextItem,它提供全文控制功能。

这里写图片描述

示例

这里写图片描述

// 定义一个简单的文本项
QGraphicsSimpleTextItem *pItem = new QGraphicsSimpleTextItem();
pItem->setText(QString::fromLocal8Bit("一去丶二三里"));

// 字体
QFont font = pItem->font();
font.setPixelSize(20);  // 像素大小
font.setItalic(true);  // 斜体
font.setUnderline(true);  // 下划线
pItem->setFont(font);

pItem->setBrush(QBrush(QColor(0, 160, 230)));

// 将简单的文本项添加至场景中
QGraphicsScene *pScene = new QGraphicsScene();
pScene->addItem(pItem);

// 为视图设置场景
QGraphicsView *pView = new QGraphicsView();
pView->setScene(pScene);
pView->setStyleSheet("border:none; background:transparent;");

pView->show();

除了使用 QGraphicsScene 的 addItem() 添加图元之外,还可以使用更简单 addSimpleText() 函数,该函数会返回一个 QGraphicsSimpleTextItem。

QGraphicsTextItem

详细描述

QGraphicsTextItem 类提供了一个格式化的文本项,可以添加到 QGraphicsScene 中。

要设置 item 的文本,可以传递 QString 到 QGraphicsTextItem 的构造函数,或调用 setHtml()/setPlainText()。

QGraphicsTextItem 使用文本的格式化大小和相关联的字体,为 boundingRect()、shape() 和 contains() 提供了一个合理的实现。可以通过调用 setFont() 设置字体。

可以通过使用 setTextInteractionFlags() 设置 Qt::TextEditorInteraction 标志来使图元可编辑。

图元的首选文本宽度可以使用 setTextWidth() 设置,并使用 textWidth() 获取。

注意:为了在中心对齐 HTML 文本,必须设置项目的文本宽度。否则,可以在设置项目的文本后调用 adjustSize()。

这里写图片描述

注意: QGraphicsTextItem 默认接受 hover 事件,可以使用 setAcceptHoverEvents() 更改此值。

纯文本

这里写图片描述

首先,通过调用 QGraphicsTextItem 的构造函数创建一个文本项(也可使用 QGraphicsScene 的 addText() 函数创建,并将文本项添加至场景中)。要设置项目的文本,传递 QString 至 QGraphicsTextItem 的构造函数,或调用 setPlainText()。

setDefaultTextColor() 如果要设置文本项的字体,通过调用 setFont() 设置。

// 定义一个文本项
QGraphicsTextItem *pItem = new QGraphicsTextItem();
pItem->setPlainText(QString::fromLocal8Bit("一去丶二三里"));
pItem->setDefaultTextColor(QColor(0, 160, 230));  // 文本色

// 字体
QFont font = pItem->font();
font.setPixelSize(20);  // 像素大小
font.setItalic(true);  // 斜体
font.setUnderline(true);  // 下划线
pItem->setFont(font);

// 将文本项添加至场景中
QGraphicsScene *pScene = new QGraphicsScene();
pScene->addItem(pItem);

// 为视图设置场景
QGraphicsView *pView = new QGraphicsView();
pView->setScene(pScene);
pView->setStyleSheet("border:none; background:transparent;");

pView->show();

富文本

假设文本是 HTML 格式,显示不同颜色的文本以及图片。

这里写图片描述

QString strHTML = QString("<html> \
                               <head> \
                               <style> \
                               font{color:white;} #f{font-size:18px; color: #00A0E6;} \
                               </style> \
                               </head> \
                               <body>\
                               <font>%1</font><font id=\"f\">%2</font> \
                               <br/><br/> \
                               <img src=\":/Images/logo\" width=\"100\" height=\"100\"> \
                               </body> \
                               </html>").arg("I am a ").arg("Qter");
pItem->setHtml(strHTML);

超链接

我们需要简单使用标签 <a></a>写一段简单的 HTML 超链接代码

方式一:

比较简单,直接调用 setOpenExternalLinks(true) 即可。

pItem->setHtml(QString("<a href = \"%1\">%2</a>").arg("http://blog.csdn.net/liang19890820").arg(QStringLiteral("一去丶二三里")));
pItem->setOpenExternalLinks(true);
pItem->setTextInteractionFlags(Qt::TextBrowserInteraction);

方式二:

连接 linkActivated() 信号,然后调用 QDesktopServices 的 openUrl() 打开链接:

pItem->setHtml(QString("<a href = \"%1\">%2</a>").arg("http://blog.csdn.net/liang19890820").arg(QStringLiteral("一去丶二三里")));
pItem->setTextInteractionFlags(Qt::TextBrowserInteraction);
connect(pItem, &QGraphicsTextItem::linkActivated, [=](QString link) {
            QDesktopServices::openUrl(QUrl(link));
});

注意:这两种方式都需要调用 setTextInteractionFlags(Qt::TextBrowserInteraction),指定交互方式为文本浏览器交互。

编辑

通过使用 setTextInteractionFlags() 设置 Qt::TextEditorInteraction 标志来使项目可编辑。

这里写图片描述

pItem->setTextInteractionFlags(Qt::TextEditorInteraction);  // 可编辑
connect(pItem->document(), &QTextDocument::contentsChanged, [=]() {
    qDebug() << pItem->toPlainText();
});

输出如下:

“Q”
“Qt”
“Qte”
“Qter”

QGraphicsLineItem

详细描述

QGraphicsLineItem 类提供了一个直线项,可以添加到 QGraphicsScene 中。

要设置图元的直线,传递 QLineF 到 QGraphicsLineItem 的构造函数,或调用 setLine() 函数。line() 返回当前直线。默认情况下,该直线为黑色,宽度为 0,但可以通过调用 setPen() 进行更改。

这里写图片描述

QGraphicsLineItem 使用直线和画笔的宽度,为 boundingRect()、shape() 和 contains() 提供了一个合理的实现。paint() 函数使用图元关联的画笔绘制直线。

示例

这里写图片描述

// 定义一个直线项
QGraphicsLineItem *pItem = new QGraphicsLineItem();

// 设置画笔
QPen pen = pItem->pen();
pen.setColor(QColor(0, 160, 230));
pen.setWidth(5);
pItem->setPen(pen);

// 设置直线位于 (x1, y1) 和 (x2, y2)之间
pItem->setLine(QLineF(0, 0, 100, 100));

// 将文本项添加至场景中
QGraphicsScene *pScene = new QGraphicsScene();
pScene->addItem(pItem);

// 为视图设置场景
QGraphicsView *pView = new QGraphicsView();
pView->setScene(pScene);
pView->setStyleSheet("border:none; background:transparent;");

pView->show();

QGraphicsPixmapItem

详细描述

QGraphicsPixmapItem 类提供了一个图像项,可以添加到 QGraphicsScene 中。

要设置 item 的图像,传递 QPixmap 到 QGraphicsPixmapItem 的构造函数,或调用 setPixmap() 函数,pixmap() 返回当前的图像。

QGraphicsPixmapItem 使用 pixmap 的可选 alpha 掩码,为 boundingRect()、shape() 和 contains() 提供了一个合理的实现。

这里写图片描述

图像在 item 的(0,0)坐标处绘制,由 offset() 返回。可以通过调用 setOffset() 更改绘图偏移量。

可以通过调用 setTransformationMode() 设置图像的变换模式。默认情况下,使用 Qt::FastTransformation,它提供了快速、不平滑的缩放。 Qt::SmoothTransformation 在 painter 上启用 QPainter::SmoothPixmapTransform,质量取决于平台和视口。结果通常不如调用 QPixmap::scale() 直接,调用 transformMode() 获取项目的当前转换模式。

示例

这里写图片描述

// 定义一个图像 item
QGraphicsPixmapItem *pItem = new QGraphicsPixmapItem();

QPixmap image(":/Images/logo");
pItem->setPixmap(image.scaled(50, 50));

// 将图像 item 添加至场景中
QGraphicsScene *pScene = new QGraphicsScene();
pScene->addItem(pItem);

// 为视图设置场景
QGraphicsView *pView = new QGraphicsView();
pView->setScene(pScene);
pView->setStyleSheet("border:none; background:transparent;");

pView->show();

QGraphicsRectItem

详细说明

QGraphicsRectItem 类提供了一个矩形项,可以添加到 QGraphicsScene 中。

要设置 item 的矩形,可以传递一个 QRectF 到 QGraphicsRectItem 的构造函数,或调用 setRect() 函数。rect() 返回当前矩形。

这里写图片描述

QGraphicsRectItem 使用矩形和画笔宽度,为 boundingRect()、shape() 和 contains() 提供了一个合理的实现。paint() 函数使用 item 关联的画笔和画刷绘制矩形,可以通过调用 setPen() 和 setBrush() 函数来设置。

注意:无效矩形(例如,宽度或高度为负)的呈现是未定义的。如果不能确定使用的是有效的矩形(例如,如果使用来自不可靠源的数据创建的矩形),那么应该使用 QRectF::normalized() 创建标准化的矩形,然后使用它们。

示例

这里写图片描述

// 定义一个矩形项
QGraphicsRectItem  *pItem = new QGraphicsRectItem();

// 设置画笔、画刷
QPen pen = pItem->pen();
pen.setWidth(5);
pen.setColor(Qt::white);
pItem->setPen(pen);
pItem->setBrush(QBrush(QColor(0, 160, 230)));

// 矩形区域 起点:(50, 50) 宽:100 高:100
pItem->setRect(QRectF(50, 50, 100, 100));

// 将矩形项添加至场景中
QGraphicsScene *pScene = new QGraphicsScene();
pScene->addItem(pItem);

// 为视图设置场景
QGraphicsView *pView = new QGraphicsView();
pView->setScene(pScene);
pView->setStyleSheet("border:none; background:transparent;");

pView->show();

QGraphicsEllipseItem

详细说明

QGraphicsEllipseItem 类提供了一个椭圆项,可以添加到 QGraphicsScene 中。

QGraphicsEllipseItem 表示一个椭圆的填充和轮廓,也可以使用它的椭圆段(见 startAngle()、spanAngle())。

这里写图片描述 这里写图片描述

要设置 item 的椭圆,可以传递一个 QRectF 到 QGraphicsEllipseItem 的构造函数,或调用 setRect()。rect() 返回当前椭圆的几何形状。

QGraphicsEllipseItem 使用 rect 和画笔宽度,为 boundingRect()、shape() 和 contains() 提供了一个合理的实现。paint() 函数使用 item 关联的画笔和画刷来绘制椭圆,可以通过调用 setPen() 和 setBrush() 来设置。

示例

这里写图片描述

// 定义一个椭圆项
QGraphicsEllipseItem *pItem = new QGraphicsEllipseItem ();

// 设置画笔、画刷
QPen pen = pItem->pen();
pen.setWidth(5);
pen.setColor(Qt::white);
pItem->setPen(pen);
pItem->setBrush(QBrush(QColor(0, 160, 230)));

// 矩形区域 起点:(50, 50) 宽:200 高:100
pItem->setRect(QRectF(50, 50, 200, 100));
pItem->setStartAngle(16 * 90);  // 起始角度
pItem->setSpanAngle(16 * 270);  // 跨角
// 默认情况下,角度为 5760(360 * 16,一个完整的椭圆)。

// 将椭圆项添加至场景中
QGraphicsScene *pScene = new QGraphicsScene();
pScene->addItem(pItem);

// 为视图设置场景
QGraphicsView *pView = new QGraphicsView();
pView->setScene(pScene);
pView->setStyleSheet("border:none; background:transparent;");

pView->show();

QGraphicsPathItem

详细描述

QGraphicsPathItem 类提供了一个路径项,可以添加到 QGraphicsScene 中。

要设置 item 的路径,可以传递 QPainterPath 到 QGraphicsPathItem 的构造函数,或调用 setPath() 函数,path() 函数返回当前路径。

这里写图片描述

QGraphicsPathItem 使用路径,为 boundingRect()、shape() 和 contains() 提供了一个合理的实现。paint() 函数使用 item 关联的画笔和画刷来绘制路径,可以通过调用 setPen() 和 setBrush() 函数来设置。

示例

这里写图片描述

const float Pi = 3.14159f;

// 定义一个路径项
QGraphicsPathItem *pItem = new QGraphicsPathItem();

// 绘制星星
QPainterPath starPath;
starPath.moveTo(90, 50);
for (int i = 1; i < 5; ++i) {
    starPath.lineTo(50 + 40 * std::cos(0.8 * i * Pi), 50 + 40 * std::sin(0.8 * i * Pi));
}
starPath.closeSubpath();
pItem->setPath(starPath);

// 设置画笔、画刷
QPen pen = pItem->pen();
pen.setWidth(2);
pen.setColor(Qt::white);
pItem->setPen(pen);
pItem->setBrush(QBrush(QColor(0, 160, 230)));

// 将路径项添加至场景中
QGraphicsScene *pScene = new QGraphicsScene();
pScene->addItem(pItem);

// 为视图设置场景
QGraphicsView *pView = new QGraphicsView();
pView->setScene(pScene);
pView->setStyleSheet("border:none; background:transparent;");

pView->show();

QGraphicsPolygonItem

详细描述

QGraphicsPolygonItem 类提供了一个多边形项,可以添加到 QGraphicsScene 中。

要设置 item 的多边形,传递 QPolygonF 到 QGraphicsPolygonItem 的构造函数,或调用 setPolygon() 函数。polygon() 返回当前的多边形。

这里写图片描述

QGraphicsPolygonItem 使用多边形和画笔宽度,为 boundingRect()、shape() 和 contains() 提供了一个合理的实现。paint() 函数使用 item 关联的画笔和画刷绘制多边形,可以通过调用 setPen() 和 setBrush() 函数进行设置。

示例

这里写图片描述

// 定义一个多边形项
QGraphicsPolygonItem *pItem = new QGraphicsPolygonItem();

// 绘制多边形
QPolygonF polygon;
polygon << QPointF(200.0, 120.0) << QPointF(230.0, 130.0)
        << QPointF(260.0, 180.0) << QPointF(200.0, 200.0);
pItem->setPolygon(polygon);

// 设置画笔、画刷
QPen pen = pItem->pen();
pen.setWidth(2);
pen.setColor(Qt::white);
pItem->setPen(pen);
pItem->setBrush(QBrush(QColor(0, 160, 230)));

// 将多边形项添加至场景中
QGraphicsScene *pScene = new QGraphicsScene();
pScene->addItem(pItem);

// 为视图设置场景
QGraphicsView *pView = new QGraphicsView();
pView->setScene(pScene);
pView->setStyleSheet("border:none; background:transparent;");

pView->show();
作者:u011012932 发表于2016/11/7 19:47:51 原文链接
阅读:54 评论:0 查看评论
Viewing all 5930 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>