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

iOS 根据屏幕宽度自适应分布按钮

$
0
0

下载demo链接:https://github.com/MinLee6/buttonShow.git

屏幕摆放的控件有两种方式,一种根据具体内容变化,一种根据屏幕宽度变化。
下面我分别将两个方式,用代码的方式呈现:
1:根据具体内容变化


//
//  StyleOneViewController.m
//  buttonShow
//
//  Created by limin on 15/06/15.
//  Copyright © 2015年 信诺汇通信息科技(北京)有限公司. All rights reserved.
//

#import "StyleOneViewController.h"
#import "UIViewExt.h"
//每列间隔
#define KViewMargin 10
//每行列数高
#define KVieH 28

#define KscreenW [UIScreen mainScreen].bounds.size.width
#define Color(R,G,B) [UIColor colorWithRed:R/255.0 green:G/255.0 blue:B/255.0 alpha:1.0]
@interface StyleOneViewController ()
{
    UIButton *tmpBtn;
    CGFloat btnW;
    CGFloat btnViewHeight;
}
/* 存放按钮的view */
@property(nonatomic,strong)UIView *btnsView;
/* 按钮上的文字 */
@property(nonatomic,strong)NSMutableArray *btnMsgArrays;
@property (nonatomic,strong) NSMutableArray* btnIDArrays;
/** 所有按钮 */
@property(nonatomic,strong)NSMutableArray *allBtnArrays;
/** 服务器提供按钮标签 */
@property(nonatomic,strong)NSArray *tagInfoArray;
//-------展示选中的文字
/* 确认按钮 */
@property(nonatomic,strong)UIButton *sureButton;
/* 文字 */
@property(nonatomic,strong)UILabel *showLabel;
@end

@implementation StyleOneViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    [self getTagMsg];
    
}
-(void)getTagMsg
{
    self.btnMsgArrays = [[NSMutableArray alloc]init];
    _allBtnArrays = [[NSMutableArray alloc]init];
    self.btnIDArrays = [[NSMutableArray alloc]init];
    self.tagInfoArray = @[@{@"id":@"1",@"tagmsg":@"味道很好味道很好"},
                          @{@"id":@"1",@"tagmsg":@"环境不错"},
                          @{@"id":@"1",@"tagmsg":@"性价比高"},
                          @{@"id":@"1",@"tagmsg":@"位置好找"},
                          @{@"id":@"1",@"tagmsg":@"上菜快"},
                          @{@"id":@"1",@"tagmsg":@"菜量足"},
                          @{@"id":@"1",@"tagmsg":@"好吃"},
                          @{@"id":@"1",@"tagmsg":@"态度好,服务周到"}
                          ];
    
    //挨个赋值
    for (int i=0; i<_tagInfoArray.count; i++) {
        NSDictionary *dict = _tagInfoArray[i];
        [self.btnIDArrays addObject:dict[@"id"]];
        [self.btnMsgArrays addObject:dict[@"tagmsg"]];
    }
    [self createBtns];
}
//创建按钮
-(void)createBtns{
    //创建放置button的view
    self.btnsView = [[UIView alloc]initWithFrame:CGRectMake(10, 40, KscreenW-2*KViewMargin, 40)];
    self.btnsView.backgroundColor = Color(237, 237, 237);
    [self.view addSubview:self.btnsView];
    /**
     *  数组存放适配屏幕大小的每行按钮的个数
     */
    NSMutableArray *indexbtns=[self returnBtnsForRowAndCol];
    //统计按钮View的高度
    btnViewHeight=indexbtns.count*(KVieH+KViewMargin)+10;
    
    //设置btnView的高度
    self.btnsView.height=btnViewHeight;
    
    NSInteger count=0;
    CGFloat Y;
    
    //循环创建按钮
    for (int row=0; row<indexbtns.count; row++) {
        for (int col=0; col<[indexbtns[row]intValue]; col++) {
            
            CGFloat X;
            Y=10+row*(KViewMargin+KVieH);
            
            //按钮的宽
            btnW=[self returnBtnWithWithStr:self.btnMsgArrays[count]];
            
            if (tmpBtn&&col) {
                X=CGRectGetMaxX(tmpBtn.frame)+KViewMargin;
            }else{
                X=KViewMargin+col*btnW;
            }
            
            
            UIButton *btn=[[UIButton alloc]initWithFrame:CGRectMake(X, Y, btnW, KVieH)];
            
            [btn setTitle:self.btnMsgArrays[count] forState:UIControlStateNormal];
            btn.titleLabel.font=[UIFont systemFontOfSize:12];
            btn.layer.borderWidth=1;
            btn.layer.borderColor = Color(156,156,156).CGColor;
            
            [btn setTitleColor:Color(156, 156, 156) forState:UIControlStateNormal];
            [btn setTitleColor:Color(202, 48, 130) forState:UIControlStateSelected];
            btn.backgroundColor=[UIColor clearColor];
            btn.layer.cornerRadius=2;
            btn.tag=[self.btnIDArrays[count] integerValue];
            [btn addTarget:self action:@selector(btnClick:) forControlEvents:UIControlEventTouchUpInside];
            [self.allBtnArrays addObject:btn];
            tmpBtn=btn;
            [self.btnsView addSubview:btn];
            count+=1;
        }
    }
    
    //创建确认按钮
    UIButton *btn = [[UIButton alloc]initWithFrame:CGRectMake(KViewMargin, self.btnsView.bottom+40, KscreenW-2*KViewMargin, 44)];
    [btn setTitle:@"确认" forState:UIControlStateNormal];
    [btn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
    btn.backgroundColor = Color(214, 116, 0);
    [btn addTarget:self action:@selector(showSelectedClick:) forControlEvents:UIControlEventTouchUpInside];
    self.sureButton = btn;
    [self.view addSubview:btn];
    //返回按钮
    UIButton *btn2 = [[UIButton alloc]initWithFrame:CGRectMake((KscreenW-80)*0.5, self.view.height-80, 80, 80)];
    [btn2 setTitle:@"返回" forState:UIControlStateNormal];
    [btn2 setTitleColor:[UIColor purpleColor] forState:UIControlStateNormal];
    btn2.layer.cornerRadius = 40;
    btn2.clipsToBounds = YES;
    btn2.layer.borderColor = [UIColor purpleColor].CGColor;
    btn2.layer.borderWidth = 2;
    [btn2 addTarget:self action:@selector(backBtnClick:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:btn2];
    
}
#pragma mark-返回view中有几行几列按钮
-(NSMutableArray*)returnBtnsForRowAndCol{
    CGFloat allWidth = 0.0;
    NSInteger countW=0;
    NSMutableArray *indexbtns=[NSMutableArray array];
    NSMutableArray *tmpbtns=[NSMutableArray array];
    for (int j=0;j<self.btnMsgArrays.count;j++) {
        CGFloat width=[self returnBtnWithWithStr:self.btnMsgArrays[j]];
        allWidth+=width+KViewMargin;
        countW+=1;
        if (allWidth>KscreenW-10) {
            //判断第一行情况
            NSInteger lastNum=[[tmpbtns lastObject]integerValue];
            [indexbtns addObject:@(lastNum)];
            
            [tmpbtns removeAllObjects];
            allWidth=0.0;
            countW=0;
            j-=1;
        }else{
            [tmpbtns addObject:@(countW)];
            
        }
        
    }
    if (tmpbtns.count!=0) {
        NSInteger lastNum=[[tmpbtns lastObject]integerValue];
        [indexbtns addObject:@(lastNum)];
    }
    return indexbtns;
}
-(CGFloat)returnBtnWithWithStr:(NSString *)str{
    //计算字符长度
    NSDictionary *minattributesri = @{NSFontAttributeName:[UIFont systemFontOfSize:12]};
    CGSize mindetailSizeRi = [str boundingRectWithSize:CGSizeMake(100, MAXFLOAT) options:NSStringDrawingUsesFontLeading attributes:minattributesri context:nil].size;
    return mindetailSizeRi.width+12;
    
}
#pragma mark-按钮点击事件
-(void)btnClick:(UIButton *)btn
{
    btn.selected = !btn.isSelected;
    if (btn.isSelected) {
        btn.layer.borderColor = Color(202, 48, 130).CGColor;
    }else
    {
        btn.layer.borderColor = Color(156, 156, 156).CGColor;
    }
}
-(void)showSelectedClick:(UIButton *)btn
{
    NSMutableArray *strArray = [[NSMutableArray alloc]init];
    for (UIButton *btn in self.allBtnArrays) {
        if (btn.isSelected) {
            [strArray addObject:btn.currentTitle];
        }
    }
    NSString *str = [strArray componentsJoinedByString:@" & "];
    UIFont *font = [UIFont systemFontOfSize:14];
    CGFloat strH = [str boundingRectWithSize:CGSizeMake(KscreenW-2*KViewMargin, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:font} context:nil].size.height;
    //展示文字
    if (!self.showLabel) {
        UILabel *label = [[UILabel alloc]initWithFrame:CGRectMake(KViewMargin, self.sureButton.bottom+20, KscreenW-2*KViewMargin, strH)];
        label.font = font;
        label.textColor = [UIColor redColor];
        label.numberOfLines = 0;
        self.showLabel = label;
        [self.view addSubview:label];
    }
    self.showLabel.text = str;
    self.showLabel.height = strH;
    
}
-(void)backBtnClick:(UIButton *)btn
{
    [self dismissViewControllerAnimated:YES completion:nil];
}
- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

/*
#pragma mark - Navigation

// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    // Get the new view controller using [segue destinationViewController].
    // Pass the selected object to the new view controller.
}
*/

@end




2:根据屏幕宽度变化。


//
//  StyleTwoViewController.m
//  buttonShow
//
//  Created by limin on 16/11/15.
//  Copyright © 2016年 君安信(北京)科技有限公司. All rights reserved.
//

#import "StyleTwoViewController.h"
#import "UIViewExt.h"
#define kTagMargin 14
#define kCellMargin 15
#define kScreenWidth [UIScreen mainScreen].bounds.size.width
#define Color(R,G,B) [UIColor colorWithRed:R/255.0 green:G/255.0 blue:B/255.0 alpha:1.0]
@interface StyleTwoViewController ()
/* 按钮 */
@property(nonatomic,strong)NSMutableArray *btnsArray;
/* 按钮文字 */
@property(nonatomic,strong)NSArray *tagsArray;
/* 默认选择的按钮 */
@property(nonatomic,strong)UIButton *selectedBtn;
/* 标签 */
@property(nonatomic,copy)NSString *selectTopicTitle;
@end

@implementation StyleTwoViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    [self getTagMsg];
}
-(void)getTagMsg
{
    _btnsArray = [NSMutableArray array];
    //添加tag按钮
    NSArray *tagsArray = @[@"美好生活1",@"美好生活2",@"美好生活3",@"美好生活4",@"美好生活5",@"美好生活6",@"美好生活7",@"美好生活8",@"美好生活9",@"美好生活10",@"美好生活11",@"美好生活12",@"美好生活13",@"美好生活14",@"美好生活15",@"美好生活16",@"美好生活17",@"美好生活18",@"美好生活19",@"美好生活20",@"美好生活21",@"美好生活22",@"美好生活23",@"美好生活24"];
    _tagsArray = tagsArray;
    [self createTagSqures:tagsArray];
}
#pragma mark - 创建方块
-(void)createTagSqures:(NSArray *)tags
{
    //一行最多4个。
    int maxCols = 4;
    //宽度、高度
    CGFloat TotalWidth = kScreenWidth - 2*kCellMargin - (maxCols-1)*kTagMargin;
    CGFloat BtnWidth = TotalWidth / maxCols;
    CGFloat BtnHeight = 30;
    for (int i=0; i<tags.count; i++) {
        
        //创建按钮
        UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
        btn.backgroundColor = Color(211, 156, 4);
        //设置按钮属性
        [btn setBackgroundImage:[UIImage imageNamed:@"discover_btnbg"] forState:UIControlStateNormal];
        [btn setBackgroundImage:[UIImage imageNamed:@"discover_btnbg"] forState:UIControlStateDisabled];
        btn.adjustsImageWhenHighlighted = NO;
        [btn setTitleColor:Color(51, 51, 51) forState:UIControlStateNormal];
        [btn setTitleColor:[UIColor whiteColor] forState:UIControlStateDisabled];
        [btn.titleLabel setFont:[UIFont systemFontOfSize:13]];
        btn.titleLabel.textAlignment = NSTextAlignmentCenter;
        btn.tag = i+10;
        
        // 监听点击
        [btn addTarget:self action:@selector(TagBtnClick:) forControlEvents:UIControlEventTouchUpInside];
        
        //按钮赋值
        [btn setTitle:tags[i] forState:UIControlStateNormal];
        
        [self.view addSubview:btn];
        
        //计算frame
        int col = i % maxCols;
        int row = i / maxCols;
        btn.x = kCellMargin + col*(BtnWidth + kTagMargin);
        btn.y = 40 + 11 + row * (BtnHeight + kTagMargin);
        btn.width = BtnWidth;
        btn.height = BtnHeight;
        //设置所有按钮默认状态为NO 。
        btn.enabled = YES;
        [self.btnsArray addObject:btn];
    }
    
    
    //默认第一个按钮为选中
    UIButton *btn = self.btnsArray[0];
    btn.enabled = NO;
    self.selectedBtn = btn;
    self.selectTopicTitle = self.tagsArray[0];
    //返回按钮
    UIButton *btn2 = [[UIButton alloc]initWithFrame:CGRectMake((kScreenWidth-80)*0.5, self.view.height-160, 80, 80)];
    [btn2 setTitle:@"返回" forState:UIControlStateNormal];
    [btn2 setTitleColor:[UIColor purpleColor] forState:UIControlStateNormal];
    btn2.layer.cornerRadius = 40;
    btn2.clipsToBounds = YES;
    btn2.layer.borderColor = [UIColor purpleColor].CGColor;
    btn2.layer.borderWidth = 2;
    [btn2 addTarget:self action:@selector(backBtnClick:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:btn2];
    //创建确认按钮
    UIButton *btn3 = [[UIButton alloc]initWithFrame:CGRectMake(kCellMargin, btn2.top-80, kScreenWidth-2*kCellMargin, 44)];
    [btn3 setTitle:@"确认" forState:UIControlStateNormal];
    [btn3 setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
    btn3.backgroundColor = Color(214, 116, 0);
    [btn3 addTarget:self action:@selector(showSelectedClick:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:btn3];
    
}
#pragma mark - 按钮点击事件
- (void)TagBtnClick:(UIButton *)button
{
    //获取点击的button,取消选中样式
    self.selectedBtn.enabled = YES;
    button.enabled = NO;
    self.selectedBtn = button;
    
    
    NSInteger index = button.tag-10;
    NSString *selectStr = self.tagsArray[index];
    
    self.selectTopicTitle = selectStr;
    
}
-(void)showSelectedClick:(UIButton *)button
{
    //保留选择标签的文字
    NSLog(@"所选择的文字:%@",self.selectTopicTitle);
    UIAlertView *alert = [[UIAlertView alloc]initWithTitle:self.selectTopicTitle message:@"" delegate:nil cancelButtonTitle:@"确认" otherButtonTitles:nil];
    [alert show];
}
-(void)backBtnClick:(UIButton *)btn
{
    [self dismissViewControllerAnimated:YES completion:nil];
}
- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

/*
#pragma mark - Navigation

// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    // Get the new view controller using [segue destinationViewController].
    // Pass the selected object to the new view controller.
}
*/

@end


作者:Leemin_ios 发表于2016/11/15 20:37:03 原文链接
阅读:35 评论:0 查看评论

水平/垂直滚动联动

$
0
0

证券炒股软件经常会遇到有很多Tab要显示,使得控件的上下/垂直滚动联动经常要使用到,比如撤单,查询等业务都要用到这个控件,今天刚好项目没那么紧,就把这个控件的实现总结一下。
先看看我们实现后的效果效果图
分析:这里我们用一个水平滑动控件HorizontalScrollView和一个ListView组合实现水平滑动和垂直滑动
一、自定义一个LinearLayout的视图容器控件,截取掉子控件的触摸监听事件,阻止 拦截 ontouch事件传递给其子控件
步骤:新建一个ObserverHScrollViewIntercept类继承LinearLayout重写onInterceptTouchEvent方法,把触摸事件消费掉,不要往子类继续传递。

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.HorizontalScrollView;

/**
 *一个视图容器控件
 * 阻止 拦截 ontouch事件传递给其子控件
 * Created by willkong on 2016/11/15.
 */

public class ObserverHScrollViewIntercept extends LinearLayout{
    public ObserverHScrollViewIntercept(Context context) {
        this(context,null);
    }

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

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

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return true;
    }
}

二、新建一个ObserverHScrollView类继承HorizontalScrollView
这里主要的技术是写了一个监听观察者,ScrollViewObserver。先给控件添加滑动变化的监听,只要用户一滑动,就通知观察者。
先在该类中写一个观察者类ScrollViewObserver。这个类的主要作用是把监听到的滑动变化回调给调用者,所以,我们要先写一个滑动变化监听回调接口OnScrollChangedListener

    /*
     * 当发生了滚动事件时,调用这个接口,根据坐标滑动到对应的位置
     */
    public static interface OnScrollChangedListener {
        public void onScrollChanged(int l, int t, int oldl, int oldt);
    }

在观察者类中定义一个list,把监听到的每一次的滚动事件保存起来。并且写两个添加监听和移除监听的方法,还有一个回调接口方法,把监听到的发生滚动事件回调给调用者。

    /*
     * 观察者
     */
    public static class ScrollViewObserver {
        List<OnScrollChangedListener> mList;

        public ScrollViewObserver() {
            super();
            mList = new ArrayList<OnScrollChangedListener>();
        }

        public void AddOnScrollChangedListener(OnScrollChangedListener listener) {
            mList.add(listener);
        }

        public void RemoveOnScrollChangedListener(
                OnScrollChangedListener listener) {
            mList.remove(listener);
        }

        public void NotifyOnScrollChanged(int l, int t, int oldl, int oldt) {
            if (mList == null || mList.size() == 0) {
                return;
            }
            for (int i = 0; i < mList.size(); i++) {
                if (mList.get(i) != null) {
                    mList.get(i).onScrollChanged(l, t, oldl, oldt);
                }
            }
        }
    }

整个类代码:


import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.HorizontalScrollView;

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

/**
 * 自定义的 滚动控件
 * 重载了 onScrollChanged(滚动条变化),监听每次的变化通知给 观察(此变化的)观察者
 * 可使用 AddOnScrollChangedListener 来订阅本控件的 滚动条变化
 * Created by willkong on 2016/11/15.
 */

public class ObserverHScrollView extends HorizontalScrollView{
    //新建一个监听者,接口回调使用
    ScrollViewObserver mScrollViewObserver = new ScrollViewObserver();

    public ObserverHScrollView(Context context) {
        this(context,null);
    }

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

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

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        return super.onTouchEvent(ev);
    }

    /**
     * 监听控件的滑动变化
     * @param l
     * @param t
     * @param oldl
     * @param oldt
     */
    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        /*
         * 当滚动条移动后,引发 滚动事件。通知给观察者,观察者会传达给其他的。
         */
        if (mScrollViewObserver != null /*&& (l != oldl || t != oldt)*/) {
            mScrollViewObserver.NotifyOnScrollChanged(l, t, oldl, oldt);
        }
        super.onScrollChanged(l, t, oldl, oldt);
    }

    /*
     * 订阅 本控件 的 滚动条变化事件
     * */
    public void AddOnScrollChangedListener(OnScrollChangedListener listener) {
        mScrollViewObserver.AddOnScrollChangedListener(listener);
    }

    /*
     * 取消 订阅 本控件 的 滚动条变化事件
     * */
    public void RemoveOnScrollChangedListener(OnScrollChangedListener listener) {
        mScrollViewObserver.RemoveOnScrollChangedListener(listener);
    }


    /**
     * 观察者
     */
    public static class ScrollViewObserver{
        List<OnScrollChangedListener> mList;
        public ScrollViewObserver(){
            super();
            mList = new ArrayList<OnScrollChangedListener>();

        }

        /**
         * 订阅 本控件 的 滚动条变化事件
         * @param listener
         */
        public void AddOnScrollChangedListener(OnScrollChangedListener listener) {
            mList.add(listener);
        }

        /**
         * 取消 订阅 本控件 的 滚动条变化事件
         * @param listener
         */
        public void RemoveOnScrollChangedListener(
                OnScrollChangedListener listener) {
            mList.remove(listener);
        }

        public void NotifyOnScrollChanged(int l, int t, int oldl, int oldt) {
            if (mList == null || mList.size() == 0) {
                return;
            }
            for (int i = 0; i < mList.size(); i++) {
                if (mList.get(i) != null) {
                    mList.get(i).onScrollChanged(l, t, oldl, oldt);
                }
            }
        }

    }

    /*
     * 当发生了滚动事件时,调用这个接口
     */
    public static interface OnScrollChangedListener {
        public void onScrollChanged(int l, int t, int oldl, int oldt);
    }
}

到这里控件就已经编写完毕了,下面我们来讲,怎么使用这个控件。
三、新建一个类HVScorllListviewActivity
首先新建一个头部标题ativity_hvscorll_listview_item的布局文件

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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:background="@drawable/demo_list_selector"
    android:descendantFocusability="blocksDescendants"
    android:orientation="horizontal"
    android:paddingBottom="10dp"
    android:paddingTop="10dp"
    tools:context="com.hvscorlllistviewdemo.HVScorllListviewActivity">
    <TextView
        android:id="@+id/textView1"
        android:layout_width="@dimen/bond_table_header_width"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_centerVertical="true"
        android:gravity="center"
        android:text="@string/bound_search_result_head_zqmc"
        android:textColor="@color/black"
        android:textSize="@dimen/bond_table_header_text" />
    <com.hvscorlllistviewdemo.ObserverHScrollViewIntercept
        android:id="@+id/scroollContainter"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_centerVertical="true"
        android:layout_toRightOf="@id/textView1"
        android:focusable="false">
        <com.hvscorlllistviewdemo.ObserverHScrollView
            android:id="@+id/horizontalScrollView1"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:focusable="false"
            android:scrollbars="none">
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:focusable="false"
                android:gravity="center"
                android:orientation="horizontal">

                <TextView
                    android:id="@+id/textView2"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_zqjc"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />

                <TextView
                    android:id="@+id/textView3"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_zqdm"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />

                <TextView
                    android:id="@+id/textView4"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_fxjc"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />

                <TextView
                    android:id="@+id/textView5"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_ksr"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />

                <TextView
                    android:id="@+id/textView6"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_fxjg"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />

                <TextView
                    android:id="@+id/textView7"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_sjfxze"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />

                <TextView
                    android:id="@+id/textView8"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_jxfs"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />

                <TextView
                    android:id="@+id/textView9"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_fxzq"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />

                <TextView
                    android:id="@+id/textView10"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_pmll"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />

                <TextView
                    android:id="@+id/textView11"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_lc"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />

                <TextView
                    android:id="@+id/textView12"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_zjll"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />

                <TextView
                    android:id="@+id/textView13"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_zqqx"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />

                <TextView
                    android:id="@+id/textView14"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_zqpj"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />

                <TextView
                    android:id="@+id/textView15"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_ztpj"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />

                <TextView
                    android:id="@+id/textView16"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_zqpjjg"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />

                <TextView
                    android:id="@+id/textView17"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_ztpjjg"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />

                <TextView
                    android:id="@+id/textView18"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_zqzwdjr"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />

                <TextView
                    android:id="@+id/textView19"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_ltr"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />

                <TextView
                    android:id="@+id/textView20"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_jzghr"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />

                <TextView
                    android:id="@+id/textView21"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_qxr"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />

                <TextView
                    android:id="@+id/textView22"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_dqr"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />
            </LinearLayout>
        </com.hvscorlllistviewdemo.ObserverHScrollView>
    </com.hvscorlllistviewdemo.ObserverHScrollViewIntercept>
</RelativeLayout>

接下来编写主类的布局文件activity_hvscorll_listview.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical">

    <include
        android:id="@+id/head"
        layout="@layout/activity_hvscorll_listview_head" />

    <ListView
        android:id="@+id/listView1"
        android:layout_weight="1"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:scrollbars="none"/>

</LinearLayout>

最后在主文件HVScorllListviewActivity中找到对应的控件:下面有两个必须注意的是要把头部控件必须设置
两个属性 mListviewHead.setFocusable(true);//将控件设置成可获取焦点状态,默认是无法获取焦点的,只有设置成true,才能获取控件的点击事件
mListviewHead.setClickable(true);//设置为true时,表明控件可以点击两个属性必须同时设置,不然点击头部是不能滑动的。
为了HorizontalScrollView和ListView控件联动起来,必须重写一个触摸事件,把这个触摸事件传递给这两个控件,使他们联动起来。

     /**
     * 列头/Listview触摸事件监听器<br>
     * 当在列头 和 listView控件上touch时,将这个touch的事件分发给 ScrollView
     */
    private View.OnTouchListener mHeadListTouchLinstener = new View.OnTouchListener() {

        @Override
        public boolean onTouch(View v, MotionEvent event) {
            mHorizontalScrollView.onTouchEvent(event);
            return false;
        }
    };

最后设置ListView的适配器,并且把触摸事件传递给每一个Item使他们整体联动起来,再把这个滚动的状态通过接口回调回去,实现控件的接口方法,smoothScrollTo(l,t)移动到对应位置

     /**
     * 实现接口,获得滑动回调
     */
    private class OnScrollChangedListenerImp implements ObserverHScrollView.OnScrollChangedListener {
        ObserverHScrollView mScrollViewArg;

        public OnScrollChangedListenerImp(ObserverHScrollView scrollViewar) {
            mScrollViewArg = scrollViewar;
        }

        @Override
        public void onScrollChanged(int l, int t, int oldl, int oldt) {
            mScrollViewArg.smoothScrollTo(l, t);
        }
    }

下面是在适配器中把他们联动起来的代码

 //列表水平滚动条
 ObserverHScrollView scrollView1 = (ObserverHScrollView) 
 convertView.findViewById(R.id.horizontalScrollView1);
 holder.scrollView = (ObserverHScrollView) convertView.findViewById
(R.id.horizontalScrollView1);
//列表表头滚动条
ObserverHScrollView headSrcrollView = (ObserverHScrollView) 
mListviewHead.findViewById(R.id.horizontalScrollView1);
headSrcrollView.AddOnScrollChangedListener(new OnScrollChangedListenerImp
(scrollView1));

到这里整个控件的使用就完成了。
下面是每个文件的代码:
ObserverHScrollViewIntercept类

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.LinearLayout;

/**
 *一个视图容器控件
 * 阻止 拦截 ontouch事件传递给其子控件
 * Created by willkong on 2016/11/15.
 */

public class ObserverHScrollViewIntercept extends LinearLayout{
    public ObserverHScrollViewIntercept(Context context) {
        this(context,null);
    }

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

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

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return true;
    }
}

ObserverHScrollView类

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.HorizontalScrollView;

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

/**
 * 自定义的 滚动控件
 * 重载了 onScrollChanged(滚动条变化),监听每次的变化通知给 观察(此变化的)观察者
 * 可使用 AddOnScrollChangedListener 来订阅本控件的 滚动条变化
 * Created by willkong on 2016/11/15.
 */

public class ObserverHScrollView extends HorizontalScrollView{

    ScrollViewObserver mScrollViewObserver = new ScrollViewObserver();

    public ObserverHScrollView(Context context) {
        this(context,null);
    }

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

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

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        return super.onTouchEvent(ev);
    }

    /**
     * 监听控件的滑动变化
     * @param l
     * @param t
     * @param oldl
     * @param oldt
     */
    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        /*
         * 当滚动条移动后,引发 滚动事件。通知给观察者,观察者会传达给其他的。
         */
        if (mScrollViewObserver != null /*&& (l != oldl || t != oldt)*/) {
            mScrollViewObserver.NotifyOnScrollChanged(l, t, oldl, oldt);
        }
        super.onScrollChanged(l, t, oldl, oldt);
    }

    /*
     * 订阅 本控件 的 滚动条变化事件
     * */
    public void AddOnScrollChangedListener(OnScrollChangedListener listener) {
        mScrollViewObserver.AddOnScrollChangedListener(listener);
    }

    /*
     * 取消 订阅 本控件 的 滚动条变化事件
     * */
    public void RemoveOnScrollChangedListener(OnScrollChangedListener listener) {
        mScrollViewObserver.RemoveOnScrollChangedListener(listener);
    }


    /**
     * 观察者
     */
    public static class ScrollViewObserver{
        List<OnScrollChangedListener> mList;
        public ScrollViewObserver(){
            super();
            mList = new ArrayList<OnScrollChangedListener>();

        }

        /**
         * 订阅 本控件 的 滚动条变化事件
         * @param listener
         */
        public void AddOnScrollChangedListener(OnScrollChangedListener listener) {
            mList.add(listener);
        }

        /**
         * 取消 订阅 本控件 的 滚动条变化事件
         * @param listener
         */
        public void RemoveOnScrollChangedListener(
                OnScrollChangedListener listener) {
            mList.remove(listener);
        }

        public void NotifyOnScrollChanged(int l, int t, int oldl, int oldt) {
            if (mList == null || mList.size() == 0) {
                return;
            }
            for (int i = 0; i < mList.size(); i++) {
                if (mList.get(i) != null) {
                    mList.get(i).onScrollChanged(l, t, oldl, oldt);
                }
            }
        }

    }

    /*
     * 当发生了滚动事件时,调用这个接口
     */
    public static interface OnScrollChangedListener {
        public void onScrollChanged(int l, int t, int oldl, int oldt);
    }
}

activity_hvscorll_listview_head.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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:background="@drawable/demo_list_selector"
    android:descendantFocusability="blocksDescendants"
    android:orientation="horizontal"
    android:paddingBottom="10dp"
    android:paddingTop="10dp"
    tools:context="com.hvscorlllistviewdemo.HVScorllListviewActivity">
    <TextView
        android:id="@+id/textView1"
        android:layout_width="@dimen/bond_table_header_width"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_centerVertical="true"
        android:gravity="center"
        android:text="@string/bound_search_result_head_zqmc"
        android:textColor="@color/black"
        android:textSize="@dimen/bond_table_header_text" />
    <com.hvscorlllistviewdemo.ObserverHScrollViewIntercept
        android:id="@+id/scroollContainter"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_centerVertical="true"
        android:layout_toRightOf="@id/textView1"
        android:focusable="false">
        <com.hvscorlllistviewdemo.ObserverHScrollView
            android:id="@+id/horizontalScrollView1"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:focusable="false"
            android:scrollbars="none">
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:focusable="false"
                android:gravity="center"
                android:orientation="horizontal">

                <TextView
                    android:id="@+id/textView2"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_zqjc"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />

                <TextView
                    android:id="@+id/textView3"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_zqdm"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />

                <TextView
                    android:id="@+id/textView4"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_fxjc"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />

                <TextView
                    android:id="@+id/textView5"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_ksr"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />

                <TextView
                    android:id="@+id/textView6"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_fxjg"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />

                <TextView
                    android:id="@+id/textView7"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_sjfxze"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />

                <TextView
                    android:id="@+id/textView8"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_jxfs"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />

                <TextView
                    android:id="@+id/textView9"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_fxzq"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />

                <TextView
                    android:id="@+id/textView10"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_pmll"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />

                <TextView
                    android:id="@+id/textView11"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_lc"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />

                <TextView
                    android:id="@+id/textView12"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_zjll"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />

                <TextView
                    android:id="@+id/textView13"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_zqqx"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />

                <TextView
                    android:id="@+id/textView14"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_zqpj"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />

                <TextView
                    android:id="@+id/textView15"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_ztpj"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />

                <TextView
                    android:id="@+id/textView16"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_zqpjjg"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />

                <TextView
                    android:id="@+id/textView17"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_ztpjjg"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />

                <TextView
                    android:id="@+id/textView18"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_zqzwdjr"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />

                <TextView
                    android:id="@+id/textView19"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_ltr"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />

                <TextView
                    android:id="@+id/textView20"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_jzghr"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />

                <TextView
                    android:id="@+id/textView21"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_qxr"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />

                <TextView
                    android:id="@+id/textView22"
                    android:layout_width="@dimen/bond_table_header_width"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:text="@string/bound_search_result_head_dqr"
                    android:textColor="@color/black"
                    android:textSize="@dimen/bond_table_header_text" />
            </LinearLayout>
        </com.hvscorlllistviewdemo.ObserverHScrollView>
    </com.hvscorlllistviewdemo.ObserverHScrollViewIntercept>
</RelativeLayout>

activity_hvscorll_listview.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical">

    <include
        android:id="@+id/head"
        layout="@layout/activity_hvscorll_listview_head" />

    <ListView
        android:id="@+id/listView1"
        android:layout_weight="1"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:scrollbars="none"/>

</LinearLayout>

HVScorllListviewActivitylei类


import android.app.Activity;
import android.content.Context;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.HorizontalScrollView;
import android.widget.ListView;
import android.widget.RelativeLayout;
import android.widget.TextView;

import static android.R.attr.data;

public class HVScorllListviewActivity extends Activity {
    private MyAdapter mAdapter;

    /**
     * 列表表头容器
     **/
    private RelativeLayout mListviewHead;
    /**
     * 列表ListView
     **/
    private ListView mListView;

    /**
     * 列表ListView水平滚动条
     **/
    private HorizontalScrollView mHorizontalScrollView;

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

    private void initView() {
        //初始化列表表头
        mListviewHead = (RelativeLayout) findViewById(R.id.head);
        //下面这个两个属性必须同时设置,不然点击头部是不能滑动的
        mListviewHead.setFocusable(true);//将控件设置成可获取焦点状态,默认是无法获取焦点的,只有设置成true,才能获取控件的点击事件
        mListviewHead.setClickable(true);//设置为true时,表明控件可以点击
        mListviewHead.setBackgroundColor(ContextCompat.getColor(this,R.color.table_header));
        mListviewHead.setOnTouchListener(mHeadListTouchLinstener);//头部设置触摸事件同时把触摸事件传递给水平滑动控件
        mHorizontalScrollView = (HorizontalScrollView) mListviewHead.findViewById(R.id.horizontalScrollView1);

        //初始化listview
        mListView = (ListView) findViewById(R.id.listView1);
        //准备数据
        //设置适配器
        mAdapter = new MyAdapter(this,mListviewHead);
        mListView.setOnTouchListener(mHeadListTouchLinstener);
        mListView.setAdapter(mAdapter);

    }

    /**
     * 列头/Listview触摸事件监听器<br>
     * 当在列头 和 listView控件上touch时,将这个touch的事件分发给 ScrollView
     */
    private View.OnTouchListener mHeadListTouchLinstener = new View.OnTouchListener() {

        @Override
        public boolean onTouch(View v, MotionEvent event) {
            mHorizontalScrollView.onTouchEvent(event);
            return false;
        }
    };

    class MyAdapter extends BaseAdapter{

        /**列表表头容器**/
        private RelativeLayout mListviewHead;
        private Context mContext;

        public MyAdapter(Context context,RelativeLayout mListviewHead){
            this.mContext = context;
            this.mListviewHead = mListviewHead;
        }

        @Override
        public int getCount() {
            return 20;
        }

        @Override
        public Object getItem(int position) {
            return null;
        }

        @Override
        public long getItemId(int position) {
            return 0;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            // 查找控件
            ViewHolder holder = null;
            if (null == convertView) {
                convertView = LayoutInflater.from(mContext).inflate(R.layout.activity_hvscorll_listview_head, null);
                holder = new ViewHolder();

                holder.txt1 = (TextView) convertView.findViewById(R.id.textView1);
                holder.txt2 = (TextView) convertView.findViewById(R.id.textView2);
                holder.txt3 = (TextView) convertView.findViewById(R.id.textView3);
                holder.txt4 = (TextView) convertView.findViewById(R.id.textView4);
                holder.txt5 = (TextView) convertView.findViewById(R.id.textView5);
                holder.txt6 = (TextView) convertView.findViewById(R.id.textView6);
                holder.txt7 = (TextView) convertView.findViewById(R.id.textView7);
                holder.txt8 = (TextView) convertView.findViewById(R.id.textView8);
                holder.txt9 = (TextView) convertView.findViewById(R.id.textView9);
                holder.txt10 = (TextView) convertView.findViewById(R.id.textView10);
                holder.txt11 = (TextView) convertView.findViewById(R.id.textView11);
                holder.txt12 = (TextView) convertView.findViewById(R.id.textView12);
                holder.txt13 = (TextView) convertView.findViewById(R.id.textView13);
                holder.txt14 = (TextView) convertView.findViewById(R.id.textView14);
                holder.txt15 = (TextView) convertView.findViewById(R.id.textView15);
                holder.txt16 = (TextView) convertView.findViewById(R.id.textView16);
                holder.txt17 = (TextView) convertView.findViewById(R.id.textView17);
                holder.txt18 = (TextView) convertView.findViewById(R.id.textView18);
                holder.txt19 = (TextView) convertView.findViewById(R.id.textView19);
                holder.txt20 = (TextView) convertView.findViewById(R.id.textView20);
                holder.txt21 = (TextView) convertView.findViewById(R.id.textView21);
                holder.txt22 = (TextView) convertView.findViewById(R.id.textView22);
                //列表水平滚动条
                ObserverHScrollView scrollView1 = (ObserverHScrollView) convertView.findViewById(R.id.horizontalScrollView1);
                holder.scrollView = (ObserverHScrollView) convertView.findViewById(R.id.horizontalScrollView1);
                //列表表头滚动条
                ObserverHScrollView headSrcrollView = (ObserverHScrollView) mListviewHead.findViewById(R.id.horizontalScrollView1);
                headSrcrollView.AddOnScrollChangedListener(new OnScrollChangedListenerImp(scrollView1));

                convertView.setTag(holder);

            } else {
                holder = (ViewHolder) convertView.getTag();
            }

            holder.txt1.setText("国债");
            holder.txt2.setText("简称");
            holder.txt3.setText("000000");
            holder.txt4.setText("财政部");
            holder.txt5.setText("2016-11-15");
            holder.txt6.setText("100.00");
            holder.txt7.setText("200.00");
            holder.txt8.setText("附息式固定利率");
            holder.txt9.setText("12");
            holder.txt10.setText("2.4200");
            holder.txt11.setText("--");
            holder.txt12.setText("--");
            holder.txt13.setText("--");
            holder.txt14.setText("--");
            holder.txt15.setText("--");
            holder.txt16.setText("--");
            holder.txt17.setText("--");
            holder.txt18.setText("--");
            holder.txt19.setText("--");
            holder.txt20.setText("--");
            holder.txt21.setText("--");
            holder.txt22.setText("结束");

            return convertView;
        }
    }

    /**
     * 实现接口,获得滑动回调
     */
    private class OnScrollChangedListenerImp implements ObserverHScrollView.OnScrollChangedListener {
        ObserverHScrollView mScrollViewArg;

        public OnScrollChangedListenerImp(ObserverHScrollView scrollViewar) {
            mScrollViewArg = scrollViewar;
        }

        @Override
        public void onScrollChanged(int l, int t, int oldl, int oldt) {
            mScrollViewArg.smoothScrollTo(l, t);
        }
    }

    private class ViewHolder {
        TextView txt1;
        TextView txt2;
        TextView txt3;
        TextView txt4;
        TextView txt5;
        TextView txt6;
        TextView txt7;
        TextView txt8;
        TextView txt9;
        TextView txt10;
        TextView txt11;
        TextView txt12;
        TextView txt13;
        TextView txt14;
        TextView txt15;
        TextView txt16;
        TextView txt17;
        TextView txt18;
        TextView txt19;
        TextView txt20;
        TextView txt21;
        TextView txt22;
        HorizontalScrollView scrollView;
    }
}
作者:jiang547860818 发表于2016/11/15 21:09:39 原文链接
阅读:30 评论:0 查看评论

Qt之FTP上传/下载

$
0
0

简述

为了方便网络编程,Qt 提供了 Network 模块。该模块包含了许多类,例如:QFtp - 能够更加轻松使用 FTP 协议进行网络编程。

但是,从 Qt5.x 之后,Qt Network 发生了很大的变化,助手中关于此部分描述如下:

The QFtp and QUrlInfo classes are no longer exported. Use QNetworkAccessManager instead. Programs that require raw FTP or HTTP streams can use the Qt FTP and Qt HTTP compatibility add-on modules that provide the QFtp and QHttp classes as they existed in Qt 4.

意思是说:不再导出 QFtp 和 QUrlInfo 类,改用 QNetworkAccessManager。

开启 FTP 服务

Linux 下实现 FTP 服务的软件很多,最常见的有:vsftpd、Wu-ftpd 和 Proftp 等。

访问 FTP 服务器时需要经过验证,只有经过了 FTP 服务器的相关验证,用户才能访问和传输文件。

首先,服务器需要安装 FTP 软件,以 vsftpd 为例:

[root@localhost wang]# which vsftpd
/sbin/vsftpd

这说明服务器已经安装了 vsftpd,再进行一系列配置即可使用。

关于 FTP 服务的搭建、配置属于 Linux 范畴,这里就不过多赘述了,请自行查看资料。

效果

实现效果如下:

这里写图片描述

如果要获取更多关于:文件剩余大小、平均速度、瞬时速度 、剩余时间等相关信息,请参考:Qt之HTTP上传/下载

FtpManager

为了便于使用,封装一个简单的 FtpManager 管理类,用于上传、下载文件。

FTPManager.h

#ifndef FTP_MANAGER
#define FTP_MANAGER

#include <QUrl>
#include <QFile>
#include <QNetworkReply>
#include <QNetworkAccessManager>

class FtpManager : public QObject
{
    Q_OBJECT

public:
    explicit FtpManager(QObject *parent = 0);
    // 设置地址和端口
    void setHostPort(const QString &host, int port = 21);
    // 设置登录 FTP 服务器的用户名和密码
    void setUserInfo(const QString &userName, const QString &password);
    // 上传文件
    void put(const QString &fileName, const QString &path);
    // 下载文件
    void get(const QString &path, const QString &fileName);

signals:
    void error(QNetworkReply::NetworkError);
    // 上传进度
    void uploadProgress(qint64 bytesSent, qint64 bytesTotal);
    // 下载进度
    void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);

private slots:
    // 下载过程中写文件
    void finished();

private:
    QUrl m_pUrl;
    QFile m_file;
    QNetworkAccessManager m_manager;
};

#endif // FTP_MANAGER

FTPManager.cpp

#include <QFileInfo>
#include "FTPManager.h"

FtpManager::FtpManager(QObject *parent)
    : QObject(parent)
{
    // 设置协议
    m_pUrl.setScheme("ftp");
}

// 设置地址和端口
void FtpManager::setHostPort(const QString &host, int port)
{
    m_pUrl.setHost(host);
    m_pUrl.setPort(port);
}

// 设置登录 FTP 服务器的用户名和密码
void FtpManager::setUserInfo(const QString &userName, const QString &password)
{
    m_pUrl.setUserName(userName);
    m_pUrl.setPassword(password);
}

// 上传文件
void FtpManager::put(const QString &fileName, const QString &path)
{
    QFile file(fileName);
    file.open(QIODevice::ReadOnly);
    QByteArray data = file.readAll();

    m_pUrl.setPath(path);
    QNetworkReply *pReply = m_manager.put(QNetworkRequest(m_pUrl), data);

    connect(pReply, SIGNAL(uploadProgress(qint64, qint64)), this, SIGNAL(uploadProgress(qint64, qint64)));
    connect(pReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SIGNAL(error(QNetworkReply::NetworkError)));
}

// 下载文件
void FtpManager::get(const QString &path, const QString &fileName)
{
    QFileInfo info;
    info.setFile(fileName);

    m_file.setFileName(fileName);
    m_file.open(QIODevice::WriteOnly | QIODevice::Append);
    m_pUrl.setPath(path);

    QNetworkReply *pReply = m_manager.get(QNetworkRequest(m_pUrl));

    connect(pReply, SIGNAL(finished()), this, SLOT(finished()));
    connect(pReply, SIGNAL(downloadProgress(qint64, qint64)), this, SIGNAL(downloadProgress(qint64, qint64)));
    connect(pReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SIGNAL(error(QNetworkReply::NetworkError)));
}

// 下载过程中写文件
void FtpManager::finished()
{
    QNetworkReply *pReply = qobject_cast<QNetworkReply *>(sender());
    switch (pReply->error()) {
    case QNetworkReply::NoError : {
        m_file.write(pReply->readAll());
        m_file.flush();
    }
        break;
    default:
        break;
    }

    m_file.close();
    pReply->deleteLater();
}

注释很详细,我就不再多做解释了。。。

注意:下载过程中文件写入是在主线程中进行的,如果文件过大,频繁写入会造成主线程卡顿现象。要避免此种情况,请在工作线程中进行。

使用

这里,只贴主要代码:

// 构建需要的控件
QPushButton *pUploadButton = new QPushButton(this);
QPushButton *pDownloadButton = new QPushButton(this);
m_pUploadBar = new QProgressBar(this);
m_pDownloadBar = new QProgressBar(this);

pUploadButton->setText(QString::fromLocal8Bit("上传"));
pDownloadButton->setText(QString::fromLocal8Bit("下载"));

// 接信号槽
connect(pUploadButton, SIGNAL(clicked(bool)), this, SLOT(upload()));
connect(pDownloadButton, SIGNAL(clicked(bool)), this, SLOT(download()));

// 设置 FTP 相关信息
m_ftp.setHostPort("192.168.***.***", 21);
m_ftp.setUserInfo("wang", "123456");

其中,m_ftp 是类变量 FtpManager。

// 上传文件
void MainWindow::upload()
{
    m_ftp.put("E:\\Qt.zip", "/home/wang/Qt.zip");
    connect(&m_ftp, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(error(QNetworkReply::NetworkError)));
    connect(&m_ftp, SIGNAL(uploadProgress(qint64, qint64)), this, SLOT(uploadProgress(qint64, qint64)));
}

// 下载文件
void MainWindow::download()
{
    m_ftp.get("/home/wang/Qt.zip", "F:\\Qt.zip");
    connect(&m_ftp, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(error(QNetworkReply::NetworkError)));
    connect(&m_ftp, SIGNAL(downloadProgress(qint64, qint64)), this, SLOT(downloadProgress(qint64, qint64)));
}

// 更新上传进度
void MainWindow::uploadProgress(qint64 bytesSent, qint64 bytesTotal)
{
    m_pUploadBar->setMaximum(bytesTotal);
    m_pUploadBar->setValue(bytesSent);
}

// 更新下载进度
void MainWindow::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
{
    m_pDownloadBar->setMaximum(bytesTotal);
    m_pDownloadBar->setValue(bytesReceived);
}

// 错误处理
void MainWindow::error(QNetworkReply::NetworkError error)
{
    switch (error) {
    case QNetworkReply::HostNotFoundError :
        qDebug() << QString::fromLocal8Bit("主机没有找到");
        break;
        // 其他错误处理
    default:
        break;
    }
}

在上传、下载过程中,确保 Server 端的路径存在:

[root@localhost wang]# pwd
/home/wang
[root@localhost wang]# ls
hello.sh
[root@localhost wang]#

上传完成后,可以去 Server 端查看一下:

[root@localhost wang]# ls -l
总用量 52980
-rw-r--r-- 1 root root       20 1116 14:01 hello.sh
-rw-r--r-- 1 wang  wang  54246299 1116 17:36 Qt.zip
[root@localhost wang]# md5sum Qt.zip 
8d010354447515d55c65d733bbba2682  Qt.zip

源文件 Qt.zip 的大小为 54,246,299 字节,显然,目标文件也一样(可使用 MD5 比对,看文件是否损坏),这说明已经完全上传成功了。

更多参考

作者:u011012932 发表于2016/11/16 18:16:39 原文链接
阅读:99 评论:0 查看评论

i2c驱动:gpio模拟i2c

$
0
0

有关linux的i2c相关文章有一下几篇,他们互相关联,应该一同看:

    - i2c 驱动一:简介

    - i2c 驱动二:devfs文件系统

    - i2c 驱动三:自己实现设备和驱动分离

    - i2c 驱动四:sysfs文件系统

    - i2c 驱动五:gpio模拟i2c


1. 简介:

gpio模拟i2c驱动可以解决i2c控制器不足的问题,但是,相对的可能要占用更多的cpu时间,此程序依然使用的是jz2440开发板


2. 内核提供的代码分析:

我们从 i2c-gpio.c 开始,文件路径:drivers/i2c/busses

(1)i2c的gpio的私有的数据 结构体如下:

struct i2c_gpio_private_data {
	struct i2c_adapter adap;
	struct i2c_algo_bit_data bit_data;
	struct i2c_gpio_platform_data pdata;
};

其中第一个成员 i2c_adapter 不在展开,他提供了i2c的通信方法

第二个成员i2c_algo_bit_data,功能和参数写在函数中,他提供了操作具体硬件上的方法

/* --- Defines for bit-adapters ---------------------------------------	*/
/*
 * 这个结构体中包含了对线的操作的函数,从名字上我们可以看出
 */
struct i2c_algo_bit_data {
	void *data;		/* private data for lowlevel routines */
	void (*setsda) (void *data, int state); /* 一些操作线的高低电平的函数 */
	void (*setscl) (void *data, int state);
	int  (*getsda) (void *data);
	int  (*getscl) (void *data);
	int  (*pre_xfer)  (struct i2c_adapter *);
	void (*post_xfer) (struct i2c_adapter *);

	/* local settings */
	int udelay;		/* half clock cycle time in us,
				   minimum 2 us for fast-mode I2C,
				   minimum 5 us for standard-mode I2C and SMBus,
				   maximum 50 us for SMBus */
	int timeout;		/* 单位 jiffies */
};

第三个成员i2c_gpio_platform_data,用于保存具体的硬件资源

/**
 * struct i2c_gpio_platform_data - Platform-dependent data for i2c-gpio
 * @sda_pin: GPIO pin ID to use for SDA
 * @scl_pin: GPIO pin ID to use for SCL
 * @udelay: signal toggle delay. SCL frequency is (500 / udelay) kHz
 * @timeout: clock stretching timeout in jiffies. If the slave keeps
 *	SCL low for longer than this, the transfer will time out.
 * @sda_is_open_drain: SDA is configured as open drain, i.e. the pin
 *	isn't actively driven high when setting the output value high.
 *	gpio_get_value() must return the actual pin state even if the
 *	pin is configured as an output.
 * @scl_is_open_drain: SCL is set up as open drain. Same requirements
 *	as for sda_is_open_drain apply.
 * @scl_is_output_only: SCL output drivers cannot be turned off.
 */
struct i2c_gpio_platform_data {
	unsigned int	sda_pin; /* sda 对应的引脚 */
	unsigned int	scl_pin; /* scl 对应的引脚 */
	int		udelay;  /* 信号触发延时,直接决定SCL引脚的频率:(500/udelay)kHz */
	int		timeout; /* 如果从设备的SCL低电平保持大于timeout jiffies,传输过程认为超时 */
	unsigned int	sda_is_open_drain:1; /* 将SDA引脚设置成开漏输出,开漏的意思是,如果设置成开漏,引脚外部没有上拉,高电平输不出来 */
	unsigned int	scl_is_open_drain:1; /* 将SCL引脚设置成开漏输出 */
	unsigned int	scl_is_output_only:1;/*  */
};

(2)i2c_gpio_setsda_dir,设置 i2c_gpio_platform_data 结构体中的 SDA 引脚的方向:1输入,0输出

/* 改变SDA引脚的方向 */
static void i2c_gpio_setsda_dir(void *data, int state)
{
	struct i2c_gpio_platform_data *pdata = data; /*  */

	if (state)
		gpio_direction_input(pdata->sda_pin);
	else
		gpio_direction_output(pdata->sda_pin, 0);
}

(3)i2c_gpio_setsda_val,设置SDA引脚上的值高/低,state可以的取值是GPIO_HIGHT / GPIO_LOW

/*
 * 改变 SDA 引脚上的电平. This is only
 * valid for pins configured as open drain (i.e. setting the value
 * high effectively turns off the output driver.)
 */
static void i2c_gpio_setsda_val(void *data, int state)
{
	struct i2c_gpio_platform_data *pdata = data;

	gpio_set_value(pdata->sda_pin, state);
}

(4)i2c_gpio_setscl_dir,设置 SDA 引脚的输入输出方向:1入,0出

/* Toggle SCL by changing the direction of the pin. */
static void i2c_gpio_setscl_dir(void *data, int state)
{
	struct i2c_gpio_platform_data *pdata = data;

	if (state)
		gpio_direction_input(pdata->scl_pin);
	else
		gpio_direction_output(pdata->scl_pin, 0);
}

(5)i2c_gpio_setscl_val,设置 SCL 引脚上的高低电平,state可以取值是GPIO_HIGHT / GPIO_LOW

/*
 * Toggle SCL by changing the output value of the pin. This is used
 * for pins that are configured as open drain and for output-only
 * pins. The latter case will break the i2c protocol, but it will
 * often work in practice.
 */
static void i2c_gpio_setscl_val(void *data, int state)
{
	struct i2c_gpio_platform_data *pdata = data;

	gpio_set_value(pdata->scl_pin, state);
}

(6)得到 SDA , SCL 引脚上的电平,返回值的取值是 GPIO_HIGHT /GPIO_LOW

static int i2c_gpio_getsda(void *data)
{
	struct i2c_gpio_platform_data *pdata = data;

	return gpio_get_value(pdata->sda_pin);
}

static int i2c_gpio_getscl(void *data)
{
	struct i2c_gpio_platform_data *pdata = data;

	return gpio_get_value(pdata->scl_pin);
}

(7)然后我们来看看模块的初始化和退出

static int __init i2c_gpio_init(void)
{
	int ret;

	ret = platform_driver_register(&i2c_gpio_driver); /* 将驱动注册到系统中 */
	if (ret)
		printk(KERN_ERR "i2c-gpio: probe failed: %d\n", ret);

	return ret;
}
subsys_initcall(i2c_gpio_init);

static void __exit i2c_gpio_exit(void)
{
	platform_driver_unregister(&i2c_gpio_driver); /*  相应的释放函数 */
}
module_exit(i2c_gpio_exit);

(8)注册的这个驱动 i2c_gpio_driver 如下

static struct platform_driver i2c_gpio_driver = {
	.driver		= {
		.name	= "i2c-gpio",
		.owner	= THIS_MODULE,
		.of_match_table	= of_match_ptr(i2c_gpio_dt_ids),
	},
	.probe		= i2c_gpio_probe,
	.remove		= __devexit_p(i2c_gpio_remove),
};

其中的 of_match_ptr 由于用的是平台文件的匹配方式,CONFIG_OF 这个宏(跟设备树有关)没有开启,因此,此处的 of_match_ptr 返回值是 i2c_gpio_dt_ids

#if defined(CONFIG_OF)
static const struct of_device_id i2c_gpio_dt_ids[] = {
	{ .compatible = "i2c-gpio", },
	{ /* sentinel */ }
};

MODULE_DEVICE_TABLE(of, i2c_gpio_dt_ids);
#endif

可以看到,即使 i2c_gpio_dt_ids 也依赖于 CONFIG_OF 这个宏,因此 .of_match_table 没有定义,根据匹配的规则,最后检查的是 .name ,这要是有 设备的名字是 i2c-gpio 就能匹配上了,匹配上,将调用 i2c_gpio_probe,此部分代码加到,部分代码将用语言代替他的实现

static int __devinit i2c_gpio_probe(struct platform_device *pdev)
{
	struct i2c_gpio_private_data *priv;
	struct i2c_gpio_platform_data *pdata;
	struct i2c_algo_bit_data *bit_data;
	struct i2c_adapter *adap;
	int ret;

	priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);

	adap = &priv->adap;
	bit_data = &priv->bit_data;
	pdata = &priv->pdata;

	if (pdev->dev.of_node) { /* 没有设备树,执行 else */
		ret = of_i2c_gpio_probe(pdev->dev.of_node, pdata);
		if (ret)
			return ret;
	} else {
		if (!pdev->dev.platform_data) return -ENXIO;
		memcpy(pdata, pdev->dev.platform_data, sizeof(*pdata));
	}

	/* 检查 SDA 和 SCL 引脚是不是有效 */

	if (pdata->sda_is_open_drain) { /* 这里实现的是当输出的时候,将开漏打开,引脚上有上拉电阻,输入的时候,关闭开漏,引脚上无上拉电阻 */
		gpio_direction_output(pdata->sda_pin, 1); /* 将 SDA 引脚设置成输出,并且输出为 1 */
		bit_data->setsda = i2c_gpio_setsda_val;
	} else {
		gpio_direction_input(pdata->sda_pin);
		bit_data->setsda = i2c_gpio_setsda_dir;
	}

	if (pdata->scl_is_open_drain || pdata->scl_is_output_only) {
		gpio_direction_output(pdata->scl_pin, 1);
		bit_data->setscl = i2c_gpio_setscl_val;
	} else {
		gpio_direction_input(pdata->scl_pin);
		bit_data->setscl = i2c_gpio_setscl_dir;
	}

	if (!pdata->scl_is_output_only) /* 绑定i2c_algo_bit_data结构体 bit_data 中的 getscl 函数 */
		bit_data->getscl = i2c_gpio_getscl; 
	bit_data->getsda = i2c_gpio_getsda; /* 绑定i2c_algo_bit_data结构体 bit_data 中的 getsda 函数 */

	if (pdata->udelay) /* 绑定udelay,用于设置scl的频率 */
		bit_data->udelay = pdata->udelay;
	else if (pdata->scl_is_output_only) /* 默认的,并且 scl 设置成单输出,频率是 10kHz */
		bit_data->udelay = 50;			/* 10 kHz */
	else
		bit_data->udelay = 5;			/* 100 kHz */

	if (pdata->timeout)
		bit_data->timeout = pdata->timeout;
	else
		bit_data->timeout = HZ / 10;		/* 默认100 ms */

	bit_data->data = pdata;

	adap->owner = THIS_MODULE;
	snprintf(adap->name, sizeof(adap->name), "i2c-gpio%d", pdev->id); /* i2c_adapter 的名字 */
	adap->algo_data = bit_data;
	adap->class = I2C_CLASS_HWMON | I2C_CLASS_SPD;
	adap->dev.parent = &pdev->dev;
	adap->dev.of_node = pdev->dev.of_node;

	/*
	 * 如果 "dev->id" 是负数,我们认为是0.
	 * The reason to do so is to avoid sysfs names that only make
	 * sense when there are multiple adapters.
	 */
	adap->nr = (pdev->id != -1) ? pdev->id : 0;
	ret = i2c_bit_add_numbered_bus(adap); /* adapter 跟 i2c总线关联,如果 nr = -1,i2c的号自动分配,得到适配器,返回0成功 */
	if (ret)
		goto err_add_bus;

	of_i2c_register_devices(adap); /* CONFIG_OF_I2C和CONFIG_OF_I2C_MODULE都没定义,不执行这句 */

	platform_set_drvdata(pdev, priv); /* ... */

	dev_info(&pdev->dev, "using pins %u (SDA) and %u (SCL%s)\n",
		 pdata->sda_pin, pdata->scl_pin,
		 pdata->scl_is_output_only
		 ? ", no clock stretching" : "");

	return 0;

err_add_bus:
	gpio_free(pdata->scl_pin);
err_request_scl:
	gpio_free(pdata->sda_pin);
err_request_sda:
	return ret;
}

相应的要有删除函数

static int __devexit i2c_gpio_remove(struct platform_device *pdev)
{
	struct i2c_gpio_private_data *priv;
	struct i2c_gpio_platform_data *pdata;
	struct i2c_adapter *adap;

	priv = platform_get_drvdata(pdev);
	adap = &priv->adap;
	pdata = &priv->pdata;

	i2c_del_adapter(adap); /* 删除适配器 */
	gpio_free(pdata->scl_pin);
	gpio_free(pdata->sda_pin); 

	return 0;
}

要用这个文件,需要做的是让内核支持,我用的内核版本是3.4.112

make menuconfig

选择 Device Drivers --->

<*> I2C support  --->

I2C Hardware Bus support  ---> 

<*> GPIO-based bitbanging I2C


重新将uImage放到开发板上,在根文件系统下会多出个节点:

./sys/bus/platform/drivers/i2c-gpio


3. 实例

对于上边的程序,在driver中已经存在,对于我们需要做的是编写 设备程序

设备可以在用户板级的配置文件中编写,也可以单独的写一个模块,然后加载,以下程序采用后者

由于手头上有个 mpu6050,所以,就以mpu6050为例,采用的传感器的小板子是 GY-521

mpu6050_gpio_dev.c

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/i2c-gpio.h>
#include <mach/regs-gpio.h>
#include <mach/hardware.h>
#include <linux/i2c.h>
#include <linux/delay.h>

MODULE_LICENSE("GPL");

static struct i2c_gpio_platform_data i2c_gpio_adapter_data = {     
    .sda_pin = S3C2410_GPB(8),     
    .scl_pin = S3C2410_GPB(7),
    .udelay = 50, //5,100kHz 50,10kHz
    .timeout = 200,     
    /* .sda_is_open_drain = 1, */
    /* .scl_is_open_drain = 1, */
    /* .scl_is_output_only = 1, */
};

static void mxs_nop_release(struct device *dev)
{
    printk("mpu6050_i2c_gpio_dev release\n");
}

static struct platform_device i2c_gpio = {     
    .name = "i2c-gpio",
    .id = 1,
    .dev = {
        .platform_data = &i2c_gpio_adapter_data,
        .release = mxs_nop_release,
    },     
};     

static int mpu6050_i2c_gpio_dev_init(void)
{
    printk("mpu6050_i2c_gpio_dev_init.\n");

    platform_device_register(&i2c_gpio);

    return 0;
}

static void mpu6050_i2c_gpio_dev_exit(void)
{
    printk("mpu6050_dev_exit.\n");

    platform_device_unregister(&i2c_gpio);
}

module_init(mpu6050_i2c_gpio_dev_init);
module_exit(mpu6050_i2c_gpio_dev_exit);

Makefile

ifeq ($(KERNELRELEASE),)

#KERNELDIR ?= /lib/modules/$(shell uname -r)/build 
KERNELDIR ?= ~/wor_lip/linux-3.4.112
PWD := $(shell pwd)

modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules

modules_install:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install

clean:
	rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions modules* Module*

.PHONY: modules modules_install clean

else
	obj-m := mpu6050_gpio_dev.o
endif

【1】将编译生成的文件,拷贝到开发板的文件系统中去,加载后出现匹配成功的字符,会在 /dev 文件夹下创建 i2c-1 ,还有一个 i2c-0 是用控制器控制的i2c接口,前边章节有讲

【2】我们使用的时候只需要像 i2c驱动二:devfs文件系统中的方法一样就行,只需要将读取的文件改成 i2c-1 即可,应用程序如下

mpu6050_devfs.c

#include <stdio.h>
#include <linux/i2c.h>
#include <linux/i2c-dev.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>

#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))

#define SMPLRT_DIV      0x19
#define CONFIG          0x1A
#define GYRO_CONFIG     0x1B
#define ACCEL_CONFIG    0x1C
#define ACCEL_XOUT_H    0x3B
#define ACCEL_XOUT_L    0x3C
#define ACCEL_YOUT_H    0x3D
#define ACCEL_YOUT_L    0x3E
#define ACCEL_ZOUT_H    0x3F
#define ACCEL_ZOUT_L    0x40
#define TEMP_OUT_H      0x41
#define TEMP_OUT_L      0x42
#define GYRO_XOUT_H     0x43
#define GYRO_XOUT_L     0x44
#define GYRO_YOUT_H     0x45
#define GYRO_YOUT_L     0x46
#define GYRO_ZOUT_H     0x47
#define GYRO_ZOUT_L     0x48
#define PWR_MGMT_1      0x6B

#define ADDR_MPU6050    0x68

static int mpu6050_read_byte(int fd, unsigned char reg)
{
    int ret = 0;
    unsigned char txbuf[1] = {reg};
    unsigned char rxbuf[1];
    struct i2c_rdwr_ioctl_data mpu_data;

    ioctl(fd, I2C_TIMEOUT, 1);
    ioctl(fd, I2C_RETRIES, 2);

    struct i2c_msg msg[] = {
        {
            .addr = ADDR_MPU6050, /* 设备的地址 */
            .flags= 0, /* 0 是写,I2C_RDWR 是读 */
            .len = ARRAY_SIZE(txbuf), /* msg 的长度 */
            .buf = txbuf
        },
        {ADDR_MPU6050, I2C_M_RD, ARRAY_SIZE(rxbuf), rxbuf},
    };
    mpu_data.msgs = msg;
    mpu_data.nmsgs = ARRAY_SIZE(msg);

    ret = ioctl(fd, I2C_RDWR, &mpu_data);
    if (ret < 0) {
        printf("ret = %d\n", ret);
        return ret;
    }

    return rxbuf[0];
}

static int mpu6050_write_byte(int fd, unsigned char reg, unsigned char val)
{
    unsigned char txbuf[2] = {reg, val};
    struct i2c_rdwr_ioctl_data mpu_data;

    ioctl(fd, I2C_TIMEOUT, 1);
    ioctl(fd, I2C_RETRIES, 2);

    struct i2c_msg msg[] = {
        {ADDR_MPU6050, 0, ARRAY_SIZE(txbuf), txbuf},
    };
    mpu_data.msgs = msg;
    mpu_data.nmsgs = ARRAY_SIZE(msg);

    ioctl(fd, I2C_RDWR, &mpu_data);

    return 0;
}

static void read_mpu6050(int fd)
{
    unsigned short accel_x = 0, accel_y = 0, accel_z = 0;
    unsigned short gyro_x = 0, gyro_y = 0, gyro_z = 0;
    unsigned short temp = 0;

    mpu6050_write_byte(fd, PWR_MGMT_1, 0x00);
    mpu6050_write_byte(fd, SMPLRT_DIV, 0x07);
    mpu6050_write_byte(fd, CONFIG, 0x06);
    mpu6050_write_byte(fd, GYRO_CONFIG, 0x18);
    mpu6050_write_byte(fd, ACCEL_CONFIG, 0x01);

    while(1) {
        accel_x = mpu6050_read_byte(fd, ACCEL_XOUT_L);
        accel_x |= mpu6050_read_byte(fd, ACCEL_XOUT_H) << 8;

        accel_y =  mpu6050_read_byte(fd, ACCEL_YOUT_L);
        accel_y |= mpu6050_read_byte(fd, ACCEL_YOUT_H) << 8;

        accel_z = mpu6050_read_byte(fd, ACCEL_ZOUT_L);
        accel_z |= mpu6050_read_byte(fd, ACCEL_ZOUT_H) << 8;

        printf("acceleration data: x = %04x, y = %04x, z = %04x\n", accel_x, accel_y, accel_z);

        gyro_x = mpu6050_read_byte(fd, GYRO_XOUT_L);
        gyro_x |= mpu6050_read_byte(fd, GYRO_XOUT_H) << 8;

        gyro_y = mpu6050_read_byte(fd, GYRO_YOUT_L);
        gyro_y |= mpu6050_read_byte(fd, GYRO_YOUT_H) << 8;

        gyro_z = mpu6050_read_byte(fd, GYRO_ZOUT_L);
        gyro_z |= mpu6050_read_byte(fd, GYRO_ZOUT_H) << 8;

        printf("gyroscope data: x = %04x, y = %04x, z = %04x\n", gyro_x, gyro_y, gyro_z);

        temp = mpu6050_read_byte(fd, TEMP_OUT_L);
        temp |= mpu6050_read_byte(fd, TEMP_OUT_H) << 8;

        printf("temperature data: %x\n", temp);

        usleep(1000*1000);
    }
}

int main(int argc, const char *argv[])
{
    int fd;

    fd = open("/dev/i2c-1", O_RDWR);
    if (fd < 0)
        perror("open error");

    read_mpu6050(fd);
    
    close(fd);
    return 0;
}

【1】编译命令别忘了:

arm-none-linux-gnueabi-gcc mpu6050_devfs.c -o mpu6050_devfs -march=armv4t

【2】以下是运行成功的打印信息


作者:qqliyunpeng 发表于2016/11/16 18:45:10 原文链接
阅读:51 评论:0 查看评论

Flutter基础—常用控件之文本

$
0
0

Text控件即容器,是一个常用的控件,下面的实例有7个不同样式的文本控件:

import 'package:flutter/material.dart';
class ContainerDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('文本控件'),
      ),
      body: new Column(
        children: <Widget>[
          new Text(
            '红色+黑色删除线+25号',
            style: new TextStyle(
              color: const Color(0xffff0000),
              decoration: TextDecoration.lineThrough,
              decorationColor: const Color(0xff000000),
              fontSize: 25.0,
            ),
          ),
          new Text(
            '橙色+下划线+24号',
            style: new TextStyle(
              color: const Color(0xffff9900),
              decoration: TextDecoration.underline,
              fontSize: 24.0,
            ),
          ),
          new Text(
            '虚线上划线+23号+倾斜',
            style: new TextStyle(
              decoration: TextDecoration.overline,
              decorationStyle: TextDecorationStyle.dashed,
              fontSize: 23.0,
              fontStyle: FontStyle.italic,
            ),
          ),
          new Text(
            'serif字体+24号',
            style: new TextStyle(
              fontFamily: 'serif',
              fontSize: 26.0,
            ),
          ),
          new Text(
            'monospace字体+24号+加粗',
            style: new TextStyle(
              fontFamily: 'monospace',
              fontSize: 24.0,
              fontWeight: FontWeight.bold,
            ),
          ),
          new Text(
            '天蓝色+25号+2行跨度',
            style: new TextStyle(
              color: const Color(0xff4a86e8),
              fontSize: 25.0,
              height: 2.0,
            ),
          ),
          new Text(
            '24号+2个字母间隔',
            style: new TextStyle(
              fontSize: 24.0,
              letterSpacing: 2.0,
            ),
          ),
        ]
      ),
    );
  }
}
void main() {
  runApp(
    new MaterialApp(
      title: 'Flutter教程',
      home: new ContainerDemo(),
    ),
  );
}

这里写图片描述

作者:hekaiyou 发表于2016/11/16 18:48:55 原文链接
阅读:60 评论:0 查看评论

Kotlin搞起来——4.类与对象

$
0
0

Kotlin搞起来——4.类与对象

标签: AndroidPocket


PS:有小伙伴说配图有点意思,嘿嘿,今天的配图是:

    本节我们继续来学习Kotlin的语法,这节学习的是非常常用的类与对象,
把这四节的内容都掌握了,基本就算kotlin入门了~不逼逼,开始本节内容~


1.类的定义与对象实例化

    Kotlin不同于Java,允许在一个文件中定义多个类
首先关注的一点是:类,成员变量,成员方法的作用域,写个简单的例子:

然后打开生成 .class文件 可以看到对应的修饰符:



会自动生成getter和setter方法:
  
同样会生成getter和setter方法!private修饰的只生成getter方法!

一清二楚,另外kotlin中还允许直接class 类名没有类实体,一般用于
表示概念(没什么卵用)!而创建一个类的对象以及方法调用同样很简单:

这里调用设置对象的属性,其实是调用对应生成的setXXX方法!对了,你还可以
自定义getter和setter方法

然后这里有个坑,就是如果你写成get() = trueHeight,会不停调用自身,然后OOM!
网上有人说在前面加个$,但实测不行,如果有解决方案的务必告知我!
另外,你还可以再get和set方法前加private,这样可以禁止外部调用!


2.构造器(构造方法)

Kotiln中的构造器有三种:

主构造器

  • 1.如果一个非抽象类没有声明任何主次构造器,那么会默认生成一个public无参的主构造器!
  • 2.一个类只有一个主构造器,类头部的一部分,在类名后,constructor(参数列表)
  • 3.主构造器中不能包含任何代码,如果想写一些初始化代码,可以写到init初始化代码块中!

使用示例

辅助构造器

  • 1.写在类中,可以有一个或者多个,用constructor关键字标识!
  • 2.可以通过this()调用其他辅助构造器,但是任何一个辅助构造器都必须调用主构造器

使用示例

私有主构造器

其实就是在主构造器前加上一个private,然后就只能通过辅助构造器来实例化对象


3.继承

Kotlin中继承父类或者实现接口,直接用 : 引号,如果有多个可以用逗号隔开,比如:

还记得一开始看class文件,类的默认修饰符是:public final,final意味着,这个类
是不能改变的,如果你想继承这个类,进行一些重写的话,你需要把class前加上 open 关键字,
子类构造方法需实现父类构造方法,而方法的修饰符也是 final 的,如果你想重写
某个方法,同样要为方法加上 open 关键字,而在重写方法处加上 override 关键字;
另外,你还可以用 super 关键字来调用父类中的方法,这点和Java是一样的。

使用示例


4.接口与抽象类

Kotlin中的接口类似于Java8,支持抽象方法抽象属性,还支持包含实现的方法
接口与其中的方法默认是 open 的和Java一样,使用 interface 声明!
接口与抽象类的区别在于:接口不能存储状态数据,可以拥有属性,但是这些属性必须
抽象的,或者提供访问器的自定义实现!

使用示例

另外,如果一个需要实现的类或接口中包含同名方法,可以使用<>来指明调用的是哪个
父类方法!比如:


5.抽象类

和接口类似,不顾默认是open的,但是对于抽象成员,需要使用 abstract关键字声明!

使用示例


7.内部类

类可以嵌套在其他类中,如果只是简单的嵌套,内部类是无法访问外部类成员的!
如果你想在内部类中访问外部成员,你需要使用 inner 关键字进行声明。

而匿名内部类就不说了,不知道的可以看回第一节。


8.单例对象与伴生对象

Kotlin中没有 静态属性和方法,如果你想实现类似于单例的功能
你可以使用关键字 object 声明一个对象,对象的构造器不能提供构造器参数
第一次使用的时候会被初始化,可用于提供常量共享不可变对象

使用示例

除此之外,Kotlin中还提供了 伴生对象 这种东西,用companion关键字声明,
可以直接类.成员访问成员,有点类似于静态成员,但是在运行时,它们依旧是实体
的实例成员,另外使用companion关键字时,伴生对象的名称可以省略

使用示例

如果你硬是要搞成Java中的静态成员和静态方法的话,你可以用:

静态成员

  • @JvmField注解:生成与该属性相同的静态字段
  • lateinit关键字:延迟初始化(没用过)
  • const()关键字:将 Kotlin 属性转换成 Java 中的静态字段(定义常量用)

静态方法

  • @JvmStatic注解:在单例对象和伴生对象中生成对应的静态方法

关于这里更多内容可见:http://kotlinlang.cn/docs/reference/object-declarations.html


9.数据类

就是只表示数据的类,用 data 进行声明,默认基于构造方法中实现了:
toString(), componentN(), copy(), equals() 和 hashCode() 方法,
另外,不在构造方法中定义的属性不会产生在toString()的结果中!
Data类可以直接使用 == 进行比较。

使用示例


10.枚举类

Kotlin中的枚举与Java中基本类似,用 enum 声明为枚举类

使用示例

  输出: 

除此之外你还可以通过valueOf(“名字”)来匹配或者values()拿到一个Array,
然后进行遍历。


11.封闭类

限制类的层次结构,值只能是集合中的类型,不饿可以是其他任何类型
可以理解成”扩展的枚举类“,枚举中的实例是唯一的,而封闭类可以
有很多实例,他们可以有不同的状态,使用关键字 sealed 声明
(内部的类都是本身的子类),直接子类必须都写在封闭类中!!!
有一点要注意:封闭类的子类的扩展可以在任何地方,不需要写在封闭类内部!
使用密封类的最主要的的好处体现在你使用 when 表达式。
可以确保声明可以覆盖到所有的情形,不需要再使用 else 情形。
没用过,直接祭出官方示例:


本章小结

    本节对Kotlin中与类和对象有关的知识点进行了全面的讲解:

  • 1.类的定义与对象实例化
  • 2.构造器(构造方法)
  • 3.继承
  • 4.接口与抽象类
  • 5.抽象类
  • 7.内部类
  • 8.单例对象与伴生对象
  • 9.数据类
  • 10.枚举类
  • 11.封闭类

相信对你对Kotlin的认识又更进一步了,大块的东西已经在这四节里面说完,
然后接下来的一节是一些小知识点补充,在下一节是结合Anko重构之前写个
的一个很小的项目,再接着是番外篇——实际开发中用Kotlin中的一些技巧
和实例~敬请期待!

如果觉得这个系列对你学习Kotlin有所帮助,点个喜欢呗~

本节参考文献如下


作者:zpj779878443 发表于2016/11/16 19:28:05 原文链接
阅读:49 评论:0 查看评论

8、Plugin.xml(基础知识)

$
0
0

翻译自cordova官方文档(如果需要链接,请自行对照原文链接进行查看):
https://cordova.apache.org/docs/en/latest/plugin_ref/spec.html

Plugin.xml

Plugin.xml是用于设置你得插件,他包含很多元素为你的插件提供详细的设置。

<pluginxmlns="http://apache.org/cordova/ns/plugins/1.0"id="cordova-plugin-splashscreen"version="3.2.0"><!--
        plugin 标签是Plugin配置文件最顶层的元素
            xmlns:用于指定命名空间(namespace),当然也可以指定针对特定平台的命名空间比如
                xmlns:android="http://schemas.android.com/apk/res/android"
               id:用于设置plugin的识别id
          version:plugin的版本号
    --><name>Splashscreen</name><!--
            用于为plugin设定一个名字。
        --><description>Cordova Splashscreen Plugin</description><!--
            plugin的描述或者说明
        --><license>Apache 2.0</license><!--
            为plugin指定使用哪种开源许可license
        --><keywords>cordova,splashscreen</keywords><!--
            使用逗号分割,用于描述plugin的关键字。
        --><author>Foo plugin author</author><!--
            用于设置作者信息。
            # 这个是为了解释这个标签特地加入的,并不是splashscreen插件带的。
        --><assetsrc="www/new-foo.js"target="js/experimental/foo.js" /><!--
            将特定js文件拷贝到工程指定目录。并且可以重命名。
            # 这个是为了解释这个标签特地加入的,并不是splashscreen插件带的。
        --><repo>https://git-wip-us.apache.org/repos/asf/cordova-plugin-splashscreen.git</repo><!--
            plugin的描述或者说明
        --><issue>https://issues.apache.org/jira/browse/CB/component/12320653</issue><dependencyid="cordova-plugin-someplugin"url="https://github.com/myuser/someplugin"commit="428931ada3891801"subdir="some/path/here" /><dependencyid="cordova-plugin-someplugin"version="1.0.1"><!--
            用于声明该plugin会依赖其他plugin,通过id或者是url来指定依赖plugin的源。
        --><engines><enginename="cordova-android"version=">=3.6.0" /><!-- Requires CordovaPlugin.preferences --></engines><!--
            engines用于设置plugin适用的平台镜像版本
            engine用于设置具体支持的版本,name用于指定镜像平台,version用于设置版本号
            ,如果不设置默认支持全平台全版本
            目前cordova支持的平台主要如下:
            .cordova
            .cordova-plugman
            .cordova-android
            .cordova-ios
            .cordova-blackberry10
            .cordova-wp8
            .cordova-windows
            .cordova-osx
            .windows-os
            .android-sdk (returns the highest Android api level installed)
            .windows-sdk (returns the native windows SDK version)
            .apple-xcode (returns the xcode version)
            .apple-ios (returns the highest iOS version installed)
            .apple-osx (returns the OSX version)
            .blackberry-ndk (returns the native blackberry SDK version)
        --><js-modulesrc="www/splashscreen.js"name="SplashScreen"><clobberstarget="navigator.splashscreen" /></js-module><!--
            js-module标签一般用于发布定义js接口的js文件。本例中,将www/splashscreen.js文件中定义的接口发布到
            window.navigator.splashscreen对象上,这样web端可以直接通过navigator.splashscreen对象来访问plugin
            的api。如果想替换掉window对象上指定接口还可以通过以下方式:
            <js-module src="socket.js" name="Socket">
                <merges target="chrome.socket" />
                # 使用merges标签,如果chrome.socket存在,那么用此plugin内的chrome.socket替换掉原有的。
            </js-module>
            <js-module src="socket.js" name="Socket">
                <runs/>
                # 使用runs标签你代码需要通过cordova.require去指定,并且不是安装到window对象上。
                这个标签用于安装模块或者是其他的时候使用。该标签和<clobbers/>或者<merges/>一起使用是多余的。
            </js-module>
        --><!--
        platform用于指定plugin安装到哪些平台,这里主要介绍android和ios,如果没有指定平台,那么cordova会把plugin看
        做只提供javascript接口的plugin。
                 name:用于指定平台名字。
          source-file:用于告诉plugin把config-file包含的内容安装到target里面。或者是target-dir里面。
          header-file:同样也是用来将指定文件拷贝到指定目录下。
        resource-file:同样也是用于拷贝文件,不同的是这个配置用于拷贝非代码资源。
                  <resource-file src="FooPluginStrings.xml" target="res/values/FooPluginStrings.xml" />
          config-file:指定需要设置的配置文件爱你,比如xml或者plist,添加进去的内容必须以xml的格式设置。
                  具体参见下面例子代码。其中parent用于指定在哪一个父元素下添加。
        plugins-plist:已经过时了。现在使用<config-file>来替代。
             lib-file: 用于添加依赖包。
                     <lib-file src="src/BlackBerry10/native/device/libfoo.so" arch="device" />
                     src用于指定plugin下的文件路径,arch用于设置依赖包需要的架构比如x86,x64,
                     device-target用于设置目标设备win,phone,all.
           framework:用于引入一个依赖framework。
                  <framework src="libsqlite3.dylib" />
                  <framework src="com.google.android.gms:play-services-gcm:+" />
                  <framework src="relative/path/rules.gradle" custom="true" type="gradleReference" />
                  <framework src="path/to/project/LibProj.csproj" custom="true" type="projectReference"/>
                  <framework src="src/windows/example.dll" arch="x64" />
                info:提供一些额外信息。说明信息之类的。
                hook:用于设置在cordova某些命令执行后需要执行的脚本比如
                         <hook type="after_plugin_install" src="scripts/afterPluginInstall.js" />   
                         具体参照[Hooks](https://cordova.apache.org/docs/en/latest/guide/appdev/hooks/index.html)
             uses-permission:用来声明plugin需要申请的权限,比如:
                     <uses-permission android:name="my-app-id.permission.C2D_MESSAGE"/>
              preference:用于在安装plugin的时候check用户设置的变量,如果变量的内容不正确,显示警告信息
                 <preference name="API_KEY" default="default-value" />
                 比如在statusbar插件中的设置
                 <config-file target="config.xml" parent="/*">
                    <feature name="StatusBar">
                        <param name="ios-package" value="CDVStatusBar" />
                        <param name="onload" value="true" />
                    </feature>
                    <preference name="StatusBarOverlaysWebView" value="true" />
                    <preference name="StatusBarStyle" value="lightcontent" />
                 </config-file>
        --><!-- android --><platformname="android"><!-- 修改AndroidManifest.xml文件 
                <config-file target="AndroidManifest.xml" parent="/manifest">
                     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
                </config-file>--><config-filetarget="res/xml/config.xml"parent="/*"><!--在androidn的config.xml中添加feature内的配置信息--><featurename="SplashScreen"><paramname="android-package"value="org.apache.cordova.splashscreen.SplashScreen"/><paramname="onload"value="true"/></feature></config-file><!--将指定文件安装到target-dir的位置--><source-filesrc="src/android/SplashScreen.java"target-dir="src/org/apache/cordova/splashscreen" /><!--将依赖的jar添加到系统--><!--<source-file src="plugin_path/**.jar" target-dir="libs" />--></platform><!-- ios --><platformname="ios"><config-filetarget="config.xml"parent="/*"><!--在config.xml中添加feature内的配置信息--><featurename="SplashScreen"><paramname="ios-package"value="CDVSplashScreen"/><paramname="onload"value="true"/></feature></config-file><!--为了举例添加的例子代码--><config-filetarget="helloworld-Info.plist"parent="CFBundleURLTypes"><array><dict><key>PackageName</key><string>$PACKAGE_NAME</string></dict></array></config-file><header-filesrc="src/ios/CDVSplashScreen.h" /><source-filesrc="src/ios/CDVSplashScreen.m" /><header-filesrc="src/ios/CDVViewController+SplashScreen.h" /><source-filesrc="src/ios/CDVViewController+SplashScreen.m" /><frameworksrc="CoreGraphics.framework" /></platform></plugin>
作者:zzh_receive 发表于2016/11/16 21:03:30 原文链接
阅读:8 评论:0 查看评论

9、插件开发指南(基础知识)

$
0
0

翻译自cordova官方文档(如果需要链接,请自行对照原文链接进行查看):
https://cordova.apache.org/docs/en/latest/guide/hybrid/plugins/index.html

插件开发指南

插件是一种可以帮助app在其运行的原生平台上通过cordova webview与注入的代码进行交流的包。插件打破了本身web架构的app和设备以及平台元素交互的壁垒。所有的主要的cordova的接口都是以插件形式实现的,另外还有好多像条码扫描,NFC或者设计日历界面等其他的插件。目前我们已经有了一系列的可获取的插件列表。

插件提供了一个JavaScript接口可以支持与多个平台相应的代码进行交互的方式。本质上,这通过一个普通的JavaScript接口隐藏了背后的各种原生代码实现。

本节介绍一个简单地echo插件,通过JavaScript传递了一个字符串到平台的后端,然后我们就可以通过这个字符串实现复杂的功能。本节讨论了基本插件的结构和对外的JavaScript接口。对于每个该插件对应的原生接口,请参阅本节末的列表。

另外,如果想写插件,最好的方式就是研究已有插件的实现。

构建插件

应用开发者可以通过命令行:plugin add命令(请参阅命令行文档)来为工程添加一个插件。该命令的参数是包含插件代码一个Git仓库的URL。以下是一个扩展了cordova设备信息的API的实例:

$ cordova plugin add https://git-wip-us.apache.org/repos/asf/cordova-plugin-device.git

本插件库必须配置一个顶级的plugin.xml配置文件。在插件配置说明中详细介绍了多种方式来对该文件进行配置。以下Device插件的缩略版提供了关于配置文件的一个简单的例子:

<?xml version="1.0" encoding="UTF-8"?><pluginxmlns="http://apache.org/cordova/ns/plugins/1.0"id="cordova-plugin-device"version="0.2.3"><name>Device</name><description>Cordova Device Plugin</description><license>Apache 2.0</license><keywords>cordova,device</keywords><js-modulesrc="www/device.js"name="device"><clobberstarget="device" /></js-module><platformname="ios"><config-filetarget="config.xml"parent="/*"><featurename="Device"><paramname="ios-package"value="CDVDevice"/></feature></config-file><header-filesrc="src/ios/CDVDevice.h" /><source-filesrc="src/ios/CDVDevice.m" /></platform></plugin>

顶层插件标签元素id使用了一种反向域的方式来确认是用户添加的插件。js-module标签标示JavaScript接口的路径。platform标签标示对应的原生代码,本例中是IOS平台。config-file标签标示能够使平台能够感知额外的代码库的config.xml文件。head-file和source-file标签标示了库的组件文件的位置。

验证插件

你可以用plugman工具来检测插件是否已经完成针对每个平台的安装。安装plugman使用下面的node命令:

$ npm install -g plugman

你需要一个有效的app的源目录,比如在命令行文档中介绍的默认使用CLI生成的顶级www目录。确保应用程序的index.html主页引用插件的JavaScript接口的名称在相同的源路径中:
然后运行下面的命令来测试iOS的依赖关系是否正确加载:(示例)

$plugmaninstall--platformios--project/path/to/my/project/www--plugin/path/to/my/plugin

有关plugman的详细说明,请参见使用Plugman管理插件。有关如何实际调试插件的信息,请参阅本页面底部列出的每个平台的原生接口介绍。

JavaScript接口

JavaScript提供了一个前置接口,使得他成为插件的最重要的部分。你可以以你喜欢的方式构建你的JavaScript插件,但是你必须调用cordova.exec来进行和原生的交互,使用下面的语法:

cordova.exec(function(winParam) {},
             function(error) {},
             "service",
             "action",
             ["firstArgument", "secondArgument", 42, false]);

下面是每个参数如何工作的说明:
function(winParam) {}:一个成功回调函数。假设你的调用成功完成,该功能与您传递给它的任何参数一起执行。
function(error) {}: 错误回调方法。如果操作没有成功执行,这个方法会带着错误参数一起执行。
“service”: 原生调用的服务名。会关联到原生类,更多信息在下面的原生指导列表中。
“action”: 这个是调用原生的行为明。会关联到原生的方法。请参阅下面的原生指导列表。

JavaScript例子

这个例子展示了一种扩展Javascript接口的方式:

window.echo = function(str, callback) {
    cordova.exec(callback, function(err) {
        callback('Nothing to echo.');
    }, "Echo", "echo", [str]);
};

在这个例子中,插件关联了一个窗口对象作为新的echo方法。这样的话插件用户的调用方式为:

window.echo("echome", function(echoValue) {
    alert(echoValue == "echome"); // should alert true.
});

然后看cordova.exec方法的最后三个参数。首先调用Echo服务,一个类名。第二个是请求echo方法,即类中的方法。第三个是包含echo字符串的一个数组,它是window.echo函数的所传递的第一个参数。

传递到EXEC的成功回调仅仅是使用window.echo成功回调函数一个参考(如上面例子中就是执行alert(echoValue == “echome”);来判断是否成功回调)。如果本机平台触发错误回调,这样也只是调用成功回调并传递一个默认的字符串’Nothing to echo.’。

原生接口

一旦你定义好插件的JavaScript方法,你需要至少一个原生接口来完成整个流程。每个平台的详细内容,每个建立在简单的Echo插件上面的例子如下:
Amazon Fire OS Plugins
Android Plugins
iOS Plugins
BlackBerry 10 Plugins
Windows Phone 8 Plugins
Windows Plugins

发布插件

一旦你开发了一个插件,并且你想发布分享到社区。你可以发布你的插件到任意的基于npmjs-的注册表。但是推荐的方式是NPM注册表。请阅读我们的发布到NPM的说明:
注意:Cordova插件注册表正在变动为只读状态。publish/unpublish的命令已经从plugman中移除,所以你需要使用相关的npm命令。
其他开发者可以使用plugman或者Cordova CLI来自动安装你的插件。(每一个开发过程的详细信息,请参阅 Using Plugman to Manage Plugins和The Command-Line Interface.)
发布插件到NPM注册表,你需要使用以下步骤:
安装plugman CLI工具

$ npm install -g plugman

为你的插件创建package.json文件

$ plugman createpackagejson /path/to/your/plugin

发布:

$ npm adduser # 这是个你没有账号的示例写法$ npm publish /path/to/your/plugin

更多的内容请参考NPM文档站点的关于如何发布一个npm包

与搜索插件集成

为了能在插件搜索中找到你的插件,请再发布插件之前在package.json文件中加入ecosystem:cordova关键词
为了表明插件支持特定的平台,需要在package.json文件中的关键字列表中加入特定的平台中加入关键字如:cordova-.Plugman的createpackagejson命令已经完成上面的功能,但是如果你没有使用plugman,那么你就需要按照下面示例中显示的方式进行修改.
例如:一个插件支持android,IOS以及windows.那么package.json中的关键字书写如下:

"keywords": [
    "ecosystem:cordova",
    "cordova-android",
    "cordova-ios",
    "cordova-windows"
]

更多关于package.json文件的内容,可以参考 package.json file of cordova-plugin-device.

指定Cordova的依赖

Cordova 6.1.0提供了插件通过package.json添加特定Cordova-related依赖的支持。插件将提供多个版本的依赖列表,从而为了能够在通过Cordova CLI从npm选择插件版本时提供引导。CLI将选择支持本地平台和插件的最新版本作为本地版本。如果未获取到合适的已发布版本,CLI将提示用户请求失败然后重新请求获取新版本。
这种方式将代替原来的再plugin.xml中的engineselement中声明依赖的方式,并且这种添加依赖的方式将有助于用户从npm下载插件之后不会出现崩溃或者出现编译错误。如果当前最新版本的插件不适合用户的当前项目,CLI将给开发者插件未满足他的项目需求的列表,从而让开发者明白为什么不支持插件,然后升级项目来支持插件。这将允许你的插件在发生重大变更时不必担心开发者还在使用老版本平台和插件。
为你的插件指定依赖,再package.json文件中替换engines元素中的cordovaDependencies中的内容,用下面的结构:

engines: {
    cordovaDependencies: {
        PLUGIN_VERSION: {
            DEPENDENCY: SEMVER_RANGE,
            DEPENDENCY: SEMVER_RANGE,
            ...
        },
        ...
    }
}

PLUGIN_VERSION 指定一个插件版本. 这应该提供一个经过npm的语义版本包定义的单一的版本或者一个版本上线(示例如下)
DEPENDENCY 将可以是下面中的一个:
The Cordova CLI, “cordova”(cordova版本)
平台 (如: “cordova-android”, “cordova-ios”, “cordova-windows”等)
另一个插件(如: “cordova-plugin-camera”等)
SEMVER_RANGE 应该是一个经过npm的语义版本包定义的范围
注意:一个平台的依赖是cordova对应的平台,不是系统(就如:是cordova-android而不是Android OS)
你的cordovaDependencies可以列出PLUGIN_VERSION的任意数目的列表和任意数目的DEPENDENCY。你的插件版本如果没有他们自己的依赖则会被认为是低于当前版本的最高版本的依赖。示例如下:考虑下面的情况:

engines: {
    cordovaDependencies: {
        "1.0.0": { "cordova-android": "<3.0.0"},
        "2.1.0": { "cordova-android": ">4.0.0"}
    }
}

插件版本如果低于当前最低版本(本例中是1.0.0),那么将被考虑没有任何依赖。任何在1.0.0至2.1.0之间的版本将会拥有1.0.0(cordova-android版本低于3.0.0)的依赖。这可以让你在有重大更新时只需要升级你的cordovaDependencies信息。

上限

另外,对于一个单独的版本,在cordovaDependencies元素中的PLUGIN_VERSION还可以指定你的插件老版本的上限。这将在一个重大更新出现所有老版本插件不支持的新的约束和依赖时很有用处。这将写成:一个<跟在一个明确的版本后面(不是一个随意的范围!)。这样使得所有低于当前版本的插件都是用当前的依赖。例如:考虑下面的情况:

engines: {
    cordovaDependencies: {
        "0.0.1":  { "cordova-ios": ">1.0.0" },
        "<1.0.0": { "cordova-ios": "<2.0.0" },
        "<2.0.0": { "cordova-ios": "<5.0.0" }
    }
}

这里我们制定一个插件版本(0.0.1)和两个更高的版本(<1.0.0和>2.0.0)都包含cordova-ios.这两个范围没有覆盖0.0.1版本,他们在升级时合并添加的。当CLI检查项目的cordova-ios版本,将会对于0.0.1版本进行重新约束,他们三个组合如下:

cordova-ios>1.0.0AND cordova-ios<2.0.0AND cordova-ios<5.0.0

请注意唯一允许的插件版本是单独的版本或者更高的版本。其他的版本并没有这些版本范围支持。

作者:zzh_receive 发表于2016/11/16 21:13:33 原文链接
阅读:8 评论:0 查看评论

Android的MVC

$
0
0

前言

大家都知道Android的设计架构是基于MVC的。对于MVC大家并没有”陌生”,而且一般来说,这个是一个广泛使用的框架。用游戏来讲述MVC是最好的,因为对于Control层的理解比较直观:control就是游戏的控制,上下左右,技能ABC,游戏的时间,事件;View是对绘制UI,场景的Face,人物Body(不知道Model是什么);Model是实体,具有动作。

MVC模型

Created with Raphaël 2.1.0ControlControlModelModelViewViewUpdate ModelNotify Controlupdate ViewUser action

大致的流向,用户的输入改变Model,并且改变View,View显示动作的Action给Control,Model的改变Notify控制。

重要的是:

  • 模型和视图要严格的分离(不能有交互)

贪吃蛇游戏的简易mvc代码

在结构上安装model,view,control来分类,代码整体的包结构如下:
这里写图片描述

编写View

注意的是view是独立的模块,不能关联control和mode,l那么就有了以下代码。说明,view是游戏的UI,它只关心绘制的点,和场景,不关心蛇,游戏。所以,我们可以独立这个view来测试,譬如测试绘制蛇,绘制食物;

package com.owant.mvcsnakegame.view;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;

import com.owant.mvcsnakegame.model.Location;

import java.util.ArrayList;

/**
 * Created by owant on 16/11/2016.
 */
public class SnakeGameView extends View {

    public static final int screenX = 30;
    public static final int screenY = 30;

    private int dx;
    private int dy;
    public ArrayList<Location> body;
    public Location food;

    private Paint mPaint;

    public SnakeGameView(Context context) {
        this(context, null, 0);
    }

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

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

        mPaint = new Paint();
        mPaint.setColor(Color.RED);
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setStrokeWidth(2);

//        testDrawSnakeBody();

//        testFood();

    }

    private void testFood() {
        food = new Location();
        food.rawY = 3;
        food.rawX = 5;
    }

    private void testDrawSnakeBody() {
        body = new ArrayList<>();
        Location location = new Location();
        location.rawX = 0;
        location.rawY = 0;
        body.add(location);

        Location location1 = new Location();
        location1.rawX = 0;
        location1.rawY = 1;
        body.add(location1);

        Location location2 = new Location();
        location2.rawX = 0;
        location2.rawY = 2;
        body.add(location2);

        Location location3 = new Location();
        location3.rawX = 0;
        location3.rawY = 3;
        body.add(location3);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        drawScreen(canvas);


        drawSnake(canvas);

        drawFood(canvas);

    }

    /**
     * draw the game screen
     *
     * @param canvas
     */
    private void drawScreen(Canvas canvas) {
        mPaint.setColor(Color.RED);

        //x
        for (int i = 1; i < screenX; i++) {
            canvas.drawLine(dx * i, 0, dx * i, getHeight(), mPaint);
        }

        //y
        for (int i = 1; i < screenY; i++) {
            canvas.drawLine(0, dy * i, getWidth(), dy * i, mPaint);
        }
    }

    /**
     * draw the snake body
     *
     * @param canvas
     */
    private void drawSnake(Canvas canvas) {
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(Color.RED);
        if (body != null) {
            for (Location b : body) {
                canvas.drawRect(b.rawX * dx, b.rawY * dy, b.rawX * dx + dx, b.rawY * dy + dy, mPaint);
            }
        }
    }

    /**
     * draw the food
     *
     * @param canvas
     */
    private void drawFood(Canvas canvas) {
        if (food != null) {
            mPaint.setColor(Color.GREEN);
            canvas.drawRect(food.rawX*dx, food.rawY*dx, food.rawX*dx + dx, food.rawY*dx + dy, mPaint);
        }
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        dx = getWidth() / screenX;
        dy = getHeight() / screenY;
    }

    public ArrayList<Location> getBody() {
        return body;
    }

    public void setBody(ArrayList<Location> bodys) {
        this.body = bodys;
    }

    @Override
    public void invalidate() {
        super.invalidate();
    }

    public Location getFood() {
        return food;
    }

    public void setFood(Location food) {
        this.food = food;
    }

    public void clearFood() {
        this.food = null;
    }

}

Model的编写

Snake。说明,蛇对自己的特性负责,不理会control和view;

package com.owant.mvcsnakegame.model;

import java.util.ArrayList;

/**
 * Created by owant on 16/11/2016.
 */
public class Snake {

    public boolean live = true;

    public ArrayList<Location> body;
    public int direction = Direction.down;
    //30ms
    public int speek = 300;

    public Snake() {
        body = new ArrayList<>();
        init();
    }

    private void init() {
        Location location = new Location();
        location.rawX = 0;
        location.rawY = 0;
        body.add(location);

        Location location1 = new Location();
        location1.rawX = 0;
        location1.rawY = 1;
        body.add(location1);

        Location location2 = new Location();
        location2.rawX = 0;
        location2.rawY = 2;
        body.add(location2);

        Location location3 = new Location();
        location3.rawX = 0;
        location3.rawY = 3;
    }

    public int getDirection() {
        return direction;
    }

    public void setDirection(int direction) {
        this.direction = direction;
    }

    public void growUp() {
    }

    public Location getHeard() {
        return body.get(body.size() - 1);
    }

    public void move() {

        Location heard = getHeard();
        Location next = new Location();

        if (direction == Direction.up) {//向上
            next.rawX = heard.rawX;
            next.rawY = heard.rawY - 1;
        } else if (direction == Direction.down) {//向下
            next.rawX = heard.rawX;
            next.rawY = heard.rawY + 1;
        } else if (direction == Direction.left) {//左边
            next.rawX = heard.rawX - 1;
            next.rawY = heard.rawY;
        } else if (direction == Direction.right) {//右
            next.rawX = heard.rawX + 1;
            next.rawY = heard.rawY;
        }

        body.remove(0);
        body.add(next);
    }


}

Location 坐标点,代码省略。

Control

控制层,需要管理View和Model,它应该包括游戏的时间控制,游戏的输入。代码如下:

package com.owant.mvcsnakegame.control;

import com.owant.mvcsnakegame.model.Direction;
import com.owant.mvcsnakegame.model.Snake;
import com.owant.mvcsnakegame.view.SnakeGameView;

/**
 * Created by owant on 16/11/2016.
 */
public class GameControl implements Runnable {

    //control view
    SnakeGameView view;
    Snake snake;

    public GameControl(SnakeGameView view, Snake snake) {
        this.view = view;
        this.snake = snake;
    }

    public void right() {
        if (snake.getDirection() == Direction.down) {
            snake.setDirection(Direction.left);
        } else if (snake.getDirection() == Direction.up) {
            snake.setDirection(Direction.right);
        } else if (snake.getDirection() == Direction.left) {
            snake.setDirection(Direction.up);
        } else if (snake.getDirection() == Direction.right) {
            snake.setDirection(Direction.down);
        }
    }

    public void left() {
        if (snake.getDirection() == Direction.left) {
            snake.setDirection(Direction.down);
        } else if (snake.getDirection() == Direction.right) {
            snake.setDirection(Direction.up);
        } else if (snake.getDirection() == Direction.up) {
            snake.setDirection(Direction.left);
        } else if (snake.getDirection() == Direction.down) {
            snake.setDirection(Direction.right);
        }
    }

    @Override
    public void run() {
        do {
            try {
                Thread.sleep(snake.speek);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            snake.move();

            view.post(new Runnable() {
                @Override
                public void run() {
                    view.invalidate();
                }
            });
        } while (snake.live);
    }
}

Activity

至于为何有Activity内,因为Android的MVC设计,Control落在了Activity上,也可以独立为一层特殊的Control,因为GameControl已经实现了所有的控制功能。Activity只是程序的入口,初始化实体,视图,接受输入传递给GameControl就行。

package com.owant.mvcsnakegame;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.KeyEvent;

import com.owant.mvcsnakegame.control.GameControl;
import com.owant.mvcsnakegame.model.Snake;
import com.owant.mvcsnakegame.view.SnakeGameView;

public class MainActivity extends AppCompatActivity {


    GameControl control;
    Snake snake;
    SnakeGameView snakeGameView;

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

        snake = new Snake();

        snakeGameView = (SnakeGameView) findViewById(R.id.main_snake_view);
        snakeGameView.setBody(snake.body);

        control = new GameControl(snakeGameView, snake);
        new Thread(control).start();

    }


    @Override
    protected void onResume() {
        super.onResume();
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        System.out.println(keyCode);
        if (keyCode == KeyEvent.KEYCODE_A) {
            control.left();
        } else if (keyCode == KeyEvent.KEYCODE_D) {
            control.right();
        }
        return super.onKeyDown(keyCode, event);
    }

}

大致对于MVC的框架有了一定了解了吧。

总结

mvc是一种框架,不是设计模式。框架是很灵活的,可以变动,主要是对model,view,control的分类,而且如何关联到Android的代码中去。注意的是:

  • model和view是独立分开的。不要在view里面进行model和control的操作。

  • 大多数的APP的control大多是loadData

一些错误和有异议的问题

  • 我在网上看了一些人的mvc,发现他们的view层居然出现model和control,这是很错误的。譬如:
    这里写图片描述

    • 在ListView的Adapter中写点击事件。
    • View出现Control和刷新,这个不能说错误,由于View自己刷新是Android自身提供的,而且代码写起来简易,可以考虑View自己刷新。
作者:u012131702 发表于2016/11/16 21:14:16 原文链接
阅读:14 评论:0 查看评论

MediaScannerService研究

$
0
0


MediaScannerService研究

侯 亮

(本文以Android 5.1为准)


1 概述

MediaScannerService是Android平台提供的一个用于扫描手机中多媒体文件的应用级service。它并不是系统服务。MediaScannerService和MediaProvider有着非常紧密的关系,因为扫描出的结果总需要存储到某个地方来展现给用户。那么它们具体是如何结合的呢?本文将逐步加以阐述。


我们先来初步了解一下MediaScannerService,它在AndroidManifest.xml文件里的相关信息如下:

【packages/providers/mediaprovider/AndroidManifest.xml】

<service android:name="MediaScannerService" android:exported="true">
    <intent-filter>
        <action android:name="android.media.IMediaScannerService" />
    </intent-filter>
</service>

MediaScannerService本身继承于Service,而且还实现了Runnable接口。其定义截选如下:【packages/providers/mediaprovider/src/com/android/providers/media/MediaScannerService.java】

public class MediaScannerService extends Service implements Runnable
{
    private static final String TAG = "MediaScannerService";


    private volatile Looper 			mServiceLooper;
    private volatile ServiceHandler 	        mServiceHandler;
    private PowerManager.WakeLock 		mWakeLock;
    private String[]                            mExternalStoragePaths;
    . . . . . .
    private final IMediaScannerService.Stub mBinder = new IMediaScannerService.Stub() 
            . . . . . .
    . . . . . .
}

1.1 在onCreate()中启动工作线程

MediaScannerService的onCreate()函数如下:
【packages/providers/mediaprovider/src/com/android/providers/media/MediaScannerService.java】

@Override
public void onCreate()
{
    PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
    mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
    StorageManager storageManager = (StorageManager)getSystemService(Context.STORAGE_SERVICE);
    mExternalStoragePaths = storageManager.getVolumePaths();

    // 启动最重要的工作线程,该线程也是个消息泵线程
    Thread thr = new Thread(null, this, "MediaScannerService");
    thr.start();
}
可以看到,onCreate()里会启动最重要的工作线程,该线程也是个消息泵线程。每当用户需要扫描媒体文件时,基本上都是在向这个消息泵里发送Message,并在处理Message时完成真正的scan动作。请注意,创建Thread时传入的第二个参数就是MediaScannerService自身,也就是说线程的主要行为其实就是MediaScannerService的run()函数,该函数的代码如下:

public void run()
{
    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND +
                              Process.THREAD_PRIORITY_LESS_FAVORABLE);
    Looper.prepare();
    mServiceLooper   = Looper.myLooper();               // 消息looper
    mServiceHandler  = new ServiceHandler();            // 发送消息的handler
    Looper.loop();
}
后续就是通过上面那个mServiceHandler向消息队列发送Message的。

1.2 向工作线程发送Message

比较常见的向消息泵发送Message的做法是调用startService(),并在MediaScannerService的onStartCommand()函数里sendMessage()。比如,和MediaScannerService配套提供的MediaScannerReceiver,当它收到类似ACTION_BOOT_COMPLETED这样的系统广播时,就会调用自己的scan()或scanFile()函数。而scan()函数的代码如下:
【packages/providers/mediaprovider/src/com/android/providers/media/MediaScannerReceiver.java】

private void scan(Context context, String volume) {
    Bundle args = new Bundle();
    args.putString("volume", volume);
    context.startService( new Intent(context, MediaScannerService.class).putExtras(args));
}
startService()动作会导致走到service的onStartCommand(),并进一步发送消息,其函数截选如下:

@Override
public int onStartCommand(Intent intent, int flags, int startId)
{
    . . . . . .
    . . . . . .
    Message msg = mServiceHandler.obtainMessage();
    msg.arg1 = startId;
    msg.obj = intent.getExtras();
    mServiceHandler.sendMessage(msg);	// 发送消息!

    // Try again later if we are killed before we can finish scanning.
    return Service.START_REDELIVER_INTENT;
}

另外一种比较常见的发送Message的做法是先直接或间接bindService(),绑定成功后会得到一个IMediaScannerService接口,而后外界再通过该接口向MediaScannerService发起命令,请求其扫描特定文件或目录。


IMediaScannerService接口只提供了两个接口函数:

  • void requestScanFile(String path, String mimeType, in IMediaScannerListener listener);
  • void scanFile(String path, String mimeType);
处理这两种请求的实体是服务内部的mBinder对象,参考代码如下:
【packages/providers/mediaprovider/src/com/android/providers/media/MediaScannerService.java】

private final IMediaScannerService.Stub mBinder = new IMediaScannerService.Stub() {
    public void requestScanFile(String path, String mimeType, IMediaScannerListener listener)
    {
        Bundle args = new Bundle();
        args.putString("filepath", path);
        args.putString("mimetype", mimeType);
        if (listener != null) {
            args.putIBinder("listener", listener.asBinder());
        }
        startService(new Intent(MediaScannerService.this,
                                MediaScannerService.class).putExtras(args));
    }

    public void scanFile(String path, String mimeType) {
        requestScanFile(path, mimeType, null);
    }
};
说到底还是在调用startService()。

具体处理消息泵线程里的消息时,执行的是ServiceHandler的handleMessage()函数:

private final class ServiceHandler extends Handler
{
    @Override
    public void handleMessage(Message msg)
    {
        Bundle arguments = (Bundle) msg.obj;
        String filePath = arguments.getString("filepath");
        . . . . . .
        if (filePath != null) {
            . . . . . .
                uri = scanFile(filePath, arguments.getString("mimetype"));
            . . . . . .
        } else {
            . . . . . .
                scan(directories, volume);
            . . . . . .
        }
        . . . . . .
        stopSelf(msg.arg1);
    }
};
此时调用的scanFile()或scan()函数才是实际进行扫描动作的地方。扫描动作中主要借助的是辅助类MediaScanner,这个类非常重要,它是打通Java层和C++层的关键,扫描动作最终会调用到MediaScanner的某个native函数,于是程序流程开始走到C++层。

现在,我们可以画一张示意图:


2 运作细节

2.1 发起扫描动作

现在我们已经了解了,要发起扫描动作,大体上只有两种方式:
1)用广播来发起扫描动作;
2)绑定服务来发起扫描动作;
下面我们细说一下这两种方式。

2.1.1 用广播来发起扫描动作

扫描服务的配套receiver是MediaScannerReceiver,它在AndroidManifest.xml里的描述如下:

<receiver android:name="MediaScannerReceiver">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
    </intent-filter>
    <intent-filter>
        <action android:name="android.intent.action.MEDIA_MOUNTED" />
        <data android:scheme="file" />
    </intent-filter>
    <intent-filter>
        <action android:name="android.intent.action.MEDIA_UNMOUNTED" />
        <data android:scheme="file" />
    </intent-filter>
    <intent-filter>
        <action android:name="android.intent.action.MEDIA_SCANNER_SCAN_FILE" />
        <data android:scheme="file" />
    </intent-filter>
</receiver>

MediaScannerReceiver的onReceive()代码如下:

public void onReceive(Context context, Intent intent) {
    final String action = intent.getAction();
    final Uri uri = intent.getData();
    
    if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
        // Scan both internal and external storage
        scan(context, MediaProvider.INTERNAL_VOLUME);  // INTERNAL_VOLUME = "internal"
        scan(context, MediaProvider.EXTERNAL_VOLUME);  // EXTERNAL_VOLUME = "external"
    } else {
        if (uri.getScheme().equals("file")) {
            // handle intents related to external storage
            . . . . . .
            Log.d(TAG, "action: " + action + " path: " + path);
            if (Intent.ACTION_MEDIA_MOUNTED.equals(action)) {
                // scan whenever any volume is mounted
                scan(context, MediaProvider.EXTERNAL_VOLUME);
            } else if (Intent.ACTION_MEDIA_SCANNER_SCAN_FILE.equals(action) &&
                    path != null && path.startsWith(externalStoragePath + "/")) {
                scanFile(context, path);
            }
        }
    }
}

  • 当系统刚刚启动时,收到ACTION_BOOT_COMPLETED广播,此时会把内部卷标(“internal”)和外部卷标(“external”)都扫描一下;
  • 如果收到ACTION_MEDIA_MOUNTED广播,则只扫描外部卷标;
  • 如果收到的是ACTION_MEDIA_SCANNER_SCAN_FILE广播,则扫描具体的文件路径。

当用户插入了扩展介质(一般指SD卡),并且该介质已经被系统正确识别、安装,系统就会发出ACTION_MEDIA_MOUNTED广播。从Android 4.4开始,ACTION_MEDIA_MOUNTED广播只能由系统(系统服务MountService)发出,普通用户是无权发送的。

另外,我们可以通过发送ACTION_MEDIA_SCANNER_SCAN_FILE广播,要求MediaScannerService扫描一下具体的文件。比如说在ExternalStorageProvider的openDocument()函数里,就会设置监听器监听用户是不是在读写模式下close了某个文件,因为close一般表示写入动作已经完成了,那么此时就需要“踢一下”MediaScannerService,让它更新一下自己的数据。这段代码截选如下:
【frameworks/base/packages/externalstorageprovider/src/com/android/externalstorage/ExternalStorageProvider.java】

@Override
public ParcelFileDescriptor openDocument(String documentId, String mode, 
                                                 CancellationSignal signal)
                                                 throws FileNotFoundException 
{
    . . . . . .
            // When finished writing, kick off media scanner
            return ParcelFileDescriptor.open(file, pfdMode, mHandler, 
                                                    new OnCloseListener() {
                @Override
                public void onClose(IOException e) {
                    final Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
                    intent.setData(Uri.fromFile(file));
                    getContext().sendBroadcast(intent);	// 用广播来发起扫描动作
                }
            });
    . . . . . .
}

2.1.2 用MediaScannerConnection来发起扫描动作

除了利用类似ACTION_MEDIA_SCANNER_SCAN_FILE这样的广播,系统中还有一种办法可以发起扫描动作,那就是先利用bindService机制得到的IMediaScannerService代理接口,而后再通过调用该接口的requestScanFile()或scanFile(),同样可以向MediaScannerService发出扫描语义。

不过,我们一般并不直白地去bindService,而是通过一种封装好的辅助类:MediaScannerConnection。该类的定义截选如下:
【frameworks/base/media/java/android/media/MediaScannerConnection.java】

public class MediaScannerConnection implements ServiceConnection {
    private static final String TAG = "MediaScannerConnection";
    private Context mContext;
    private MediaScannerConnectionClient mClient;
    private IMediaScannerService mService;
    private boolean mConnected; // true if connect() has been called since last disconnect()
    private final IMediaScannerListener.Stub mListener = new IMediaScannerListener.Stub()
    . . . . . .
请注意那个mService成员,它就是为了绑定service而设计的。

MediaScannerConnection里设计了两个scanFile()函数,一个动态的,一个静态的。大家不要搞混了。

2.1.2.1 动态形式scanFile()

动态形式scanFile()的代码截选:

public void scanFile(String path, String mimeType) {
    . . . . . .
            mService.requestScanFile(path, mimeType, mListener);
    . . . . . .
}

对于动态形式的scanFile()而言,它只能在MediaScannerConnection成功绑定到MediaScannerService之后调用,此时它简单地调用mService.requestScanFile()将语义传递给MediaScannerService,再由MediaScannerService通过startService()向自己的消息泵线程打入消息。

mService.requestScanFile()的最后一个参数mListener的定义如下:

private final IMediaScannerListener.Stub mListener = new IMediaScannerListener.Stub() {
    public void scanCompleted(String path, Uri uri) {
        MediaScannerConnectionClient client = mClient;
        if (client != null) {
            client.onScanCompleted(path, uri);
        }
    }
};
它是个简单的binder实体。每当MediaScannerService扫描完所指定的一个文件后,就会回调到该实体的scanCompleted()。此时一般会经由client.onScanCompleted()一句间接调用下一次scanFile()的动作,从而使扫描多个文件的动作连贯起来。

2.1.2.2 静态形式scanFile()

静态形式scanFile()的代码截选:

public static void scanFile(Context context, String[] paths, String[] mimeTypes,
                            OnScanCompletedListener callback) {
    ClientProxy client = new ClientProxy(paths, mimeTypes, callback);
    MediaScannerConnection connection = new MediaScannerConnection(context, client);
    client.mConnection = connection;
    connection.connect();  // 内部主要是bindService动作
}

对于静态形式的scanFile()而言,会重新创建一个MediaScannerConnection对象,并通过connect()动作和MediaScannerService联系起来。

请大家注意创建MediaScannerConnection时传入的第二个参数client,它必须实现MediaScannerConnectionClient接口。说穿了是为了监听两种事情:
1)和MediaScannerService之间的连接是否建立好了;
2)MediaScannerService中扫描某文件的动作是否执行完了;
     
MediaScannerConnectionClient接口的定义如下:
【frameworks/base/media/java/android/media/MediaScannerConnection.java】

public interface MediaScannerConnectionClient extends OnScanCompletedListener {
    public void onMediaScannerConnected();
    public void onScanCompleted(String path, Uri uri);
}

在静态形式的scanFile()中,实现MediaScannerConnectionClient接口的类是ClientProxy,它是这样实现onMediaScannerConnected()和onScanCompleted()的:
【frameworks/base/media/java/android/media/MediaScannerConnection.java】

public void onMediaScannerConnected() {
    scanNextPath();
}

public void onScanCompleted(String path, Uri uri) {
    if (mClient != null) {
        mClient.onScanCompleted(path, uri);
    }
    scanNextPath();
}
可以看到一旦连接建立成功或者某个文件扫描完毕,就会调用scanNextPath(),进一步扫描接下来的内容,直到把调用静态scanFile()时传入的paths数组遍历完毕。

void scanNextPath() {
    if (mNextPath >= mPaths.length) {
        mConnection.disconnect();
        return;
    }
    String mimeType = mMimeTypes != null ? mMimeTypes[mNextPath] : null;
    mConnection.scanFile(mPaths[mNextPath], mimeType);
    mNextPath++;
}

实际上,MediaScannerConnection的connect()动作就是在bindService(),它的代码如下:
【frameworks/base/media/java/android/media/MediaScannerConnection.java】

public void connect() {
    synchronized (this) {
        if (!mConnected) {
            Intent intent = new Intent(IMediaScannerService.class.getName());
            intent.setComponent( new ComponentName("com.android.providers.media",
                                        "com.android.providers.media.MediaScannerService"));
            mContext.bindService(intent, this, Context.BIND_AUTO_CREATE);
            mConnected = true;
        }
    }
}
因为bindService()动作本身是异步的,初始时mService的值还是null,所以我们不能直接在这里执行类似mService.requestScanFile()这样的操作。我们必须等到bind动作成功完成,系统回调到MediaScannerConnection的onServiceConnected(),才会给mService赋值:

public void onServiceConnected(ComponentName className, IBinder service) {
    . . . . . .
    synchronized (this) {
        mService = IMediaScannerService.Stub.asInterface(service);
        if (mService != null && mClient != null) {
            mClient.onMediaScannerConnected();
        }
    }
}
如果bind动作是成功的,而且用户在构造MediaScannerConnection对象时传入了client参数。那么此时就会回调mClient的onMediaScannerConnected()函数。

请注意,静态的scanFile()方法最终并没有直接执行requestScanFile(),它先建立了和MediaScannerService的绑定关系,然后在onServiceConnected()中感知到绑定已经成功之后,才会经由ClientProxy间接转过头调用到自己的scanFile()函数,从而执行到requestScanFile()。

ClientProxy、MediaScannerConnection、MediaScannerService三者之间的关系如下图所示:


以MediaScannerConnection对象为桥梁:
1)其mService“指向”MediaScannerService的mBinder;
2)其mClient指向ClientProxy对象;

当然,在看懂上图后,我们也可以不使用默认的ClientProxy,而添加我们自定义的client对象,只要这个client对象实现了MediaScannerConnectionClient接口即可。比如在MediaProvider中,就定义了另一个类ScannerClient类,代码截选如下:
【packages/providers/mediaprovider/src/com/android/providers/media/MediaProvider.java】

private static final class ScannerClient implements MediaScannerConnectionClient {
    String mPath = null;
    MediaScannerConnection mScannerConnection;
    SQLiteDatabase mDb;


    public ScannerClient(Context context, SQLiteDatabase db, String path) {
        mDb = db;
        mPath = path;
        mScannerConnection = new MediaScannerConnection(context, this);
        mScannerConnection.connect();
    }


    @Override
    public void onMediaScannerConnected() {
        . . . . . .
    }


    @Override
    public void onScanCompleted(String path, Uri uri) {
    }
}

这么看来,MediaScannerConnection还真是起连接作用的“connection”,它将发起扫描请求的client和最终执行扫描动作的MediaScannerService连接起来了。我们把上面那张图简化一下,可以看到如下示意图:


以上介绍的就是发起scan动作的方法,接下来我们来看看到底有哪些地方在使用这些方法。

2.2 谁会发起扫描动作

2.2.1 发起者列表

发出ACTION_MEDIA_SCANNER_SCAN_FILE广播的地方:

发起方
相关代码位置
说明
ExternalStorageProvider openDocument()注册OnCloseListener的地方  
ComposeMessageActivity MMS里copyPart()函数中 saveRingtone()、
copyMedia()中都会调用copyPart()。
DownloadProvider openFile()注册OnCloseListener的地方  
EmlAttachmentProvider copyAttachment(),将附件拷到外部下载目录(一般是SD卡)时 provider在update()中处理ATTACHMENT的地方
SoundRecorder addToMediaDB() 录制sample后,要添加进多媒体数据库


利用MediaScannerConnection的地方:

发起方
相关代码位置
说明
AttachmentUtilities saveAttachment() 代码截选见下文
BeamTransferManager      processFiles() NFC方面,
finishTransfer()、handleMessage()处理MSG_NEXT_TRANSFER_TIMER时,都会调用processFiles()。
BluetoothOppService MediaScannerNotifier    没有直接使用MediaScannerConnection.scanFile(),而是编写了自己的MediaScannerNotifier
CalendarDebugActivity                                    doInBackground() DumpDbTask的doInBackground(),将数据库文件存成calendar.db.zip之后,调用MediaScannerConnection.scanFile()
DownloadScanner DownloadScanner 没有直接使用MediaScannerConnection.scanFile(),而是编写了自己的DownloadScanner
FmRecorder addRecordingToDatabase()                              MediaScannerConnection.scanFile(context, 
new String[] { mRecordFile.getPath() },
                null, null);
IngestService ScannerClient 没有直接使用MediaScannerConnection.scanFile(),而是编写了自己的ScannerClient
MediaProvider ScannerClient 没有直接使用MediaScannerConnection.scanFile(),而是编写了自己的ScannerClient
VCardService CustomeMediaScannerConnectionClient                                            没有直接使用MediaScannerConnection.scanFile(),而是编写了自己的CustomeMediaScannerConnectionClient

2.2.2 saveAttachment()中的示例代码

我们举一个实际的例子。在Email模块中,如果附件存入了外部存储器,那么就有必要扫描一次媒体文件了,这样才能够立即将相关文件体现到Gallery、Music中。所以在saveAttachment()函数里,就会调用MediaScannerConnection.scanFile():
【packages/apps/email/emailcommon/src/com/android/emailcommon/utility/AttachmentUtilities.java】

public static void saveAttachment(Context context, InputStream in, Attachment attachment) {
    . . . . . .
        ContentResolver resolver = context.getContentResolver();
        if (attachment.mUiDestination == UIProvider.AttachmentDestination.CACHE) {
            . . . . . .
        } else if (Utility.isExternalStorageMounted()) {
            . . . . . .
            File file = Utility.createUniqueFile(downloads, attachment.mFileName);
            size = copyFile(in, new FileOutputStream(file));
            String absolutePath = file.getAbsolutePath();

            // 尽管下载管理器会扫描媒体文件,但只会在用户运行download APP并点击相关按钮后,
            // 才会进行扫描。所以,我们自己运行一下media scanner,以便把附件立即添加进gallery / music。
            MediaScannerConnection.scanFile(context, new String[] {absolutePath}, 
                                                  null, null);
            . . . . . .
                DownloadManager dm = (DownloadManager) 
                                      context.getSystemService(Context.DOWNLOAD_SERVICE);
                long id = dm.addCompletedDownload(attachment.mFileName, 
                        attachment.mFileName,
                        false /* do not use media scanner */,
                        mimeType, absolutePath, size,
                        true /* show notification */);
                contentUri = dm.getUriForDownloadedFile(id).toString();
            . . . . . .
        } else {
            . . . . . .
            throw new IOException();
        }
    . . . . . .
    context.getContentResolver().update(uri, cv, null, null);
}

2.3 说说实际的扫描动作

前文介绍MediaScannerService的消息泵线程时已经说过,最终ServiceHandler的handleMessage()会调用scanFile()或scan()来完成扫描。现在我们来看看scanFile()、scan()的细节。

2.3.1 scanFile()动作

MediaScannerService的scanFile()定义如下:
【packages/providers/mediaprovider/src/com/android/providers/media/MediaScannerService.java】

private Uri scanFile(String path, String mimeType) {
    String volumeName = MediaProvider.EXTERNAL_VOLUME;
    openDatabase(volumeName);
    MediaScanner scanner = createMediaScanner();
    try {
        String canonicalPath = new File(path).getCanonicalPath();
        return scanner.scanSingleFile(canonicalPath, volumeName, mimeType);
    } catch (Exception e) {
        Log.e(TAG, "bad path " + path + " in scanFile()", e);
        return null;
    }
}
可以看到,scanFile()函数内部借助了辅助类MediaScanner,调用了该类的scanSingleFile()。这个MediaScanner才是重头戏,它的scanSingleFile()代码截选如下: 
【frameworks/base/media/java/android/media/MediaScanner.java】

public Uri scanSingleFile(String path, String volumeName, String mimeType) {
    . . . . . .
        initialize(volumeName);
        prescan(path, true);
        File file = new File(path);
        . . . . . .
        // always scan the file, so we can return the content://media Uri for existing files
        return mClient.doScanFile(path, mimeType, lastModifiedSeconds, file.length(),
                false, true, MediaScanner.isNoMediaPath(path));
    . . . . . .
}
借助了mClient.doScanFile()。

此处的mClient类型为MyMediaScannerClient,mClient的定义是:

private final MyMediaScannerClient mClient = new MyMediaScannerClient();
MyMediaScannerClient类的doScanFile()的代码截选如下:
【frameworks/base/media/java/android/media/MediaScanner.java】

public Uri doScanFile(String path, String mimeType, long lastModified,
        long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) {
    . . . . . .
        FileEntry entry = beginFile(path, mimeType, lastModified,
                fileSize, isDirectory, noMedia);
        . . . . . .
        if (entry != null && (entry.mLastModifiedChanged || scanAlways)) {
            if (noMedia) {
                result = endFile(entry, false, false, false, false, false);
            } else {
                . . . . . .
                . . . . . .
                // we only extract metadata for audio and video files
                if (isaudio || isvideo) {
                    processFile(path, mimeType, this);
                }
                if (isimage) {
                    processImageFile(path);
                }
                result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
            }
        }
    . . . . . .
    return result;
}
因为MyMediaScannerClient是MediaScanner的内嵌类,所以它可以直接调用MediaScanner的processFile()。

现在我们画一张scanFile()的调用关系图:


2.3.2 scan()动作

与scanFile()动作类似,MediaScannerService中扫描目录的动作是scan():
【packages/providers/mediaprovider/src/com/android/providers/media/MediaScannerService.java】

private void scan(String[] directories, String volumeName) {
    . . . . . .
    values.put(MediaStore.MEDIA_SCANNER_VOLUME, volumeName);
    Uri scanUri = getContentResolver().insert(MediaStore.getMediaScannerUri(), values);
    . . . . . .
            MediaScanner scanner = createMediaScanner();
            scanner.scanDirectories(directories, volumeName);
    . . . . . .
        sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_FINISHED, uri));
    . . . . . .
}
同样是借助了辅助类MediaScanner,调用了该类的scanDirectories()。

scanDirectories()的代码截选如下:
【frameworks/base/media/java/android/media/MediaScanner.java】

public void scanDirectories(String[] directories, String volumeName) {
    . . . . . . 
        for (int i = 0; i < directories.length; i++) {
            processDirectory(directories[i], mClient);
        }
    . . . . . .
}

我们画一张scan()的调用关系图:



2.3.3 MediaScanner

顾名思义,MediaScanner就是个“媒体文件扫描器”。它必须打通java层次和C++层次。请大家注意它的两个native函数:native_init()和native_setup(),以及两个重要成员变量:一个是上文刚刚提到的mClient成员,另一个是mNativeContext。

MediaScanner的相关代码截选如下:
【frameworks/base/media/java/android/media/MediaScanner.java】

public class MediaScanner
{
    static {
        System.loadLibrary("media_jni");
        native_init(); // 将java层和c++层联系起来
    }
    . . . . . .
    private long mNativeContext;
    . . . . . .
    public MediaScanner(Context c) {
        native_setup();
        . . . . . .
    }
. . . . . .
    // 一开始就具有明确的mClient对象
    private final MyMediaScannerClient mClient = new MyMediaScannerClient();
    . . . . . .
}
MediaScanner类加载之时,就会同时加载动态链接库“media_jni”,并调用native_init()将java层和c++层联系起来。而且MediaScanner对象一开始就具有明确的mClient对象,类型为MyMediaScannerClient。

经过分析代码,我们发现在C++层会有个与MediaScanner相对应的类,叫作StagefrightMediaScanner。当java层创建MediaScanner对象时,MediaScanner的构造函数就调用了native_setup(),该函数对应到C++层就是android_media_MediaScanner_native_setup(),其代码如下:
【frameworks/base/media/jni/android_media_MediaScanner.cpp】

static void
android_media_MediaScanner_native_setup(JNIEnv *env, jobject thiz)
{
    ALOGV("native_setup");
    MediaScanner *mp = new StagefrightMediaScanner;
    if (mp == NULL) {
        jniThrowException(env, kRunTimeException, "Out of memory");
        return;
    }
    env->SetLongField(thiz, fields.context, (jlong)mp);
}
最后一句env->SetLongField()其实就是在为java层MediaScanner的mNativeContext域赋值。

后续我们会看到,每当C++层执行扫描动作时,还会再创建一个MyMediaScannerClient对象,这个对象和Java层的同名类对应。我们画一张图来说明:


2.3.4 调用到C++层次

不管是扫描文件,还是扫描目录,总之MediaScannerService已经把工作委托给MediaScanner的scanSingleFile()和scanDirectories()了,而这两个函数到头来都是调用MediaScanner自己的native函数,即processFile()和processDirectory()。其声明如下:
【frameworks/base/media/java/android/media/MediaScanner.java】

private native void processDirectory(String path, MediaScannerClient client);
private native void processFile(String path, String mimeType, MediaScannerClient client);

MediaScanner中调用的processFile()对应于C++层的android_media_MediaScanner_processFile()。代码截选如下:
【frameworks/base/media/jni/android_media_MediaScanner.cpp】

static void android_media_MediaScanner_processFile(
                JNIEnv *env, jobject thiz, jstring path,
                jstring mimeType, jobject client)
{
    . . . . . .
    MediaScanner *mp = getNativeScanner_l(env, thiz);
    . . . . . .
    const char *mimeTypeStr =
        (mimeType ? env->GetStringUTFChars(mimeType, NULL) : NULL);
    if (mimeType && mimeTypeStr == NULL) {  // Out of memory
        // ReleaseStringUTFChars can be called with an exception pending.
        env->ReleaseStringUTFChars(path, pathStr);
        return;
    }

    MyMediaScannerClient myClient(env, client); // 构造一个临时的myClient
    MediaScanResult result = mp->processFile(pathStr, mimeTypeStr, myClient);
    if (result == MEDIA_SCAN_RESULT_ERROR) {
        ALOGE("An error occurred while scanning file '%s'.", pathStr);
    }
    . . . . . .
}
注意这里构造了一个局部的(C++层次)MyMediaScannerClient对象,构造myClient时传入的client参数来自于Java层调用processFile()时传入的那个(Java层次)MyMediaScannerClient对象。这个对象会记录在C++层MyMediaScannerClient的mClient域中,这个在前面的示意图中已有表示。

相应的,processDirectory()对应于C++层的android_media_MediaScanner_processDirectory()。代码截选如下:
【frameworks/base/media/jni/android_media_MediaScanner.cpp】

static void android_media_MediaScanner_processDirectory(
        JNIEnv *env, jobject thiz, jstring path, jobject client)
{
    . . . . . .
    MediaScanner *mp = getNativeScanner_l(env, thiz);
    . . . . . .
    MyMediaScannerClient myClient(env, client);
    MediaScanResult result = mp->processDirectory(pathStr, myClient);
    . . . . . .
}

2.3.4.1 processFile()

android_media_MediaScanner_processFile()函数中的那个mp是经由下面这句得到的:

MediaScanner *mp = getNativeScanner_l(env, thiz);
它指向的其实就是StagefrightMediaScanner,所以这里调用的processFile就是:
【frameworks/av/media/libstagefright/StagefrightMediaScanner.cpp】

MediaScanResult StagefrightMediaScanner::processFile(
        const char *path, const char *mimeType,
        MediaScannerClient &client) {
    ALOGV("processFile '%s'.", path);

    client.setLocale(locale());
    client.beginFile();
    MediaScanResult result = processFileInternal(path, mimeType, client);
    client.endFile();
    return result;
}
主要行为在processFileInternal()里:
【frameworks/av/media/libstagefright/StagefrightMediaScanner.cpp】

MediaScanResult StagefrightMediaScanner::processFileInternal(
        const char *path, const char * /* mimeType */,
        MediaScannerClient &client) {
    const char *extension = strrchr(path, '.');
    . . . . . .
    if (!FileHasAcceptableExtension(extension)) {
        return MEDIA_SCAN_RESULT_SKIPPED;
    }

    if (!strcasecmp(extension, ".mid")
            || !strcasecmp(extension, ".smf")
            || !strcasecmp(extension, ".imy")
            . . . . . .
        return HandleMIDI(path, &client);
    }

    sp<MediaMetadataRetriever> mRetriever(new MediaMetadataRetriever);
    int fd = open(path, O_RDONLY | O_LARGEFILE);
    . . . . . .
        status = mRetriever->setDataSource(fd, 0, 0x7ffffffffffffffL);
        close(fd);
    . . . . . .

    const char *value;
    if ((value = mRetriever->extractMetadata(
                    METADATA_KEY_MIMETYPE)) != NULL) {
        status = client.setMimeType(value);
        . . . . . .
    }

    struct KeyMap {
        const char *tag;
        int key;
    };
    static const KeyMap kKeyMap[] = {
        { "tracknumber", METADATA_KEY_CD_TRACK_NUMBER },
        { "discnumber", METADATA_KEY_DISC_NUMBER },
        { "album", METADATA_KEY_ALBUM },
        { "artist", METADATA_KEY_ARTIST },
        . . . . . .
    };
    static const size_t kNumEntries = sizeof(kKeyMap) / sizeof(kKeyMap[0]);
    for (size_t i = 0; i < kNumEntries; ++i) {
        const char *value;
        if ((value = mRetriever->extractMetadata(kKeyMap[i].key)) != NULL) {
            status = client.addStringTag(kKeyMap[i].tag, value);
            . . . . . .
        }
    }
    return MEDIA_SCAN_RESULT_OK;
}

可以看到,processFileInternal()里扫描具体文件的大体流程,无非是先获取多媒体文件的元数据,然后再通过MyMediaScannerClient将元数据信息从C++层传递到Java层。

processFileInternal()里的主要细节有:


调用FileHasAcceptableExtension()函数,看看文件的扩展名是不是属于多媒体文件扩展名,合适的扩展名有:
【frameworks/av/media/libstagefright/StagefrightMediaScanner.cpp】

static bool FileHasAcceptableExtension(const char *extension) {
    static const char *kValidExtensions[] = {
        ".mp3", ".mp4", ".m4a", ".3gp", ".3gpp", ".3g2", ".3gpp2",
        ".mpeg", ".ogg", ".mid", ".smf", ".imy", ".wma", ".aac",
        ".wav", ".amr", ".midi", ".xmf", ".rtttl", ".rtx", ".ota",
        ".mkv", ".mka", ".webm", ".ts", ".fl", ".flac", ".mxmf",
        ".avi", ".mpeg", ".mpg", ".awb", ".mpga"
    };
    . . . . . .
}
如果扩展名不合适,则直接return MEDIA_SCAN_RESULT_SKIPPED。


看看文件是不是midi文件,如果是midi文件,则以HandleMIDI()来处理。
【frameworks/av/media/libstagefright/StagefrightMediaScanner.cpp】

    if (!strcasecmp(extension, ".mid")
            || !strcasecmp(extension, ".smf")
            || !strcasecmp(extension, ".imy")
            || !strcasecmp(extension, ".midi")
            || !strcasecmp(extension, ".xmf")
            || !strcasecmp(extension, ".rtttl")
            || !strcasecmp(extension, ".rtx")
            || !strcasecmp(extension, ".ota")
            || !strcasecmp(extension, ".mxmf")) {
        return HandleMIDI(path, &client);
    }
从HandleMIDI()的代码看,要解析并提取midi文件的元数据,需要用到一种EAS引擎,利用EAS_ParseMetaData()解析出时长信息。并调用MyMediaScannerClient的addStringTag()。


如果是其他支持的多媒体文件,则利用工具类MediaMetadataRetriever来获取文件的元数据,并将得到的元数据传递给MyMediaScannerClient。其实MediaMetadataRetriever内部是利用系统服务“media.player”来解析多媒体文件的,这个系统服务对应的代理接口是IMediaPlayerService,它有个成员函数createMetadataRetriever()可以用于获取IMediaMetadataRetriever接口,而后就可以调用该接口的setDataSource()和extractMetadata()了。


processFileInternal()里主要通过两个函数,向Java层的MyMediaScannerClient传递数据,一个是setMimeType(),另一个是addStringTag()。以C++层的setMimeType()为例,其代码如下:
【frameworks/base/media/jni/android_media_MediaScanner.cpp】

virtual status_t setMimeType(const char* mimeType)
{
    ALOGV("setMimeType: %s", mimeType);
    jstring mimeTypeStr;
    if ((mimeTypeStr = mEnv->NewStringUTF(mimeType)) == NULL) {
        mEnv->ExceptionClear();
        return NO_MEMORY;
    }

    mEnv->CallVoidMethod(mClient, mSetMimeTypeMethodID, mimeTypeStr);
    mEnv->DeleteLocalRef(mimeTypeStr);
    return checkAndClearExceptionFromCallback(mEnv, "setMimeType");
}
基本上只是通过JNI技术,调用到Java层的setMimeType()而已。

现在我们画一张关于扫描文件的简单示意图,来整理一下思路。大家顺着箭头看图就可以了。


2.3.4.2 processDirectory()

按理说,和processFile()类似,processDirectory()最终对应的代码也应该在StagefrightMediaScanner里,但是StagefrightMediaScanner并没有编写这个函数,又因为StagefrightMediaScanner继承于MediaScanner(C++层次),所以实际上使用的是MediaScanner的ProcessDirectory()
【frameworks/av/media/libmedia/MediaScanner.cpp】

MediaScanResult MediaScanner::processDirectory(
        const char *path, MediaScannerClient &client) {
    int pathLength = strlen(path);
    . . . . . .
    char* pathBuffer = (char *)malloc(PATH_MAX + 1);
    . . . . . .
    strcpy(pathBuffer, path);
    . . . . . .
    client.setLocale(locale());
    MediaScanResult result = doProcessDirectory(pathBuffer, pathRemaining, client, false);
    free(pathBuffer);
    return result;
}

【frameworks/av/media/libmedia/MediaScanner.cpp】

MediaScanResult MediaScanner::doProcessDirectory(char *path, int pathRemaining, 
                                                 MediaScannerClient &client, bool noMedia) {
    char* fileSpot = path + strlen(path);
    struct dirent* entry;

    if (shouldSkipDirectory(path)) {
        . . . . . .
        return MEDIA_SCAN_RESULT_OK;
    }

    // Treat all files as non-media in directories that contain a  ".nomedia" file
    if (pathRemaining >= 8 /* strlen(".nomedia") */ ) {
        strcpy(fileSpot, ".nomedia");
        if (access(path, F_OK) == 0) {
            ALOGV("found .nomedia, setting noMedia flag");
            noMedia = true;
        }
        . . . . . .
    }

    DIR* dir = opendir(path);
    . . . . . .
    MediaScanResult result = MEDIA_SCAN_RESULT_OK;
    while ((entry = readdir(dir))) {
        if (doProcessDirectoryEntry(path, pathRemaining, client, noMedia, entry, fileSpot)
                == MEDIA_SCAN_RESULT_ERROR) {
            result = MEDIA_SCAN_RESULT_ERROR;
            break;
        }
    }
    closedir(dir);
    return result;
}

doProcessDirectory()先判断需要扫描的目录是不是应该“跳过”的目录,如果是的话,则直接return MEDIA_SCAN_RESULT_OK。判断函数shouldSkipDirectory()的代码如下:
【frameworks/av/media/libmedia/MediaScanner.cpp】

bool MediaScanner::shouldSkipDirectory(char *path) {
    if (path && mSkipList && mSkipIndex) {
        int len = strlen(path);
        int idx = 0;
        int startPos = 0;
        while (mSkipIndex[idx] != -1) {
            if ((len == mSkipIndex[idx])
                && (strncmp(path, &mSkipList[startPos], len) == 0)) {
                return true;
            }
            startPos += mSkipIndex[idx] + 1; // extra char for the delimiter
            idx++;
        }
    }
    return false;
}
其实就是比对一下“需要扫描的目录”是否存在于mSkipList列表中。这个列表的内容其实来自于“testing.mediascanner.skiplist”属性,该属性可以记录若干目录名,目录名之间以逗号分隔。在C++层的MediaScanner构造函数中,会调用loadSkipList()来读取这个属性,解析属性中记录的所有目录名并写入mSkipList列表。

接着doProcessDirectory()用一个while循环多次调用doProcessDirectoryEntry(),其内部在必要时候,会再次调用doProcessDirectory()分析子目录。while语句的循环判断部分用到了readdir()函数,readdir()是linux上返回所指目录中“下一个进入点”(next entry)的函数,我们常常在一个while循环中调用它,以便遍历出目录中的所有内容。

doProcessDirectoryEntry()函数的定义截选如下:
【frameworks/av/media/libmedia/MediaScanner.cpp】

MediaScanResult MediaScanner::doProcessDirectoryEntry(
        char *path, int pathRemaining, MediaScannerClient &client, bool noMedia,
        struct dirent* entry, char* fileSpot) {
    struct stat statbuf;
    const char* name = entry->d_name;
    . . . . . .
    int type = entry->d_type;
    . . . . . .
    if (type == DT_DIR) {   // 普通目录
        . . . . . .
        if (stat(path, &statbuf) == 0) {
            status_t status = client.scanFile(path, statbuf.st_mtime, 0,
                    true /*isDirectory*/, childNoMedia);
            . . . . . .
        }

        // and now process its contents
        strcat(fileSpot, "/");
        MediaScanResult result = doProcessDirectory(path, pathRemaining - nameLength - 1,
                client, childNoMedia);
        . . . . . .
    } else if (type == DT_REG) {    // 普通文件
        stat(path, &statbuf);
        status_t status = client.scanFile(path, statbuf.st_mtime, statbuf.st_size,
                false /*isDirectory*/, noMedia);
        . . . . . .
    }
    return MEDIA_SCAN_RESULT_OK;
}
不管当前处理的入口类型是“目录”还是“文件”,最终都是依靠client的scanFile()来处理,只不过前者倒数第二个参数(isDirectory)为true,后者为false而已。

client.scanFile()最终也是要调回到Java层的,MyMediaScannerClient的scanFile()代码截选如下:
【frameworks/base/media/jni/android_media_MediaScanner.cpp】

virtual status_t scanFile(const char* path, long long lastModified,
        long long fileSize, bool isDirectory, bool noMedia)
{
    . . . . . .
    jstring pathStr;
    if ((pathStr = mEnv->NewStringUTF(path)) == NULL) {
        mEnv->ExceptionClear();
        return NO_MEMORY;
    }
    mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified,
            fileSize, isDirectory, noMedia);
    mEnv->DeleteLocalRef(pathStr);
    return checkAndClearExceptionFromCallback(mEnv, "scanFile");
}

【frameworks/base/media/java/android/media/MediaScanner.java】

@Override
public void scanFile(String path, long lastModified, long fileSize,
        boolean isDirectory, boolean noMedia) {
    doScanFile(path, null, lastModified, fileSize, isDirectory, false, noMedia);
}
调用到doScanFile()函数。

现在我们再画一张关于扫描目录的简单示意图:


2.3.4.3 doScanFile()和MediaProvider

站在Java层次来看,不管是扫描具体的文件,还是扫描一个目录,最终都会走到Java层MyMediaScannerClient的doScanFile()。在前文我们已经列出过这个函数的代码,为了说明问题,这里再列一下其中的重要句子:
【frameworks/base/media/java/android/media/MediaScanner.java】

public Uri doScanFile(String path, String mimeType, long lastModified,
        long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) {
    . . . . . .
        FileEntry entry = beginFile(path, mimeType, lastModified,
                fileSize, isDirectory, noMedia);
        . . . . . .
                if (isaudio || isvideo) {
                    processFile(path, mimeType, this);
                }
                if (isimage) {
                    processImageFile(path);
                }
                result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
    . . . . . .
    return result;
}
本小节着重看一下其中和MediaProvider相关的beginFile()和endFile()。

beginFile()是为了后续和MediaProvider打交道,准备一个FileEntry。FileEntry的定义如下:
【frameworks/base/media/java/android/media/MediaScanner.java】

private static class FileEntry {
    long mRowId;
    String mPath;
    long mLastModified;
    int mFormat;
    boolean mLastModifiedChanged;

    FileEntry(long rowId, String path, long lastModified, int format) {
        mRowId = rowId;
        mPath = path;
        mLastModified = lastModified;
        mFormat = format;
        mLastModifiedChanged = false;
    }
    . . . . . .
}
FileEntry的几个成员变量,其实体现了查表时的若干列的值。

beginFile()的代码截选如下:
【frameworks/base/media/java/android/media/MediaScanner.java】

public FileEntry beginFile(String path, String mimeType, long lastModified,
        long fileSize, boolean isDirectory, boolean noMedia) {
    . . . . . .
    FileEntry entry = makeEntryFor(path);   // 从MediaProvider中查出该文件或目录对应的入口
    . . . . . .
    if (entry == null || wasModified) {
        if (wasModified) {
            entry.mLastModified = lastModified;
        } else {
            // 如果前面没查到FileEntry,就在这里new一个新的FileEntry
            entry = new FileEntry(0, path, lastModified,
                    (isDirectory ? MtpConstants.FORMAT_ASSOCIATION : 0));
        }
        entry.mLastModifiedChanged = true;
    }
    . . . . . .
    return entry;
}
其中调用的makeEntryFor()内部就会查询MediaProvider:

FileEntry makeEntryFor(String path) {
    String where;
    String[] selectionArgs;

    Cursor c = null;
    try {
        where = Files.FileColumns.DATA + "=?";
        selectionArgs = new String[] { path };
        c = mMediaProvider.query(mPackageName, mFilesUriNoNotify, 
                                      FILES_PRESCAN_PROJECTION,
                                      where, selectionArgs, null, null);
        if (c.moveToFirst()) {
            long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX);
            int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX);
            long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX);
            return new FileEntry(rowId, path, lastModified, format);
        }
    } catch (RemoteException e) {
    } finally {
        if (c != null) {
            c.close();
        }
    }
    return null;
}
查询语句中用的FILES_PRESCAN_PROJECTION的定义如下:

    private static final String[] FILES_PRESCAN_PROJECTION = new String[] {
            Files.FileColumns._ID, // 0
            Files.FileColumns.DATA, // 1
            Files.FileColumns.FORMAT, // 2
            Files.FileColumns.DATE_MODIFIED, // 3
    };
看到了吗,特意要去查一下MediaProvider中记录的待查文件的最后修改日期。能查到就返回一个FileEntry,如果查询时出现异常就返回null。beginFile()的lastModified参数可以理解为是从文件系统里拿到的待查文件的最后修改日期,它应该是最准确的。而MediaProvider里记录的信息则有可能“较老”。beginFile()内部通过比对这两个“最后修改日期”,就可以知道该文件是不是真的改动了。如果的确改动了,就要把FileEntry里的mLastModified调整成最新数据。

基本上而言,beginFile()会返回一个FileEntry。如果该阶段没能在MediaProvider里找到文件对应的记录,那么FileEntry对象的mRowId会为0,而如果找到了,则为非0值。

与beginFile()相对的,就是endFile()了。endFile()是真正向MediaProvider数据库插入数据或更新数据的地方。当FileEntry的mRowId为0时,会考虑调用:

result = mMediaProvider.insert(mPackageName, tableUri, values);
而当mRowId为非0值时,则会考虑调用:

mMediaProvider.update(mPackageName, result, values, null, null);
这就是改变MediaProvider中相关信息的最核心句子啦。

endFile()的代码截选如下:
【frameworks/base/media/java/android/media/MediaScanner.java】

private Uri endFile(FileEntry entry, boolean ringtones, boolean notifications,
        boolean alarms, boolean music, boolean podcasts)
        throws RemoteException {
    . . . . . .
    ContentValues values = toValues();
    String title = values.getAsString(MediaStore.MediaColumns.TITLE);
    if (title == null || TextUtils.isEmpty(title.trim())) {
        title = MediaFile.getFileTitle(values.getAsString(MediaStore.MediaColumns.DATA));
        values.put(MediaStore.MediaColumns.TITLE, title);
    }
    . . . . . .
    long rowId = entry.mRowId;
    if (MediaFile.isAudioFileType(mFileType) && (rowId == 0 || mMtpObjectHandle != 0)) {
        . . . . . .
        values.put(Audio.Media.IS_ALARM, alarms);
        values.put(Audio.Media.IS_MUSIC, music);
        values.put(Audio.Media.IS_PODCAST, podcasts);
    } else if (mFileType == MediaFile.FILE_TYPE_JPEG && !mNoMedia) {
        . . . . . .
    }

    . . . . . .
    if (rowId == 0) {
        . . . . . .
        // 扫描的是新文件,insert记录。如果是目录的话,必须比它所含有的所有文件更早插入记录,
        // 所以在批量插入时,就需要有更高的优先权。如果是文件的话,而且我们现在就需要其对应
        // 的rowId,那么应该立即进行插入,此时不过多考虑批量插入。
        if (inserter == null || needToSetSettings) {
            if (inserter != null) {
                inserter.flushAll();
            }
            result = mMediaProvider.insert(mPackageName, tableUri, values);
        } else if (entry.mFormat == MtpConstants.FORMAT_ASSOCIATION) {
            inserter.insertwithPriority(tableUri, values);
        } else {
            inserter.insert(tableUri, values);
        }

        if (result != null) {
            rowId = ContentUris.parseId(result);
            entry.mRowId = rowId;
        }
    } else {
        . . . . . .
        mMediaProvider.update(mPackageName, result, values, null, null);
    }
    . . . . . .
    return result;
}
除了直接调用mMediaProvider.insert()向MediaProvider中写入数据,函数中还有一种方式是经由inserter对象,其类型为MediaInserter。

MediaInserter也是向MediaProvider中写入数据,最终大体上会走到其flush()函数,该函数的代码如下:
【frameworks/base/media/java/android/media/MediaInserter.java】

    private void flush(Uri tableUri, List<ContentValues> list) throws RemoteException {
        if (!list.isEmpty()) {
            ContentValues[] valuesArray = new ContentValues[list.size()];
            valuesArray = list.toArray(valuesArray);
            mProvider.bulkInsert(mPackageName, tableUri, valuesArray);
            list.clear();
        }
    }

3 小节

写了这么多,终于看到MediaScannerService是如何更新MediaProvider的了。当然,里面还有大量的细节,本文就不展开来讲了,要不然相信大家头壳都得炸掉。那么就先写这么多了。

作者:codefly 发表于2016/11/16 21:34:06 原文链接
阅读:17 评论:0 查看评论

Unity3D开发小贴士(十一)ToLua协同程序

$
0
0

Unity3D开发小贴士(二)协程(Coroutine)中介绍了在Unity3D中使用协同程序(C#),Lua语法小贴士(八)协同程序中介绍了使用Lua的协同程序。Lua的协同程序功能相对有限,所以ToLua(参考Unity3D开发小贴士(三)愉快的使用Lua开发)帮我们对Lua的协同程序进行了扩展。

具体的文件是ToLua/Lua/System/coroutine.lua。

它对Lua原生的coroutine进行了扩展。

coroutine.start(f, ...)

输入f为函数,...为f所需的参数,返回协程的句柄。与原生的coroutine.create方法相比,为协程增加了一个定时器,使得协程在当前帧的LateUpdate里执行。

coroutine.wait(t, co, ...)

t为秒数,co为协同程序句柄,...为等待结束继续执行时所需要的参数。

coroutine.step(t, co, ...)

t为帧数,co为协同程序句柄,...为等待结束继续执行时所需要的参数。

coroutine.www(www, co)

www为WWW的对象,co为协同程序句柄(为什么没有参数???),会在WWW下载完毕之后继续协程。

coroutine.stop(co)

co为协同程序。会停止协同程序相关的计时器(wait\step\www)。

示例:

local co
co = coroutine.start(function()
    print("coroutine")
    coroutine.wait(1,co)
    print("wait")
end)


作者:ecidevilin 发表于2016/11/16 21:47:14 原文链接
阅读:22 评论:0 查看评论

Android多屏幕适配笔记

$
0
0

1、常用单位及其关系

 px:像素

inch:英寸

pt:1/72 英寸

dpi:一英寸长的直线上的像素点的数量,即像素密度。不同的设备,dpi值不同,显示效果不同,dpi的值跟设备硬件有关。标准值是160dp。

dp(dip):独立像素密度。即在标准屏幕下,1个像素点的长度,标准屏幕是160dpi,可以理解为1英寸长度上有160个像素。标准屏幕中1dp=1px。

px = dp*(dpi/160);//当dpi=160时,1px=1dp

分辨率:屏幕上长宽方向上像素点的数量,即一个屏幕上像素的数量。

例如:720*1280 = 屏幕x轴上有720个像素,屏幕y轴上有1280个像素

分辨率单位:dpi(点每英寸)、lpi(线每英寸)、ppi(像素每英寸)

屏幕的物理尺寸:屏幕对角线的长度,单位是inch

sp:专用于设定文字大小,受dpi影响和用户的字体偏好设定影响。

各单位和px的换算关系见TypedValue.applyDimension方法

public static float applyDimension(int unit, float value,
                                       DisplayMetrics metrics)
    {
        switch (unit) {
        case COMPLEX_UNIT_PX:
            return value;
        case COMPLEX_UNIT_DIP:
            return value * metrics.density;
        case COMPLEX_UNIT_SP:
            return value * metrics.scaledDensity;
        case COMPLEX_UNIT_PT:
            return value * metrics.xdpi * (1.0f/72);
        case COMPLEX_UNIT_IN:
            return value * metrics.xdpi;
        case COMPLEX_UNIT_MM:
            return value * metrics.xdpi * (1.0f/25.4f);
        }
        return 0;
    }

//显示器
DisplayMetrics d = getResources().getDisplayMetrics();//可以获得scaledDensity,densityDpi,heightPixels,widthPixels等信息。
Configuration configuration = getResources().getConfiguration();//获取设备的配置信息
//configuration.screenHeightDp  当前屏幕可用空间的高度,单位是dp
//configuration.screenWidthDp   当前屏幕可用空间的宽度,单位是dp
//configuration.densityDpi      当前设备的dpi信息

例子:

已知设备1080*1920,使用DisplayMetrics获取的实际信息是1080*1776y轴方向上的像素有误差是因为软键盘,实际屏幕要小。

使用Configuration获取的设备的dpi=480dpi,根据公式px=dp*(dpi/160)

现在px=1080dpi=480,则dp=360.

使用configuration.screenWidthDp得到的数值为360,和上面用公式算出的一致。

但是y轴方向上用公式计算出来的应该是1776/3=592.但是用scrrenHeightDp获取的只有567

 

通过源码验证上述是否成立:

applyDimension方法就是通过输入的任何值转换成px,也就是说,该方法是任何单位和px的换算关系。

COMPLEX_UNIT_DIP 就是dip单位,就是平时说的dp。和px的换算关系是

value * metrics.density;

metrics.density是密度,默认值是

SystemProperties.getInt("qemu.sf.lcd_density",SystemProperties.getInt("ro.sf.lcd_density", DENSITY_DEFAULT)) / 160

160是中密度屏幕的标准dpi

所以屏幕密度density=设备dpi/160

DENSITY_DEFAULT=160

densityDpi=SystemProperties.getInt("qemu.sf.lcd_density",SystemProperties.getInt("ro.sf.lcd_density", DENSITY_DEFAULT))/ DENSITY_DEFAULT

density= densityDpi/ DENSITY_DEFAULT

scaledDensity=density

xdpi= densityDpi

ydpi= densityDpi

但是实际上得到的xdpi!=ydpi

xdpi:x轴屏幕每英寸的实际物理像素


2、换算方程式

px=dp*(dpi/160)
px=sp* metrics.scaledDensity   px=sp*(dpi/160)
px=pt*xdpi*(1.0f/72)   pt*(1/72)=inch
px=inch*xdpi
px=mm*xdpi*(1.0f*25.4f)  mm*(1.0f/25.4f)=inch

易乱点:

分辨率1080*1920单位是像素,有的认为单位是dpi,但是通过源码验证,单位就是px

物理尺寸单位是inch

dpi跟设备有关系

只有在dpi=160时,1px=1dp


3、资源查找原则

app运行时,系统会根据属性选择适配的资源进行展示。如果有符合的资源则使用,反之,当符合的资源不存在时,系统会去寻找最相近的可用资源来代替。但是,查找的属性不同,查找的顺序会有所有差异。

 

res文件夹下的资源文件以优先级从高到低排列,命名规则:资源名-属性1-属性2-…..

v15修饰的资源仅用于4.0及以上的设备

w640dp修饰的适配640dp宽度的设备

h720dp修饰的资源适配720dp高度的设备

屏幕密度属性ldpimdpihdpixhdpixxhdpinodpitvdpi

ldpi=120

mdpi=160

hdpi=240

xhdpi=320

xxhdpi=480

xxxhdpi=640

 

其中nodpi用于开发者不希望系统对图片进行缩放的情况

tvdpi介于hdpimdpi之间。tvdpi一般在213左右,多用于android系统的只能电视,大部分app很少用到

 

dpi属性来说,查找的顺序为,高dpi的资源优先。例如,没能找到hdpi的图片资源,则系统的搜索顺序是

drawable-xhdpi->drawable-xxhdpi->drawable-mdip->drawable->drawable->drawable-ldpi

这里drawable被认为比drawable-ldpi更接近hdpi

 

有时候,我们的图片资源不一定是从drawable文件夹中读取的,还有可能是从sd卡上读取的,或者从网络上下载的。这个时候,我们需要注意,默认情况下,通过BitmapFactory.decodeFile()函数生成的图片被认为是MDPI的,如果想让图片也获得与drawable文件夹相似的缩放能力,则需要通过BitmapFactory.Option.inDensity属性设置(例如如果图片是为hdpi准备的,则设置为240)。

 

对于screan size,查找的顺序则是小尺寸优先,大尺寸放弃。例如,在Galaxy note 2上执行apk时,如果未能找到layout-large资源,则查找顺序为:layout-normal->layout->layout-small,不会查找layout-xlarge


4、屏幕适配方案一

步骤:

1) 假设设计图的基准是720*1280,那么在设计图中1px对于1920*1080分辨率的手机应该是1.5px




在布局文件中就可以直接使用像素标识了,其实本质上是百分比

布局文件中的使用,比如说现在做一个670*80px的按钮,在density=1的时候,1dp=1px,在density=2.0的手机,1px=2.0dp,在density=1.5的手机中1px=1.5dp

如果使用dp设置这个button的大小,以2.0为基准,设置大小335*40dp

density=1.5的手机上,实际占的大小是502.5*60px。如果手机的分辨率是480*800px的就会出现button显示不全的情况。

 

但是,使用上面指定分辨率属性资源的,根据不同的手机分辨率应用自动寻找对应的资源文件。dimen_670_dip*dimen_80_dip在分辨率720*1280手机中是670*80px

1080*1920分辨率的手机中是1005*120px。不需要计算就可以得到相应的尺寸。(不需要人为去计算,根据资源查找原则,系统会自动选择最适合屏幕的资源)

<Button 
           android:layout_width="@dimen/dimen_670_dip"
           android:layout_height="@dimen/dimen_80_dip"
           android:text="登陆"
/>

因为大小按照不同的手机不同的分辨率进行了适配但是字体也要进行适配不然大小变小了字体没有适配就会出现太小或太大的问题

ptsp的换算关系还跟用户的设置有关系,所以最好的方法就是,字体大小也使用px单位的换算。


5、屏幕适配方案二-------谷歌百分比布局库

android-support-percent.jar

方案一的适配已经运用在多款互联网应用,但是方案一的适配有一个缺点,就是会增加apk的大小。

使用谷歌的百分比布局库就可以解决这个问题

百分比布局库就是讲RelativeLayout换成PercentRelativeLayout

将FrameLayout换成PercentFrameLayout

支持宽高设置和margin设置

 

Eclipse中直接将jar复制进libs文件夹,并依赖库percent

F:\Android\SDK\extras\android\support\percent

sdk路径/extras/android/support/percent

AndroidStudiobuild.gradle添加compile 'com.android.support:percent:22.2.0'

使用示例

<android.support.percent.PercentRelativeLayout
   xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
        <TextView
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:layout_heightPercent="20%"
            app:layout_widthPercent="30%"
            android:text="w:30%,h:20%" />
    </android.support.percent.PercentRelativeLayout>

6、常用方法

/**
     * 根据手机的分辨率从 dp 的单位 转成为 px(像素)
     */
    public static int dip2px(Context context, float dpValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }

    /**
     * 根据手机的分辨率从 px(像素) 的单位 转成为 dp
     */
    public static int px2dip(Context context, float pxValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (pxValue / scale + 0.5f);
    }


    public static int getScreenWidth(Activity activity){
        DisplayMetrics dm = new DisplayMetrics();
        activity.getWindowManager().getDefaultDisplay().getMetrics(dm);
        int screenWidth = dm.widthPixels;
        return screenWidth;
    }

    public static int getScreenHeigth(Activity activity){
        DisplayMetrics dm = new DisplayMetrics();
        activity.getWindowManager().getDefaultDisplay().getMetrics(dm);
        int screenHeigh = dm.heightPixels;
        return screenHeigh;
    }


    public static int measureHeight(View view){

        int w = View.MeasureSpec.makeMeasureSpec(0,View.MeasureSpec.UNSPECIFIED);
        int h = View.MeasureSpec.makeMeasureSpec(0,View.MeasureSpec.UNSPECIFIED);
        view.measure(w, h);
        int height =view.getMeasuredHeight();
        return height;
    }

    public static int measureWidth(View view){
        int w = View.MeasureSpec.makeMeasureSpec(0,View.MeasureSpec.UNSPECIFIED);
        int h = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
        view.measure(w, h);
        int width =view.getMeasuredWidth();
        return width;
    }




参考资料:

谷歌官方资料:http://developer.android.com/guide/topics/resources/providing-resources.html

Android屏幕适配工具包(像素最大值是599pxsp最大值是49sp)http://download.csdn.net/detail/zmobs/8597341

屏幕分辨率信息查询:http://screensiz.es/phone

百分比布局库使用示例:https://github.com/JulienGenoud/android-percent-support-lib-sample

Android屏幕适配工具包2(像素最大值是2000pxsp最大值是200)

开源,原创,实用Android 屏幕适配方案分享http://blog.csdn.net/i7788/article/details/44937277

Android多屏幕适配学习笔记

开源,原创,实用Android 屏幕适配方案分享
















作者:qq_27570955 发表于2016/11/17 22:37:50 原文链接
阅读:7 评论:0 查看评论

【Android图像处理】老照片滤镜(效果)

$
0
0

说到老照片,很多人就会想起儿时的照片。没错,老照片就是这样的,我称之为情怀滤镜。

先说一下Android图像矩阵处理(图片来源  慕课网)


也就是说,每一个矩阵都对应着一个唯一的滤镜(效果)。

那么,老照片滤镜(效果)是一个什么样的矩阵呢?

先看一下代码:

	//老照片
	public static Bitmap OldPhoto(Bitmap bm){
		int Width = bm.getWidth();
		int Height = bm.getHeight();

		Bitmap bitmap = Bitmap.createBitmap(Width, Height, Bitmap.Config.ARGB_8888);

		int color = 0;
		int r,g,b,a,r1,g1,b1;

		int[] oldPx = new int[Width * Height];
		int[] newPx = new int[Width * Height];
		bm.getPixels(oldPx, 0, Width, 0, 0, Width, Height);
		
		for(int i = 0; i < Width * Height; i++){
			color = oldPx[i];

			r = Color.red(color);
			g = Color.green(color);
			b = Color.blue(color);
			a = Color.alpha(color);
			
			//老照片矩阵
			r1 = (int) (0.393 * r + 0.769 * b + 0.189 * b);
			g1 = (int) (0.349 * r + 0.686 * g + 0.168 * b);
			b1 = (int) (0.272 * r + 0.534 * g + 0.131 * b);

			//检查各像素值是否超出范围
			if(r1 > 255){
				r1 = 255;
			}

			if(g1 > 255){
				g1 = 255;
			}

			if(b1 == 255){
				b1 = 255;
			}
			newPx[i] = Color.argb(a, r1, g1, b1);
		}
		bitmap.setPixels(newPx, 0, Width, 0, 0, Width, Height);	
		return bitmap;
	}
老照片矩阵就是这样的:

0.393 0.769 0.189
0.349 0.686 0.168
0.272 0.534 0.131

每一行之和都为1,就是说,每个点的rgb值都是原来rgb值按照这个比例实现的。

现在来看一下效果:


满满的情怀。

作者:qq_32353771 发表于2016/11/17 22:42:37 原文链接
阅读:21 评论:0 查看评论

OkHttp面试之--HttpEngine中的sendRequest方法详解

$
0
0

上一节我们介绍了OkHttp网络异步请求的整个流程。其中在流程的最后阶段,我们发现最终创建了HttpEngine对象,并分别调用的此对象的sendRequest和readResponse方法。这两个方法 分别有它相应的作用。这一节我们着重来分析sendRequest流程。


以下是sendRequest的整个方法中的内容:

public void sendRequest() throws RequestException, RouteException, IOException {
    if (cacheStrategy != null) return; // Already sent.
    if (httpStream != null) throw new IllegalStateException();

    Request request = networkRequest(userRequest);

    InternalCache responseCache = Internal.instance.internalCache(client);
    Response cacheCandidate = responseCache != null
        ? responseCache.get(request)
        : null;
    long now = System.currentTimeMillis();
    cacheStrategy = new CacheStrategy.Factory(now, request, cacheCandidate).get();
    networkRequest = cacheStrategy.networkRequest;
    cacheResponse = cacheStrategy.cacheResponse;

    if (responseCache != null) {
      responseCache.trackResponse(cacheStrategy);
    }

    if (cacheCandidate != null && cacheResponse == null) {
      closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
    }

    // If we're forbidden from using the network and the cache is insufficient, fail.
    if (networkRequest == null && cacheResponse == null) {
      userResponse = new Response.Builder()
          .request(userRequest)
          .priorResponse(stripBody(priorResponse))
          .protocol(Protocol.HTTP_1_1)
          .code(504)
          .message("Unsatisfiable Request (only-if-cached)")
          .body(EMPTY_BODY)
          .build();
      return;
    }

    // If we don't need the network, we're done.
    if (networkRequest == null) {
      userResponse = cacheResponse.newBuilder()
          .request(userRequest)
          .priorResponse(stripBody(priorResponse))
          .cacheResponse(stripBody(cacheResponse))
          .build();
      userResponse = unzip(userResponse);
      return;
    }

    // We need the network to satisfy this request. Possibly for validating a conditional GET.
    boolean success = false;
    try {
      httpStream = connect();
      httpStream.setHttpEngine(this);

      if (writeRequestHeadersEagerly()) {
        long contentLength = OkHeaders.contentLength(request);
        if (bufferRequestBody) {
          if (contentLength > Integer.MAX_VALUE) {
            throw new IllegalStateException("Use setFixedLengthStreamingMode() or "
                + "setChunkedStreamingMode() for requests larger than 2 GiB.");
          }

          if (contentLength != -1) {
            // Buffer a request body of a known length.
            httpStream.writeRequestHeaders(networkRequest);
            requestBodyOut = new RetryableSink((int) contentLength);
          } else {
            // Buffer a request body of an unknown length. Don't write request headers until the
            // entire body is ready; otherwise we can't set the Content-Length header correctly.
            requestBodyOut = new RetryableSink();
          }
        } else {
          httpStream.writeRequestHeaders(networkRequest);
          requestBodyOut = httpStream.createRequestBody(networkRequest, contentLength);
        }
      }
      success = true;
    } finally {
      // If we're crashing on I/O or otherwise, don't leak the cache body.
      if (!success && cacheCandidate != null) {
        closeQuietly(cacheCandidate.body());
      }
    }
  }

可以看到sendRequest方法比较长,我对它进行分块解析。其中主要分以下两大块
1 先从 Cache 中判断当前请求是否可以从缓存中返回

这里写图片描述

2 如果没有Cache则连接网络
这里写图片描述

先来看下connect方法中是如何创建HttpStream对象的

这里写图片描述

调用SteamAllocation.newStream的方法创建HttpStream对象并返回。点进去代码如下所示:

这里写图片描述
从上图中可以看出,在newStream方法中先通过findHealthyConnection方法获取一个RealConnection对象,实际上就是查找可用的Socket对象。在OkHttp框架中有一个特点就是OkHttp可以使用一个Socket对象来维护拥有过个ip的Server端,对于Socket的实现后续再单独讲解,此处不再做介绍。
获取RealConnction对象之后,根据此对象再获取相应的HttpStream对象,我们一般返回的是Http1xStream对象,最后将resultStream赋值给全局变量stream。而这个全局变量会再下一节readResponse方法中再使用。



注意:本节主要对于sendRequest方法中比较核心的代码进行的跟踪分析,在此方法中还有对Request请求的Head和Body的添加操作并没有进行详细描述。感兴趣的同学可以自行研究。

下一节继续讲解HttpEngine.readResponse方法的流程

作者:zxm317122667 发表于2016/11/17 22:52:35 原文链接
阅读:46 评论:0 查看评论

【Android图像处理】圆角滤镜(效果)

$
0
0

说到圆角滤镜(效果)很多人会想到app的图标,没错,就是图标。圆角化的图片用来做图标很美观,这是事实。国人喜爱的iPhone的应用图标采用的就是圆角化,很多Android手机的应用突变也是如此。

现在假设有一张画布,先画一张图片,再画一个和图片一样大的圆角矩形,选取矩形框内的图片作为新的图片,那么原图就变成了圆角图片,就实现了圆角滤镜(效果)。

看一下代码:

	//圆角
	public static Bitmap RoundCorner(Bitmap bitmap) {
		int Width = bitmap.getWidth();
		int Height = bitmap.getHeight();

		Bitmap output = Bitmap.createBitmap(Width, Height, Bitmap.Config.ARGB_8888);
		Canvas canvas = new Canvas(output);

		final int color = 0xff424242;
		final Paint paint = new Paint();
		final Rect rect = new Rect(0, 0, Width, Height);
		final RectF rectF = new RectF(rect);

		paint.setAntiAlias(true);
		canvas.drawARGB(0, 0, 0, 0);
		paint.setColor(color);
		canvas.drawRoundRect(rectF, 20, 20, paint);
		
		//选取圆角矩形的部分
		paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN));
		canvas.drawBitmap(bitmap, rect, rect, paint);

		return output;
	}
其中,圆角矩形的圆角半径是20个像素,最好改成图片宽高的比例值,这样输入不同的图片也会取得比较好的效果。

//选取圆角矩形的部分
		paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN));
就是选取矩形框内的图片。

下面来看一下效果:


还是挺不错的。

作者:qq_32353771 发表于2016/11/17 22:57:02 原文链接
阅读:12 评论:0 查看评论

Ionic开发入门教程_5

$
0
0

创建Session控制器

  今天有点时间,就多翻译一篇,现在才发现自己英语是有多滥,意思都能看懂,但就是不知道该如何表达,看来距离一个大牛还有很远的路要走啊,继续努力~~

  原文链接:http://ccoenraets.github.io/ionic-tutorial/create-angular-controller.html

这里写图片描述


第五章 创建Session控制器

  AngularJS中的控制器,扮演着视图和服务之间的胶水角色。控制器通常在服务中调用一个方法,以获取它存储在一个范围变量中的数据,以便可以通过视图进行显示。
  在这个模块中,你创建了两个控制器:SessionsCtrl 管理会议的列表视图,SessionCtrl 管理会议的详细信息视图。

步骤1:声明starter.services作为一个依赖

  在这个模块中,你将使用在starter.services模块定义的Session服务来创建两个Controller。添加starter.services作为依赖到starter.controller模块。
  
  打开conference/www/js/controllers.js,添加starter.services作为依赖,以确保Session服务对控制器可用:

angular.module('starter.controllers', ['starter.services'])

步骤2:实现SessionList控制器

  在controllers.js中,删除PlayListsCtrl(复数)

  取而代之的是一个名叫SessionsCtrl 控制器,使用Session服务来获取会议数据,并且存储在一个名为sessions的变量中。

 .controller('SessionsCtrl', function($scope, Session) {
    $scope.sessions = Session.query();
})

步骤3:实现SessionDetails控制器

  在controllers.js中,删除PlayListCtrl(单数)。

  取而代之的是一个名叫SessionCtrl 控制器,使用Session服务来获取特定的会议数据,并且存储在一个名为session的变量中。

.controller('SessionCtrl', function($scope, $stateParams, Session) {
    $scope.session = Session.get({sessionId: $stateParams.sessionId});
});

  至此,controller.js的完整代码如下:

angular.module('starter.controllers', ['starter.services'])
.controller('AppCtrl', function($scope, $ionicModal, $timeout) {
  $scope.loginData = {};

  $ionicModal.fromTemplateUrl('templates/login.html', {
    scope: $scope
  }).then(function(modal) {
    $scope.modal = modal;
  });

  $scope.closeLogin = function() {
    $scope.modal.hide();
  };

  $scope.login = function() {
    $scope.modal.show();
  };

  $scope.doLogin = function() {
    console.log('Doing login', $scope.loginData);
    $timeout(function() {
      $scope.closeLogin();
    }, 1000);
  };
})
.controller('SessionsCtrl', function($scope, Session) {
  $scope.sessions = Session.query();
})
.controller('SessionCtrl', function($scope, $stateParams, Session) {
  $scope.session = Session.get({
    sessionId: $stateParams.sessionId
  });
});
作者:suqingheangle 发表于2016/11/18 18:10:45 原文链接
阅读:63 评论:0 查看评论

IOS 中 UIApplication 常用属性简介

$
0
0

什么是 UIApplication?

  1. UIApplication 对象是应用程序的象征;
  2. 每一个应用都有自己的 UIApplication 对象,而且是单例的;
  3. 通过 [UIApplication sharedApplication] 可以获得这个单例对象;
  4. 一个 IOS 程序启动后创建的第一个对象就是 UIApplication 对象;
  5. 利用 UIApplication 对象,能进行一些应用级别的操作。

注意:UIApplication不能手动创建,不能alloc init,一个应用程序只允许 一个。

为什么要弄成单例?

UIApplication 对象是用来设置应用全局信息的,一个应用程序如果有很多 UIApplication 对象,就会导致不知道听谁的。

UIApplication 的作用:做应用级别的操作

1. 设置应用程序图标右上角的红色提醒数字

@property(nonatomic) NSInteger applicationIconBadgeNumber;

// 0. 获取应用程序的象征
UIApplication *app = [UIApplication sharedApplication];

// 1. 设置应用程序图标的提醒数字
app.applicationIconBadgeNumber = 10;

// 创建通知对象
UIUserNotificationSettings *setting = [UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeBadge categories:nil];

// 注册用户通知(IOS 8 之后才需要注册用户通知,之前不需要,主要是为了提升用户体验)
[app registerUserNotificationSettings:setting];

2.设置联网指示器的可见性

@property(nonatomic,getter = isNetworkActivityIndicatorVisible) BOOL networkActivityIndicatorVisible;

// 2. 设置联网指示器的提示
app.networkActivityIndicatorVisible = YES;

3.管理状态栏

从 IOS 7 开始,系统提供了2种管理状态栏的方式:
(1)通过 UIViewController 管理(每一个 UIViewController 都可以拥有自己不同的状态栏);
(2)通过 UIApplication 管理(一个应用程序的状态栏都由它统一管理)。

在 IOS 7 中,默认情况下,状态栏都是由 UIViewController 管理的,UIViewController 实现下列方法就可以轻松管理状态栏的可见性和样式:

状态栏的样式
- (UIStatusBarStyle)preferredStatusBarStyle;

状态栏的可见性
(BOOL)prefersStatusBarHidden;

在 IOS 7 之后,状态栏默认交给控制器管理,不能直接直接赋值为 YES,需要作一些配置。如果想利用 UIApplication 来管理状态栏,首先得修改 Info.plist 的设置,添加一行如下:
这里写图片描述

// 3. 设置状态栏
app.statusBarHidden = YES;

4. 打开一个资源

UIApplication有个功能十分强大的openURL:方法
- (BOOL)openURL:(NSURL)url;*

openURL:方法的部分功能有
打电话
*UIApplication *app = [UIApplication sharedApplication];
[app openURL:[NSURL URLWithString:@”tel://10086”]];

发短信
[app openURL:[NSURL URLWithString:@”sms://10086”]];

发邮件
[app openURL:[NSURL URLWithString:@”mailto://12345@qq.com”]];

打开一个网页资源
[app openURL:[NSURL URLWithString:@”http://www.baidu.com“]];

示例:

// 4. 打开网页(根据协议头判断用什么软件打开)
NSURL *url = [NSURL URLWithString:@"http://www.baidu.com"];
[app openURL:url];

UIApplication打开资源的好处:不用判断用什么软件打开,系统会自动根据协议头判断。

整个代码工程以及属性图示如下:
这里写图片描述

这里写图片描述

作者:huangfei711 发表于2016/11/18 18:22:03 原文链接
阅读:64 评论:0 查看评论

Ionic开发入门教程_6

$
0
0

创建模版

  今天周五,没有太多事情,索性再来一篇~
  原文链接:http://ccoenraets.github.io/ionic-tutorial/create-ionic-template.html
  
这里写图片描述

第六章 创建模版

  在这一章节中,你将来创建两个模版:sessions.html来展示会议列表,session.html来展示一个特定会议的具体细节。

步骤1:创建Sessions模版


1、在conference/www/templates目录中,将palylists.html重命名为sessions.html

2、按照如下代码实现sessions.html模版:

<ion-view view-title="Sessions">
  <ion-content>
    <ion-list>
      <ion-item ng-repeat="session in sessions"
                href="#/app/sessions/{{session.id}}">{{session.title}}</ion-item>
    </ion-list>
  </ion-content>
</ion-view>

注意:这里使用了ng-repeat指令来展示会议列表

步骤2: 创建session模版


1、将playlist.html重命名为session.html

2、按照如下代码实现session.html模版:

<ion-view view-title="Session">
  <ion-content>
    <div class="list card">
      <div class="item">
        <h3>{{session.time}}</h3>
        <h2>{{session.title}}</h2>
        <p>{{session.speaker}}</p>
      </div>
      <div class="item item-body">
        <p>{{session.description}}</p>
      </div>
      <div class="item tabs tabs-secondary tabs-icon-left">
        <a class="tab-item">
          <i class="icon ion-thumbsup"></i>
          Like
        </a>
        <a class="tab-item">
          <i class="icon ion-chatbox"></i>
          Comment
        </a>
        <a class="tab-item">
          <i class="icon ion-share"></i>
          Share
        </a>
      </div>
    </div>
  </ion-content>
</ion-view>
作者:suqingheangle 发表于2016/11/18 18:35:19 原文链接
阅读:54 评论:0 查看评论

Android进阶——基于API24的AsyncTask使用与源码分析

$
0
0

基于API24的AsyncTask使用与源码分析


AsyncTask是什么

AsyncTask是一种轻量级的异步任务类,它可以在线程池中执行后台任务,然后把执行的进度和最终结果传递给主线程并主线程中更新UI,通过AsyncTask可以更加方便执行后台任务以及在主线程中访问UI,但是AsyncTask并不适合进行特别耗时的后台任务,对于特别耗时的任务来说,建议使用线程池

AsyncTask的使用

我们简单的模拟下载文件的案例来分析,我们创建自己的异步类继承AsyncTask

private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {

    @Override
    protected void onPreExecute() {
        //异步任务开启之前回调,在主线程中执行
        super.onPreExecute();
    }

    @Override
    protected Long doInBackground(URL... urls) {
        //执行异步任务,在线程池中执行
        long totalSize = 0;
        int i = 0;
        try {
            while (i < 100) {
                Thread.sleep(50);
                i = i + 5;
                publishProgress(i);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        totalSize = totalSize + i;
        return totalSize;
    }

    @Override
    protected void onProgressUpdate(Integer... progress) {
        //当doInBackground中调用publishProgress时回调,在主线程中执行
        pb_progress.setProgress(progress[0]);
    }

    @Override
    protected void onPostExecute(Long result) {
        //在异步任务执行之后回调,在主线程中执行
        Toast.makeText(MainActivity.this, "下载完成,结果是" + result, Toast.LENGTH_SHORT).show();
    }

    @Override
    protected void onCancelled() {
        //在异步任务被取消时回调
        super.onCancelled();
    }
}

参数分析

通过源码,可以看到AsyncTask是个抽象的泛型类

public abstract class AsyncTask<Params, Progress, Result>
  • Params:表示后台任务执行时的参数类型(对应例子中的URL),该参数会传给AysncTask的doInBackground()方法
  • Progress:表示后台任务的执行进度的参数类型(对应例子中的Integer),该参数会作为onProgressUpdate()方法的参数
  • Result:表示后台任务的返回结果的参数类型(对应例子中的Long),该参数会作为onPostExecute()方法的参数

注意这三个参数都是代表的类型,如果AsyncTask确实不需要传递具体的参数,那么这三个泛型参数可以用Void来代替

方法分析

常用的AsyncTask继承的方法有

  • onPreExecute():异步任务开启之前回调,在主线程中执行
  • doInBackground():执行异步任务,在线程池中执行
  • onProgressUpdate():当doInBackground中调用publishProgress时回调,在主线程中执行
  • onPostExecute():在异步任务执行之后回调,在主线程中执行
  • onCancelled():在异步任务被取消时回调

开启异步

在主Activity中通过点击按钮执行我们创建的异步任务,然后通过execute()方法执行异步任务

bt_down.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        try {
            URL url = new URL("http://blog.csdn.net/");
            new DownloadFilesTask().execute(url);
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
    }
});

效果图

AsyncTask源码分析

我将会按照下面AsyncTask运行的过程来分析源码

主分支

  1. 首先,execute()方法,开启异步任务
  2. 接着,onPreExecute()方法,异步任务开启前
  3. 接着,doInBackground()方法,异步任务正在执行
  4. 最后,onPostExecute()方法,异步任务完成

次分支

  1. onProgressUpdate()方法,异步任务更新UI
  2. onCancelled()方法,异步任务取消

主分支部分

代码开始的地方,是在创建AsyncTask类之后执行的execute()方法

public final AsyncTask<Params, Progress, Result> execute(Params... params) {
    return executeOnExecutor(sDefaultExecutor, params);
}

execute()方法会调用executeOnExecutor()方法

public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
            Params... params) {
    if (mStatus != Status.PENDING) {
        switch (mStatus) {
            case RUNNING:
                throw new IllegalStateException("Cannot execute task:"
                        + " the task is already running.");
            case FINISHED:
                throw new IllegalStateException("Cannot execute task:"
                        + " the task has already been executed "
                        + "(a task can be executed only once)");
        }
    }

    mStatus = Status.RUNNING;

    onPreExecute();

    mWorker.mParams = params;
    exec.execute(mFuture);

    return this;
}

AsyncTask定义了一个mStatus变量,表示异步任务的运行状态,分别是PENDING、RUNNING、FINISHED,当只有PENDING状态时,AsyncTask才会执行,这样也就保证了AsyncTask只会被执行一次

继续往下执行,mStatus会被标记为RUNNING,接着执行,onPreExecute(),将参数赋值给mWorker,然后还有execute(mFuture)

这里的mWorker和mFuture究竟是什么,我们往下追踪,来到AsyncTask的构造函数中,可以找到这两个的初始化

public AsyncTask() {
    mWorker = new WorkerRunnable<Params, Result>() {
        public Result call() throws Exception {
            mTaskInvoked.set(true);

            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
            //noinspection unchecked
            Result result = doInBackground(mParams);
            Binder.flushPendingCommands();
            return postResult(result);
        }
    };

    mFuture = new FutureTask<Result>(mWorker) {
        @Override
        protected void done() {
            try {
                postResultIfNotInvoked(get());
            } catch (InterruptedException e) {
                android.util.Log.w(LOG_TAG, e);
            } catch (ExecutionException e) {
                throw new RuntimeException("An error occurred while executing doInBackground()",
                        e.getCause());
            } catch (CancellationException e) {
                postResultIfNotInvoked(null);
            }
        }
    };
}

1)分析mWorker

mWorker是一个WorkerRunnable对象,跟踪WorkerRunnable

private static abstract class WorkerRunnable<Params, Result> implements Callable<Result> {
    Params[] mParams;
}

实际上,WorkerRunnable是AsyncTask的一个抽象内部类,实现了Callable接口

2)分析mFuture

mFuture是一个FutureTask对象,跟踪FutureTask

public FutureTask(Callable<V> callable) {
    if (callable == null)
        throw new NullPointerException();
    this.callable = callable;
    this.state = NEW;       // ensure visibility of callable
}

实际上,FutureTask是java.util.concurrent包下的一个类,参数是个callable,并且将它赋值给FutureTask类中的callable

3)回过头来看

回到我们AsyncTask初始化mFuture,这里的参数是mWorker也就不奇怪了,因为mWorker就是一个callable,我们在上面赋值给FutureTask类中的callable就是这个mWorker

mFuture = new FutureTask<Result>(mWorker)

而关于mWorker和mFuture的初始化早在我们Activity中初始化好了,因为构造函数是跟AsyncTask类的创建而执行的

new DownloadFilesTask()

知道了mWorker和mFuture是什么后,我们回到原来的executeOnExecutor()方法,在这里将mWorker的参数传过去后,就开始用线程池execute这个mFuture

mWorker.mParams = params;
exec.execute(mFuture);

1)分析exec

exec是通过executeOnExecutor()参数传进来的

public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
            Params... params)

也就是我们execute()方法传过来的

public final AsyncTask<Params, Progress, Result> execute(Params... params) {
    return executeOnExecutor(sDefaultExecutor, params);
}

这里可以看到exec就是这个sDefaultExecutor

2)分析sDefaultExecutor

我们跟踪这个sDefaultExecutor,截取有关它的代码

public static final Executor SERIAL_EXECUTOR = new SerialExecutor();

private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;

private static class SerialExecutor implements Executor {
    final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
    Runnable mActive;

    public synchronized void execute(final Runnable r) {
        mTasks.offer(new Runnable() {
            public void run() {
                try {
                    r.run();
                } finally {
                    scheduleNext();
                }
            }
        });
        if (mActive == null) {
            scheduleNext();
        }
    }

    protected synchronized void scheduleNext() {
        if ((mActive = mTasks.poll()) != null) {
            THREAD_POOL_EXECUTOR.execute(mActive);
        }
    }
}

从SerialExecutor可以发现,exec.execute(mFuture)就是在调用SerialExecutor类的execute(final Runnable r)方法,这里的参数r就是mFuture

继续往下走,SerialExecutor的execute()方法会将r封装成Runnable,并添加到mTasks任务队列中

继续往下走,如果这时候没有正在活动的AsyncTask任务,那么就会调用SerialExecutor的scheduleNext()方法,来执行下一个AsyncTask任务

if (mActive == null) {
    scheduleNext();
}

继续往下走,通过mTasks.poll()取出,将封装在mTask的Runnable交给mActive,最后真正执行的这个mActive的是THREAD_POOL_EXECUTOR,即执行的这个mActive,也就是包装在Runnable里面的mFuture

protected synchronized void scheduleNext() {
    if ((mActive = mTasks.poll()) != null) {
        THREAD_POOL_EXECUTOR.execute(mActive);
    }
}

mFuture被执行了,也就会执行它的run()方法

public void run() {
    try {
        r.run();
    } finally {
        scheduleNext();
    }
}

我们跟踪到mFuture的run()方法中,切换到FutureTask类

public void run() {
    if (state != NEW ||
        !U.compareAndSwapObject(this, RUNNER, null, Thread.currentThread()))
        return;
    try {
        Callable<V> c = callable;
        if (c != null && state == NEW) {
            V result;
            boolean ran;
            try {
                result = c.call();
                ran = true;
            } catch (Throwable ex) {
                result = null;
                ran = false;
                setException(ex);
            }
            if (ran)
                set(result);
        }
    } finally {
        // runner must be non-null until state is settled to
        // prevent concurrent calls to run()
        runner = null;
        // state must be re-read after nulling runner to prevent
        // leaked interrupts
        int s = state;
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
}

这一段代码其实就是将之前在mFutrue创建对象时候传进来的mWorker交给c

Callable<V> c = callable;

然后再调用c的call()方法,也就是mWorker的call()方法

result = c.call();

代码又重新的定位到了mWorker类的call()方法

 mWorker = new WorkerRunnable<Params, Result>() {
    public Result call() throws Exception {
        mTaskInvoked.set(true);

        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
        //noinspection unchecked
        Result result = doInBackground(mParams);
        Binder.flushPendingCommands();
        return postResult(result);
    }
};

可以发现,这里就调用了我们的doInBackground()方法,最后还返回postResult(),我们跟踪这个postResult()方法

private Result postResult(Result result) {
    @SuppressWarnings("unchecked")
    Message message = getHandler().obtainMessage(MESSAGE_POST_RESULT,
            new AsyncTaskResult<Result>(this, result));
    message.sendToTarget();
    return result;
}

观察代码,首先是getHandler(),它是一个单例,返回sHandler

private static Handler getHandler() {
    synchronized (AsyncTask.class) {
        if (sHandler == null) {
            sHandler = new InternalHandler();
        }
        return sHandler;
    }
}

也就是说postResult方法会通过sHandler发送一个MESSAGE_POST_RESULT的消息,这个时候我们追踪到sHandler

private static InternalHandler sHandler;

private static class InternalHandler extends Handler {
    public InternalHandler() {
        super(Looper.getMainLooper());
    }

    @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
    @Override
    public void handleMessage(Message msg) {
        AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
        switch (msg.what) {
            case MESSAGE_POST_RESULT:
                // There is only one result
                result.mTask.finish(result.mData[0]);
                break;
            case MESSAGE_POST_PROGRESS:
                result.mTask.onProgressUpdate(result.mData);
                break;
        }
    }
}

可以发现,sHandler收到MESSAGE_POST_PROGRESS消息后会调用result.mTask.finish(result.mData[0]),那么我们还必须知道result是个什么东西

AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;

result是AsyncTask的内部类,实际上就是个实体类,用来存储变量的

private static class AsyncTaskResult<Data> {
    final AsyncTask mTask;
    final Data[] mData;

    AsyncTaskResult(AsyncTask task, Data... data) {
        mTask = task;
        mData = data;
    }
}

result.mTask也就是AsyncTask,最后调用result.mTask.finish(result.mData[0]),即AsyncTask的finish()方法,我们跟踪到finish()方法

private void finish(Result result) {
    if (isCancelled()) {
        onCancelled(result);
    } else {
        onPostExecute(result);
    }
    mStatus = Status.FINISHED;
}

这里判断AsyncTask是否已经取消,如果不取消就执行我们的onPostExecute(),最后将状态设置为FINISHED,整一个AsyncTask的方法都执行完了,我们只需要继承AsyncTask实现其中的方法就可以按分析的顺序往下执行了

次分支部分

在AsyncTask中的finish()方法,我们可以看到onCancelled()方法跟onPostExecute()一起的,只要isCancelled()的值为true,就执行onCancelled()方法

private void finish(Result result) {
    if (isCancelled()) {
        onCancelled(result);
    } else {
        onPostExecute(result);
    }
    mStatus = Status.FINISHED;
}

我们代码跟踪isCancelled()方法

public final boolean isCancelled() {
    return mCancelled.get();
}

发现是在mCancelled中获取的,那我们就必须知道这个mCancelled是什么,代码跟踪到mCancelled

private final AtomicBoolean mCancelled = new AtomicBoolean();

mCancelled实际上就是个Boolean对象,那我们搜索它是在哪个时候设置的

public final boolean cancel(boolean mayInterruptIfRunning) {
    mCancelled.set(true);
    return mFuture.cancel(mayInterruptIfRunning);
}

可以发现,只要我们在AsyncTask类中调用这个方法即可停止异步任务


而onProgressUpdate()方法,是在sHandler中执行,sHandler收到MESSAGE_POST_PROGRESS消息后,执行,我们搜索MESSAGE_POST_PROGRESS在什么时候发送的

private static class InternalHandler extends Handler {
   public InternalHandler() {
        super(Looper.getMainLooper());
    }

    @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
    @Override
    public void handleMessage(Message msg) {
        AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
        switch (msg.what) {
            case MESSAGE_POST_RESULT:
                // There is only one result
                result.mTask.finish(result.mData[0]);
                break;
            case MESSAGE_POST_PROGRESS:
                result.mTask.onProgressUpdate(result.mData);
                break;
        }
    }
}

可以发现,只要我们在AsyncTask类中调用publishProgress()方法即可执行onProgressUpdate()方法

protected final void publishProgress(Progress... values) {
    if (!isCancelled()) {
        getHandler().obtainMessage(MESSAGE_POST_PROGRESS,
                new AsyncTaskResult<Progress>(this, values)).sendToTarget();
    }
}

结语

AsyncTask使用与源码分析就到这里结束了,如果是初学者,这可能会是比较难的分析,如果是想往中高级进阶的朋友,了解AsyncTask的原理是必须的。随着时代的发展,流行的异步框架RxJava的出现已经可以说是替代了这个AsyncTask,为了跟进时代的发展,我也得抽取时间来学习了,废话就不说了,还是节省点时间来学习吧

作者:qq_30379689 发表于2016/11/18 18:41:14 原文链接
阅读:57 评论:0 查看评论

Ionic开发入门教程_7

$
0
0

实现路由

  写顺手了,继续吧~
  原文链接:http://ccoenraets.github.io/ionic-tutorial/angular-ui-router.html
  
这里写图片描述

第七章 实现路由

  在这一章节中,你将给应用添加两个新的路由(状态): app.sessions来驾照会议列表视图;app.session来加载会议细节视图。
  

步骤1:定义app.sessions路由


  1. conference/www/js中打开app.js文件

  2. 删除app.playlists状态

  3. 使用app.sessions状态来替换,代码如下:

.state('app.sessions', {
  url: "/sessions",
  views: {
      'menuContent': {
          templateUrl: "templates/sessions.html",
          controller: 'SessionsCtrl'
      }
  }
})

步骤2:定义app.session路由


  1. 删除app.single状态

  2. 使用app.session状态替换,定义如下:

.state('app.session', {
    url: "/sessions/:sessionId",
    views: {
        'menuContent': {
          templateUrl: "templates/session.html",
          controller: 'SessionCtrl'
      }
    }
});

步骤3:修改默认路由


  1. 修改默认的回退路由为会议列表(app.js最后一行)
$urlRouterProvider.otherwise('/app/sessions');

步骤4:修改 sideMenu页面


  1. 打开conference/www/templates文件夹中的menu.html页面

  2. 按照如下内容修改Playlists menu item 页面(同时修改label及href)

<ion-item menu-close href="#/app/sessions">
    Sessions
</ion-item>

步骤5:测试应用


  1. 确保你本地服务器上的ionic serve在持续运行

    • 服务仍在运行,只是你关闭了浏览器的页面,这时你可以通过访问:http://localhost:8100 地址来重新加载。
    • 父母没有运行,打开命令行窗口,导航至ionic-tutorial目录下,运行:ionic serve
  2. 在confierence应用中,打开侧边菜单(左上角按钮)选择“Sessions”,选中列表中的一个具体的session来查看会议细节。


  最终效果如下:
这里写图片描述

作者:suqingheangle 发表于2016/11/18 19:10:47 原文链接
阅读:55 评论:0 查看评论
Viewing all 5930 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>