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

设计模式之单例

$
0
0

单例模式就是存在一个实例。这个实例在整个应用中只存在一个。接下来我就演示下几种常见的单例:

1 饿汉模式

饿汉模式就是类一加载,该实例就创建了,

public class SingleSimple {
    /**
     * private代表是不向外部暴露出去,
     */
    private static SingleSimple single=new SingleSimple();
    private SingleSimple(){}//private代表不能new创建该实例。必须为private

    public static SingleSimple newInstance(){

        return single;
    }
}

这就是一个简单的饿汉单例。

然后测试下:

public class SingleSimpleTest {

    public static void main(String[] args) {

        SingleSimple single01=SingleSimple.newInstance();
        SingleSimple single02=SingleSimple.newInstance();

        System.out.println(single01==single02);
    }

}

运行结果:
这里写图片描述

可以看到,运行结果为true,这就说明single01和single02是指向内存中同一个。

2 懒汉模式(线程不安全)

懒汉模式是根据调用者是不是想要个实例,要的话就创建一个,不要的话就不创建。

public class SingleSimple {
    /**
     * private代表是不向外部暴露出去,
     */
    private static  SingleSimple single=null;
    private SingleSimple(){}//private代表不能new创建该实例。

    public static SingleSimple newInstance(){
        if (single==null) {
              single=new SingleSimple();    
        }
        return single;
    }
}

这是一个超级简单的懒汉单例。

测试:

public class SingleSimpleTest {

    public static void main(String[] args) {

        SingleSimple single01=SingleSimple.newInstance();
        SingleSimple single02=SingleSimple.newInstance();

        System.out.println(single01==single02);
    }

}

运行结果:

这里写图片描述

还是为true说明我们写的懒汉单例没问题。

2 懒汉模式(线程安全)

懒汉模式是根据调用者是不是想要个实例,要的话就创建一个,不要的话就不创建。

public class SingleSimple {
    /**
     * private代表是不向外部暴露出去,
     */
    private static  SingleSimple single=null;
    private SingleSimple(){}//private代表不能new创建该实例。

    public static synchronized SingleSimple newInstance(){
        if (single==null) {
              single=new SingleSimple();    
        }
        return single;
    }
}

这是一个超级简单的懒汉单例。只是加了个同步

测试:

public class SingleSimpleTest {

    public static void main(String[] args) {

        SingleSimple single01=SingleSimple.newInstance();
        SingleSimple single02=SingleSimple.newInstance();

        System.out.println(single01==single02);
    }

}

运行结果:

这里写图片描述

还是为true说明我们写的懒汉单例没问题。

4, 饿汉单例变形(静态代码块)。

静态代码块实现,静态代码块也是随着类加载的。只加载一次

public class SingleSimple {
    /**
     * private代表是不向外部暴露出去,
     */
    private static  SingleSimple single=null;
    private SingleSimple(){}//private代表不能new创建该实例。

    static{
        single=new SingleSimple();
    }
    public static SingleSimple newInstance(){

        return single;
    }
}

测试代码:

public class SingleSimpleTest {

    public static   SingleSimple single01=null;
    public  static  SingleSimple single02=null;
    public static void main(String[] args) {

        single01=SingleSimple.newInstance();
        single02=SingleSimple.newInstance();
        System.out.println(single01==single02);
        System.out.println(single02);
        System.out.println(single01);


    }

}

这里写图片描述

也为true ,说明这也是一种单例。

5, 静态内部类

public class SingleSimple {

    private SingleSimple(){}//private代表不能new创建该实例。

    static class InnerClass{
        private static  SingleSimple single=new SingleSimple();
    }
    public static SingleSimple newInstance(){

        return InnerClass.single;
    }
}

测试代码一样。不贴出来了:

运行结果:

这里写图片描述

还是为true,说明也是单例。

6 , 双重判断。

public class SingleSimple {
    /**
     * private代表是不向外部暴露出去,
     * volatile关键字是代表可见性
     */
    private static volatile SingleSimple single=null;
    private SingleSimple(){}//private代表不能new创建该实例。

    public static SingleSimple newInstance(){
        if (single==null) {
            synchronized (SingleSimple.class) {
                if (single==null) {
                    single=new SingleSimple();  
                }
            }

        }
        return single;
    }
}

在第二个判断时,加了锁,线程是安全的。比第三种实现方式更安全。

测试代码一样,不贴出来了。

运行结果:

这里写图片描述

为true就说明单例是成功的。

7 , 枚举实现

枚举是java1.5才出现的。它也可以实现单例

public enum SingleSimple {
    SINGLE; 
}

测试代码:

public class SingleSimpleTest {

    public static void main(String[] args) {

        SingleSimple S1=SingleSimple.SINGLE;
        SingleSimple S2=SingleSimple.SINGLE;
        System.out.println(S1==S2);
        System.out.println(S1);
        System.out.println(S2);
    }

}

运行结果:

这里写图片描述

为true说明单例成功了。如果不明白枚举的可以去看看。枚举其实挺重要的。

下面我就简单演示下怎么用枚举吧 :

首先是用enum声明的,就这样

public enum SingleSimple {
    SINGLE01,SINGLE02,SINGLE03;
    public void methodInEnum(){
        System.out.println("我是来自枚举中的方法。。。");
    }
}

然后就演示下怎么用枚举。

public class SingleSimpleTest {

    public static void main(String[] args) {

        SingleSimple s1=SingleSimple.SINGLE01;
        SingleSimple s2=SingleSimple.SINGLE02;
        SingleSimple s3=SingleSimple.SINGLE03;
        s1.methodInEnum();//调用枚举中的方法
        int position01=s1.ordinal();//返回的是第几个位置
        int position02=s2.ordinal();//返回的是第几个位置
        int position03=s3.ordinal();//返回的是第几个位置
        System.out.println("SINGLE01的位置"+position01);
        System.out.println("SINGLE02的位置"+position02); 
        System.out.println("SINGLE03的位置"+position03); 
        System.out.println(s1.compareTo(s2));//比较的是位置的大小
        SingleSimple all[]= s1.values();//返回的是SINGLE01,SINGLE02,SINGLE03
        /**
         * 全部打印出来
         */
        for (int i = 0; i <all.length; i++) {
            System.out.println(all[i].name());
        }   
    }
}

运行结果:

这里写图片描述

其实枚举也没什么难度,只有几个方法。我差不多都演示了。

总结:

单例模式我写了七种,目的就是一个,就是在整个应用中只存在一个实例。双重判断现在是用的最多的单例。
最后补充一句:其实反射可以创建实例。即使是构造方法被private修饰,也可以创建一个新实例,但是这样就不能构成单例。反射不会的,应该要加强了。哈哈。。。

作者:song_shui_lin 发表于2016/10/3 13:23:23 原文链接
阅读:208 评论:4 查看评论

appWidget小应用----简易音乐播放器

$
0
0

制作一个类似与下图的简易音乐播放器
这里写图片描述

1.创建一个布局

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:paddingTop="4dp"
              android:paddingBottom="4dp"
              android:paddingLeft="8dp"
              android:paddingRight="8dp"
              android:orientation="horizontal"
              android:weightSum="5"
              android:layout_width="match_parent"
              android:layout_height="match_parent">
<LinearLayout
    android:orientation="vertical"
    android:layout_width="0dp"
    android:layout_weight="3"
    android:layout_height="56dp">
    <TextView
        android:textSize="20sp"
        android:textStyle="bold"
        android:id="@+id/musicTitle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
    <TextView
        android:textSize="18sp"
        android:layout_marginTop="3dp"
        android:text="artist"
        android:id="@+id/musicArtist"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
</LinearLayout>
    <ImageView
        android:src="@drawable/ic_media_play"
        android:id="@+id/musicPlay"
        android:layout_weight="1"
        android:layout_width="0dp"
        android:layout_height="56dp"/>
    <ImageView
        android:src="@drawable/ic_media_next"
        android:id="@+id/musicNext"
        android:layout_weight="1"
        android:layout_width="0dp"
        android:layout_height="56dp"/>
</LinearLayout>

布局很简单,两个TextView和两个ImageView。

2.实现AppWidgetProvider

实现一个appWidget一般需要两步,第一步实现实现一个AppWidgetProvider中的onUpdate()方法,有点类似Activity中的onCreate()方法。第二步,在AndroidManifest中声明。

public class MusicWidgetProvider extends AppWidgetProvider{

    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        //开启一个服务
        context.startService(new Intent(context,MusicService.class));
        super.onUpdate(context,appWidgetManager,appWidgetIds);
    }

    @Override
    public void onDisabled(Context context) {
        //关闭服务
        context.stopService(new Intent(context, MusicService.class));
        super.onDisabled(context);
    }
}

在MusicWidgetProvider 中只是简单的开启和关闭一个服务,因为在MusicWidgetProvider 中不能很好的实现音乐的播放,将音乐的播放逻辑放在服务中实现。

3.声明appWidget

<receiver android:name=".MusicWidgetProvider">
            <intent-filter >
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
            </intent-filter>
            <meta-data android:name="android.appwidget.provider"
                       android:resource="@xml/music_provider_info"
                />
 </receiver>

还需一个xml文件

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
                    android:minWidth="250dp"
                    android:minHeight="56dp"
                    android:updatePeriodMillis="86400000"
                    android:initialLayout="@layout/music_provider">

4.在服务中实现音乐播放

在实现音乐播放前,需要创建一个类用于存放音乐信息

public class SongInfo {
    private String title;//歌曲名
    private String artist;//歌手
    private String data;//歌曲所在路径

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getArtist() {
        return artist;
    }

    public void setArtist(String artist) {
        this.artist = artist;
    }

    public String getData() {
        return data;
    }

    public void setData(String data) {
        this.data = data;
    }
}

还需要一个方法去获取手机中的音乐文件

public class MusicManager {
    /**
     * 获取手机内的所有歌曲信息
     * @param context
     * @return
     */
    public static List<SongInfo> getSongList(Context context)
    {
        List<SongInfo> songList=new ArrayList<>();
        ContentResolver contentResolver=context.getContentResolver();
        //从对应的数据库中获取对应列的歌曲信息
        Cursor cursor=contentResolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
                new String[]{
                        MediaStore.Audio.Media.TITLE,
                        MediaStore.Audio.Media.ARTIST,
                        MediaStore.Audio.Media.DATA
                },null,null,MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
        cursor.moveToFirst();
        for (int i = 0; i < cursor.getCount(); i++) {
            SongInfo song=new SongInfo();
            song.setTitle(cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.TITLE)));
            song.setArtist(cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST)));
            song.setData(cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.DATA)));
            songList.add(song);
            cursor.moveToNext();
        }
        cursor.close();
        return songList;
    }
}

存放歌曲信息的数据库中还存放了其他信息,如DURATION(歌曲长度),SIZE(大小)。。。在本例中只需要歌曲名,歌手,路径即可。
准备工作完成,开始实现音乐播放。

public class MusicService extends Service{
    private static final String TAG = "MusicService";

    private static final String MUSIC_PLAY_ACTION="appwidget.action.musicplay";
    private static final String MUSIC_NEXT_ACTION="appwidget.action.musicnext";

    private List<SongInfo> songList;//存放歌曲信息
    private int currentSong=0;//当前播放歌曲的位置
    private MediaPlayer mediaPlayer;


    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }


    @Override
    public void onCreate() {
        super.onCreate();
        songList=MusicManager.getSongList(this);
        //注册广播
        IntentFilter filter=new IntentFilter(MUSIC_PLAY_ACTION);
        filter.addAction(MUSIC_NEXT_ACTION);
        registerReceiver(new MusicReceiver(), filter);
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        //更新appWidget
        updateAppWidget();
        return super.onStartCommand(intent, flags, startId);
    }

    /**
     * 更新appWidget
     */
    public void updateAppWidget()
    {
        AppWidgetManager appWidgetManager=AppWidgetManager.getInstance(this);

        RemoteViews remoteViews=new RemoteViews(this.getPackageName(),R.layout.music_provider);
        //设置歌曲名和歌手
        remoteViews.setTextViewText(R.id.musicTitle,getCurrentSongTitle());
        remoteViews.setTextViewText(R.id.musicArtist, getCurrentSongArtist());

        Intent playIntent=new Intent(MUSIC_PLAY_ACTION);
        if (mediaPlayer!=null)
        {
            //根据音乐是否播放,更改imageView的图片
            remoteViews.setImageViewResource(R.id.musicPlay, mediaPlayer.isPlaying() ?
                    R.drawable.ic_media_pause : R.drawable.ic_media_play);
        }
        //点击开启广播
        PendingIntent playBroad=PendingIntent.getBroadcast(this,0,playIntent,0);
        remoteViews.setOnClickPendingIntent(R.id.musicPlay, playBroad);

        Intent nextIntent=new Intent(MUSIC_NEXT_ACTION);
        PendingIntent nextBroad=PendingIntent.getBroadcast(this,0,nextIntent,0);
        remoteViews.setOnClickPendingIntent(R.id.musicNext, nextBroad);

        ComponentName componentName=new ComponentName(this,MusicWidgetProvider.class);
        appWidgetManager.updateAppWidget(componentName, remoteViews);
    }

    /**
     * 获取当前歌曲名
     * @return
     */
    public String getCurrentSongTitle()
    {
        return songList==null?"暂无歌曲":songList.get(currentSong).getTitle();
    }

    /**
     * 获取当前歌手
     * @return
     */
    public String getCurrentSongArtist()
    {
        return songList==null?"未知歌手":songList.get(currentSong).getArtist();
    }


    public void play()
    {
        try {
            mediaPlayer=new MediaPlayer();
            mediaPlayer.setDataSource(songList.get(currentSong).getData());
            mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
            mediaPlayer.prepareAsync();
            mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
                @Override
                public void onPrepared(MediaPlayer mp) {
                    //准备完成后,播放音乐并更新appwidget
                    mediaPlayer.start();
                    updateAppWidget();
                }
            });
            mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
                @Override
                public void onCompletion(MediaPlayer mp) {
                    //播放结束,播放下一首
                    playNext();
                }
            });
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    /**
     * 播放下一首
     */
    public void playNext()
    {
        if(mediaPlayer!=null)
        {
            Stop();
        }
        //指向下一首,这里需要对是否是最后一首进行判断
        currentSong++;
        play();
    }

    /**
     * 暂停播放,更新appWidget
     */
    public void pause()
    {
        mediaPlayer.pause();
        updateAppWidget();
    }

    /**
     * 播放,更新appWidget
     */
    public void start()
    {
        mediaPlayer.start();
        updateAppWidget();
    }
    /**
     * 音乐停止
     */
    public void Stop()
    {
        if (mediaPlayer!=null)
        {
            mediaPlayer.stop();
            mediaPlayer.release();
            mediaPlayer=null;
        }
    }
    @Override
    public void onDestroy() {
        super.onDestroy();
        Stop();
    }

    /**
     * 接收广播,处理对应请求
     */
    public class MusicReceiver extends BroadcastReceiver
    {
        @Override
        public void onReceive(Context context, Intent intent) {

            if (intent.getAction().equals(MUSIC_PLAY_ACTION))
            {
                //当mediaPlayer为null时,表示当前未播放音乐
                if (mediaPlayer==null)
                {
                    play();
                }else {
                    //当音乐正在播放时,暂停播放
                    if (mediaPlayer.isPlaying())
                    {
                        pause();
                    }else {
                        start();
                    }
                }

            }else if (intent.getAction().equals(MUSIC_NEXT_ACTION))
            {
                playNext();
            }

        }
    }
}

5.运行
这里写图片描述

作者:qq_31244409 发表于2016/10/3 14:26:40 原文链接
阅读:118 评论:0 查看评论

Matrix

$
0
0

初识Matrix

第一次见到Matrix是在图片缩放的时候,Matrix(矩阵),在网上搜索一番,觉得还是直接写下记录一下方便以后复习翻阅

public class MyView extends View {

Bitmap mBitmap;

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

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

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

public Bitmap getBitmap() {
    return mBitmap;
}

public void setBitmap(Bitmap bitmap) {
    mBitmap = bitmap;
}

public boolean isEmpty() {
    return mBitmap == null;
}

@Override
protected void onDraw(Canvas canvas) {

    if (mBitmap != null) {
        Matrix matrix = new Matrix();
        canvas.drawBitmap(mBitmap, matrix, null);
    }
}
}  

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:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.jinxiong.matric.MainActivity">

   <com.example.jinxiong.matric.MyView

       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:id="@+id/myImageView"
       />
</RelativeLayout>  

Activity

public class MainActivity extends AppCompatActivity {

ImageLoader mImageLoader;
MyView mImageView;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    ImageLoaderConfiguration imageLoaderConfiguration = ImageLoaderConfiguration.createDefault(this);
    ImageLoader.getInstance().init(imageLoaderConfiguration);

    mImageLoader = ImageLoader.getInstance();
    mImageView = (MyView) this.findViewById(R.id.myImageView);

    Bitmap bitmap = mImageLoader.loadImageSync("drawable://" + R.drawable.test);
    mImageView.setBitmap(bitmap);

}
}


可以看到这张图片是大于手机屏幕的,需要对图片进行缩小,那么就要使用到Matrix了

 @Override
    protected void onDraw(Canvas canvas) {
    if (mBitmap != null) {
        Matrix matrix = new Matrix();
        matrix.setScale(0.5f, 0.5f);
        canvas.drawBitmap(mBitmap, matrix, null);
    }
}  

那么就可以看到
这里写图片描述
那么Matrix那么厉害,他究竟是什么?

走进Matrix

matrix就是矩阵的意思,
这里写图片描述
正常我们通过

Matrix matrix = new Matrix();  

创建的矩阵式单位矩阵:
这里写图片描述
我们可以通过

matrix.toShortString()  

打印出来

[1.0, 0.0, 0.0][0.0, 1.0, 0.0][0.0, 0.0, 1.0]  

我认为Matrix最重要的就是实现变换,基本的变换有四种
1. 缩放
2. 错切
3. 平移
4. 旋转
参数控制:
这里写图片描述

缩放

在初次创建Matrix 对象的时候,Matrix的缩放两个参数都是1,表示按原本尺寸显示,

matrix.setScale();
matrix.postScale();
matrix.preScale();

有这三个方法,参数都是一样,先说下两个参数的情况
这里写图片描述
setScale 就是在你原有的矩阵中 插入两个值,一个是1行2列 ,一个是2行1列中的值
而preScale 就是矩阵的乘,把原有的矩阵放在乘的左边,目标矩阵放在右边(因为矩阵的左乘和右乘是不一样的),
这里写图片描述
左边的第一行与右边的第一列的值相乘相加后变成结果矩阵的第一行第一列的值,左边第一行与右边第二列的值相乘相加结果变成借股票矩阵的第一行第二列的值,左边的第一行和右边的第三列相乘相加的值变为结果矩阵的第一行第三列的值,以此类推

而postScale 也是这样,只不过把源矩阵和目标矩阵的位置交换,那么结果自然是不一样的了

而当它为四个参数的时候

public void setScale(float sx, float sy, float px, float py) {
    native_setScale(native_instance, sx, sy, px, py);
}

后面两个新参数就是缩放中心的坐标,如果使用两位参数的setScale的话,默认的缩放中心坐标就是(0,0)

@Override
protected void onDraw(Canvas canvas) {

    if (mBitmap != null) {
        Matrix matrix = new Matrix();
        matrix.setScale(0.5f, 0.5f, mBitmap.getWidth() / 2, mBitmap.getHeight() / 2);
        canvas.drawBitmap(mBitmap, matrix, null);
    }

} 

将刚刚那张图片以图片中心为缩放中心缩小一半,
这里写图片描述
对于这三种方法设置图片的缩放,对于set方法,是直接将值付给相应的矩阵的位置的值,对源矩阵来说,改变的只是那两个缩放的值,但是对于pre 和 post的话,因为涉及到矩阵的乘法,还是会对源矩阵一些本来的值会造成影响,可能这并不是你想要的,至于什么时候使用哪一种,具体还是看自己想要怎么样

错切

错切也有三个方法和两种参数格式,跟scale 是一样的,这里就以setSkew为例子

matrix.setSkew(0.5f,0f);

这里写图片描述

matrix.setSkew(0f,0.5f);

这里写图片描述

matrix.setSkew(0.5f,0.5f);

这里写图片描述

matrix.setSkew(0.5f, 0.5f, mBitmap.getWidth() / 2, mBitmap.getHeight() / 2);   

这里写图片描述

旋转

旋转也是对应着三个方法

matrix.setRotate(45);   

这里写图片描述原点为中心旋转45度

matrix.setRotate(180, mBitmap.getWidth() / 2, mBitmap.getHeight() / 2);  

这里写图片描述图片中心为中心,180度

平移

这个也是很简单的方法

matrix.setTranslate(0,200);  

这里写图片描述
这里的translation 和 view.setTranslation 相似,但是移动 的偏移量是不同的位置
这里写图片描述

setPolyToPoly

这里写图片描述
这个特效怎么实现尼 ,答案就是setPolyToPoly

@Override
protected void onDraw(Canvas canvas) {

    if (mBitmap != null) {
        Matrix matrix = new Matrix();

        float[] src = new float[]{
                0,0,
                mBitmap.getWidth(),0,
                mBitmap.getWidth(),mBitmap.getHeight(),
                0,mBitmap.getHeight()
        };

        float[] dst = new float[]{
                0, 0,
                mBitmap.getWidth()/2, 100,
                mBitmap.getWidth()/2, mBitmap.getHeight() - 100,
                0, mBitmap.getHeight()
        };

        matrix.setPolyToPoly(src, 0, dst, 0, 4);

        canvas.drawBitmap(mBitmap, matrix, null);
    }

} 

poly就是多边形的意思,

/**
 * Set the matrix such that the specified src points would map to the
 * specified dst points. The "points" are represented as an array of floats,
 * order [x0, y0, x1, y1, ...], where each "point" is 2 float values.
 *
 * @param src   The array of src [x,y] pairs (points)
 * @param srcIndex Index of the first pair of src values
 * @param dst   The array of dst [x,y] pairs (points)
 * @param dstIndex Index of the first pair of dst values
 * @param pointCount The number of pairs/points to be used. Must be [0..4]
 * @return true if the matrix was set to the specified transformation
 */
public boolean setPolyToPoly(float[] src, int srcIndex,
                             float[] dst, int dstIndex,
                             int pointCount)  

src:代表的是一个坐标数组,这个多边形的点的坐标,srcIndex代表的是从前面的数组哪一个下标开始,而
dst就是你想要变成的多边形的坐标数组,

setRectToRect

这里写图片描述
也是使用一个源矩形和目标矩形进行变换,这是使图片居中(感觉微信的图片是不是这样实现尼?)

@Override
protected void onDraw(Canvas canvas) {

    if (mBitmap != null) {
        Matrix matrix = new Matrix();

        RectF src = new RectF(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
        RectF dst = new RectF(0, 0, getWidth(), getHeight());

        matrix.setRectToRect(src, dst, Matrix.ScaleToFit.CENTER);

        canvas.drawBitmap(mBitmap, matrix, null);
    }

}
} 

最后

基本的使用就是这样 ,更多详细可以看着个大神blog
http://www.gcssloop.com/customview/Matrix_Basic
http://www.gcssloop.com/customview/Matrix_Method

作者:lijinxiong520 发表于2016/10/3 14:44:57 原文链接
阅读:132 评论:0 查看评论

IOS 动画设计(2)——里氏代换原则

$
0
0

里氏代换原则由2008年图灵奖得主、美国第一位计算机科学女博士Barbara Liskov教授和卡内基·梅隆大学Jeannette Wing教授于1994年提出。传统的官方定义比较拗口且难以理解,这里,给出一个较为通俗易懂的定义:
所有引用基类(父类)的地方必须能透明地使用其子类的对象。

只要父类能出现的地方子类就可以出现,而且替换为子类还不产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但是,反过来就不行了,有子类出现的地方,父类未必就能适应。

下面通过具体程序实例进行进一步的解释。
父类作函数声明,但并不实现具体函数,以虚函数的形式呈现,即什么也不做,空实现。
这里写图片描述

子类一继承自父类SourceView,并具体实现 show 方法
这里写图片描述

子类二同样继承自父类SourceView,并具体实现 show 方法
这里写图片描述

最后在ViewController中引入,并实例化,最终运行结果如下:
这里写图片描述

由此,便用一个非常简单的实例演示了里氏代换原则。

里氏代换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。

在使用里氏代换原则时需要注意如下几个问题:

  1. 子类必须实现父类所有非私有的属性和方法 或 子类的所有非私有属性和方法必须在父类中声明。即,子类可以有自己的“个性”,这也就是说,里氏代换原则可以正着用,不能反着用(在项目中,采用里氏替换原则时,尽量避免子类的“个性”,一旦子类有“个性”,这个子类和父类之间的关系就很难调和了)。根据里氏代换原则,为了保证系统的扩展性,在程序中通常使用父类来进行定义,如果一个方法只存在子类中,在父类中不提供相应的声明,则无法在以父类定义的对象中使用该方法。

  2. 尽量把父类设计为抽象类或者接口,让子类继承父类或实现父接口,并实现在父类中声明的方法,运行时,子类实例替换父类实例,我们可以很方便地扩展系统的功能,同时无须修改原有子类的代码,增加新的功能可以通过增加一个新的子类来实现。

作者:huangfei711 发表于2016/10/3 15:16:01 原文链接
阅读:143 评论:0 查看评论

ViewPager和Fragment结合使用实现新闻类app框架(二)

$
0
0

ViewPager和Fragment结合使用实现新闻类app框架(二)


注:本文是在上一篇博客的基础上继续完善之前的ViewPager和Fragment结合使用实现新闻类app框架(一)


首先来看一下今天实现的效果:



package zhangtao.wind.viewpager;

import android.app.Activity;
import android.app.Fragment;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.graphics.Color;
import android.os.Bundle;
import android.support.annotation.MainThread;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.View;
import android.view.Menu;
import android.view.MenuItem;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.TextView;
import android.widget.Toast;

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

import Listener.MyOnPageChangedListener;
import MyView.MyFragment;
import MyView.MyLinearLayout;

public class MainActivity extends FragmentActivity {

    private ViewPager viewPager;
    private MyLinearLayout mly;
    private MyOnPageChangedListener onPageChangedListener;


    //自定义ViewPager的标题;
    private ArrayList <String>list = new ArrayList<String>();
    private String[]a =new String[]{"第一页","第二页","第三页","第四页","第五页","第六页","第七页","第八页","第九页","第十页"};
    private List<MyFragment> fragmentList = new ArrayList<MyFragment>();

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

    private void init() {
        viewPager = (ViewPager) findViewById(R.id.viewPager);
        mly = (MyLinearLayout) findViewById(R.id.myLinearLayout);
        onPageChangedListener=new MyOnPageChangedListener(mly);
        initList();
        getFragmentList();
        getTabTitleList();
        viewPager.setAdapter(adapter);
        viewPager.setOnPageChangeListener(onPageChangedListener);
//为自定义View绑定监听事件
        setonLinerTitleClickListener(mly);
    }

    private void initList(){
        for(int i=0;i<a.length;i++){
            list.add(a[i]);
        }
    }
    private void getTabTitleList() {
        mly.createTextView( list);
    }

    private void getFragmentList() {
        for (int i = 0; i < list.size(); i++) {
            fragmentList.add(MyFragment.getnewInstance((String) list.get(i)));
        }
    }

    //ViewPager的适配器
    private FragmentPagerAdapter adapter = new FragmentPagerAdapter(getSupportFragmentManager()) {
        @Override
        public android.support.v4.app.Fragment getItem(int position) {

            return fragmentList.get(position);
        }

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

    private int currentPosition;
//为自定义中的TextView绑定监听事件
    private void setonLinerTitleClickListener(final MyLinearLayout ly){
            for( int i=0;i<ly.getChildCount();i++){
                currentPosition=i;
              final TextView tv= (TextView) ly.getChildAt(currentPosition);
                tv.setOnClickListener(new View.OnClickListener() {

                    @Override
                    public void onClick(View view) {
                        Log.d("onclick", currentPosition + "");
                 
                        viewPager.setCurrentItem(ly.a.get(tv));
                    }
                });

            }

    }

}

//该类为我们的标题栏的自定义View
public class MyLinearLayout extends LinearLayout {

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

    private void init(Context context, AttributeSet attrs) {
        //自定义属性:
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.MyLinearLayout);
        title_visible_count = array.getInteger(R.styleable.MyLinearLayout_linear_my_view_count, 0);
        //获取屏幕的宽和高
        screenInfo = new GetScreenInfo(context);
        getHeight = screenInfo.getScreenHeight();
        getWidth = screenInfo.getScreenWidth();
        initPaintAndLine();
    }

    private GetScreenInfo screenInfo;
    private float layoutWidth, layoutHeight;
    public static int title_visible_count;

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
      /*  重写onMeasure()方法是为了能适配不同大小的机型,如果我们不重写该方法,在4.0英寸的手机上和5.5英
      寸的手机上都使用同一个height,那给用户的感觉肯定是不舒服的,所以我们能通过该方法实现一点简单的适配
      功能,当然如果你刚开始学习自定义View,那你可以不用重写该方法,直接在xml中的height值写成固定值就可以了。
     */
        Log.d("zt", "onmeasure");
        int getWidthpx, widthMode, getHeightpx, heightMode;
        getWidthpx = MeasureSpec.getSize(widthMeasureSpec);
        widthMode = MeasureSpec.getMode(widthMeasureSpec);
        getHeightpx = MeasureSpec.getSize(heightMeasureSpec);
        heightMode = MeasureSpec.getMode(heightMeasureSpec);

       /* 该出Mode一共有3中,EXACTLY,AT_MOST,UNSPECIFIED;其中当我们在xml中设置了width或者height为match_parent或者某个
        固定值时,那么这个Mode就是EXACTLY,如果设置成wrap_content的话,那么这个Mode就是AT_MOST, UNSPECIFIED是什么我也不太清楚,
        如果你用过这个值,可以告诉留言告诉我啊!哈哈!*/
        if (widthMode == MeasureSpec.EXACTLY) {
            //当xml中width被设置成了固定的值或者设置成match_parent那么我们就直接用这个值
            layoutWidth = getWidthpx;
        } else {
            //否则我们就使用getScreenSize()方法中获得的屏幕的宽度。
            layoutWidth = getWidth;
        }
        if (heightMode == MeasureSpec.EXACTLY) {
            layoutHeight = getHeightpx;
        } else {
            //如果xml中没有定义高度,那么我们就让标题栏占屏幕的1/14,这样大概差不多比较合适,当然,你可以自己改变这个值。
            layoutHeight = getHeight / 15;
        }
        setMeasuredDimension((int) layoutWidth, (int) layoutHeight);
    }


    private float getHeight, getWidth;
<span style="white-space:pre">	</span>//建立一个集合,用来为每个TextView指定一个int值,用来在点击事件中调用,获得当前的position;
    public Map<TextView,Integer> a=new HashMap<TextView,Integer>();
    private int textViewPosition;
    private TextView getTextView(String name) {
        //因为在新闻的标题栏里,有很多很多标题,可能多达20个,我们不可能在布局文件里一直添加吧,所以就直接动态生成TextView.
        TextView tv = new TextView(getContext());
        Log.d("zt", "getTextView");
        a.put(tv,textViewPosition++);
        LinearLayout.LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
        //根据自定义属性中的控制显示title数量的值,来动态改变width的值,使得显示的数量和定义的相符合。
        lp.width = (int) (getWidth / title_visible_count);
        lp.gravity = Gravity.CENTER;
        tv.setText(name + "");
        tv.setGravity(Gravity.CENTER);
        tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 15);
        tv.setTextColor(Color.GRAY);
        tv.setLayoutParams(lp);
        return tv;
    }

    private int count;

    //该自定义View中暴露出来的方法,让外界调用
    public void createTextView(ArrayList list) {
        if (list != null && list.size() > 0) {
            this.removeAllViews();
            count = list.size();
            for (int i = 0; i < list.size(); i++) {
                MyLinearLayout.this.addView(getTextView(list.get(i) + ""));
            }
        }
    }

    //10.2
    private Paint paint;
    private float mLineWidth;
    private float lineEndX;
    private float lineStartX;
    private int currentPositon;
    private void initPaintAndLine() {
        paint = new Paint();
        paint.setStrokeWidth(4);
        paint.setColor(Color.RED);
        paint.setStyle(Paint.Style.STROKE);
        paint.setAntiAlias(true);
        //  mLineWidth=  (getWidth/title_visible_count*2/3);
        mLineWidth = getWidth / (title_visible_count * 2);
        lineEndX = (int) (mLineWidth + mLineWidth);
        lineStartX = (getWidth / title_visible_count - mLineWidth) / 2.0f;
    }

    private MyLinearChangedListener myLinearChangeListener;

    public void setOnMyLiChangListener(MyLinearChangedListener my) {
        myLinearChangeListener = my;
    }

  

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawLine(lineStartX, getMeasuredHeight(), lineEndX, getMeasuredHeight(), paint);
        Log.d("zt", "onDraw");
    }

    private float titleWidth;
    //暴露该方法让外界调用,来动态改变标题底部的红色的线,以及标题的颜色
    public void lineScroll(int position, float offset) {

        titleWidth = getWidth / title_visible_count;
   <span style="white-space:pre">	</span>//通过这个判断,实现底端红线的动画效果。
        if ((offset * getWidth) < (getWidth / 2)) {
            lineEndX = mLineWidth + titleWidth * offset * 2.0f + titleWidth * position + titleWidth * 1.0f / 4.0f;
        } else {
            lineStartX = lineEndX - (1.0f - offset) * 2.0f * titleWidth - mLineWidth;
        }
        Log.d("hehe", lineEndX - lineStartX + "");
        setTitleBackGround(position);
        invalidate();
        linearScroll(position, offset);
    }
    //实现自定义View的滚动,通过这个判断控制在什么位置开始滚动,什么位置停止滚动
    private void linearScroll(int position,float offset){
        currentPositon=position;
        float titleWidth=getWidth/title_visible_count;
        if(position>=(title_visible_count/2)&&offset>0&&getChildCount()>title_visible_count&&position<=5){
                this.scrollTo((int) ((position-(title_visible_count-4))*titleWidth+(titleWidth*offset)),0);
        }
    }
//设置标题的字体颜色。
    private void setTitleBackGround(int position) {
      for(int i=0;i<getChildCount();i++){
          TextView tv= (TextView) getChildAt(i);
          tv.setTextColor(Color.GRAY);
      }
        TextView view= (TextView) getChildAt(position);
        view.setTextColor(Color.RED);
    }


}

怎么样?在昨天简陋的基础上,今天完善了不少,但是如果你真正按这样写,然后进行实际操作,你会发现有些显示与预期的有很大的差别,如以下:

1:在滑动界面时,底端的红线可能出现长短变化,在点击标题实现跳转时,更加明显,容易出现长短的误差更加明显,但是目前我还不知道怎么解决,等我知道了我会继续更新,

PS:如同上面我的描述,如果你知道原因,希望能教教我,告诉我是什么原因了啊大笑,共同进步嘛!


下面我会继续更新该博客哦!请等待!

作者:zhangtao19931104 发表于2016/10/3 16:13:50 原文链接
阅读:116 评论:0 查看评论

无需权限将文件保存到sdcard或应用缓存文件中

$
0
0
Context的方法
getCacheDir
getFilesDir
getExternalCacheDir

getExternalFilesDir


特点1:无需权限将assets中的图片文件保存到sdcard或内存中(当然资源是从网络下载需添加网络权限)
特点2:随着应用卸载缓存数据也一并删除
如果不想应用卸载也将相关资源卸载请保存到除此之外的其他目录

目录结构



activity_main.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:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="cn.weimei.tiankong.com.fileproject.MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        android:id="@+id/textView" />

    <ImageView
        android:id="@+id/iv"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:layout_below="@+id/textView" />

    <WebView
        android:id="@+id/web"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:layout_below="@+id/iv" />
</RelativeLayout>


MainActivity.java

package cn.weimei.tiankong.com.fileproject;

import android.content.Context;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Environment;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.webkit.WebView;
import android.widget.ImageView;
import android.widget.Toast;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * 调用方法=打印的路径
 * getCacheDir=/data/user/0/cn.weimei.tiankong.com.fileproject/cache
 * getFilesDir=/data/user/0/cn.weimei.tiankong.com.fileproject/files
 * getExternalCacheDir=/storage/emulated/0/Android/data/cn.weimei.tiankong.com.fileproject/cache
 * getExternalFilesDir=/storage/emulated/0/Android/data/cn.weimei.tiankong.com.fileproject/files
 * 这几个方法存储无需权限
 */
public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";
    private ImageView iv;
    private InputStream in;
    private WebView web;

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

        printdifrentPath();
        initView();
        //setImageViewByInputStrim();
        //loadImageByWebView();
        //saveAndLoadImgCacheDir();
        //saveAndLoadImgExternalCacheDir();
        //saveAndLoadFilesDir();
        //saveAndLoadExtranalFileDir();
        saveSdcardOrDataDir();
    }

    /**
     * 初始化视图
     */
    private void initView() {
        iv = (ImageView) findViewById(R.id.iv);
        web = (WebView) findViewById(R.id.web);
    }

    /**
     * 将assets目录的图片加载到imageView中
     */
    private void setImageViewByInputStrim() {
        try {
            in = getAssets().open("12345.png");
        } catch (IOException e) {
            e.printStackTrace();
        }
        iv.setImageBitmap(BitmapFactory.decodeStream(in));
    }

    /**
     * 将assets目录的图片加载到WebView中
     */
    private void loadImageByWebView() {
        web.loadUrl("file:///android_asset/12345.png");
    }


    /**
     * 调用方法=打印的路径
     * getCacheDir=/data/user/0/cn.weimei.tiankong.com.fileproject/cache
     * getFilesDir=/data/user/0/cn.weimei.tiankong.com.fileproject/files
     * getExternalCacheDir=/storage/emulated/0/Android/data/cn.weimei.tiankong.com.fileproject/cache
     * getExternalFilesDir=/storage/emulated/0/Android/data/cn.weimei.tiankong.com.fileproject/files
     * 这几个方法存储无需权限
     */
    public void printdifrentPath() {
        Log.d(TAG, "getCacheDir=" + getCacheDir());
        Log.d(TAG, "getFilesDir=" + getFilesDir());
        Log.d(TAG, "getExternalCacheDir=" + getExternalCacheDir());
        Log.d(TAG, "getExternalFilesDir=" + getExternalFilesDir(null));
    }

    /**
     * 将assets目录的图片保存到/data/user/0/cn.weimei.tiankong.com.fileproject/cache目录下并显示在imageView中
     */
    public void saveAndLoadCacheDir() {
        try {
            InputStream in = getAssets().open("12345.png");
            File filepath = getCacheDir();//getCacheDir
            File fileOut = new File(filepath.getPath(), "savecachedir.png");
            FileOutputStream os = new FileOutputStream(fileOut);
            byte[] buffer = new byte[1024];
            int count = 0;
            while ((count = in.read(buffer)) > 0) {
                os.write(buffer, 0, count);
            }
            //加载文件保存在cachedir的图片
            iv.post(new Runnable() {
                @Override
                public void run() {
                    FileInputStream fileIs = null;
                    try {//getCacheDir
                        File loadFile = new File(getCacheDir().getPath() + "/savecachedir.png");
                        Log.d(TAG, "run: getCacheDir--path=" + loadFile.getPath());
                        fileIs = new FileInputStream(loadFile);
                        iv.setImageBitmap(BitmapFactory.decodeStream(fileIs));
                    } catch (FileNotFoundException e) {
                        e.printStackTrace();
                    }
                }
            });
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    /**
     * 将assets目录的图片保存到/storage/emulated/0/Android/data/cn.weimei.tiankong.com.fileproject/cache目录下并显示在imageView中
     */
    public void saveAndLoadExternalCacheDir() {
        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            try {
                InputStream in = getAssets().open("12345.png");
                File filepath = getExternalCacheDir();//getExternalCacheDir
                File fileOut = new File(filepath.getPath(), "saveextranalcachedir.png");
                FileOutputStream os = new FileOutputStream(fileOut);
                byte[] buffer = new byte[1024];
                int count = 0;
                while ((count = in.read(buffer)) > 0) {
                    os.write(buffer, 0, count);
                }
                //加载文件保存在cachedir的图片
                iv.post(new Runnable() {
                    @Override
                    public void run() {
                        FileInputStream fileIs = null;
                        try {//getExternalCacheDir
                            //这样也可以 File loadFile=new File(getExternalCacheDir().getPath()+"/saveextranalcachedir.png");
                            File loadFile = new File(getExternalCacheDir().getPath(), "saveextranalcachedir.png");
                            Log.d(TAG, "run: getExternalCacheDir--path=" + loadFile.getPath());
                            fileIs = new FileInputStream(loadFile);
                            iv.setImageBitmap(BitmapFactory.decodeStream(fileIs));
                        } catch (FileNotFoundException e) {
                            e.printStackTrace();
                        }
                    }
                });
            } catch (IOException e) {
                e.printStackTrace();
            }

        } else {
            Toast.makeText(this, "sdcard不存在", Toast.LENGTH_SHORT).show();
        }
    }
    /**
     * 将assets目录的图片保存到/data/user/0/cn.weimei.tiankong.com.fileproject/files目录下并显示在imageView中
     */
    public void saveAndLoadFilesDir() {
        try {
            InputStream is = getAssets().open("12345.png");
            File filepath = getFilesDir();//getFilesDir
            File fileOut = new File(filepath.getPath(), "saveandloadfiledir.png");
            FileOutputStream os = new FileOutputStream(fileOut);
            byte[] buffer = new byte[1024];
            int count = 0;
            while ((count = (is.read(buffer))) > 0) {
                os.write(buffer, 0, count);
            }
            //加载文件保存在filedir的图片
            iv.post(new Runnable() {
                @Override
                public void run() {
                    FileInputStream fileIs = null;
                    try {//getFilesDir
                        //这样也可以 File loadFile=new File(getFilesDir().getPath()+"/saveandloadfiledir.png");
                        File loadFile = new File(getFilesDir().getPath() + "/saveandloadfiledir.png");
                        Log.d(TAG, "run: getExternalCacheDir--path=" + loadFile.getPath());
                        fileIs = new FileInputStream(loadFile);
                        iv.setImageBitmap(BitmapFactory.decodeStream(fileIs));
                    } catch (FileNotFoundException e) {
                        e.printStackTrace();
                    }
                }
            });
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    /**
     * 将assets目录的图片保存到/storage/emulated/0/Android/data/cn.weimei.tiankong.com.fileproject/files目录下并显示在imageView中
     */
    public void saveAndLoadExtranalFileDir() {
        try {
            InputStream is = getAssets().open("12345.png");
            if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
                File outFile = new File(getExternalFilesDir(null).getPath(), "12345.png");
                FileOutputStream os = new FileOutputStream(outFile);
                int count = 0;
                byte[] buffer = new byte[1024];
                while ((count = (is.read(buffer))) > 0) {//注意buffer不要忘记了
                    os.write(buffer, 0, count);
                }
                iv.post(new Runnable() {
                    @Override
                    public void run() {
                        File loadFile = new File(getExternalFilesDir(null), "12345.png");
                        try {
                            Log.d(TAG, "run: getExternalFilesDir--path=" + loadFile.getPath());
                            FileInputStream is = new FileInputStream(loadFile);
                            iv.setImageBitmap(BitmapFactory.decodeStream(is));
                        } catch (FileNotFoundException e) {
                            e.printStackTrace();
                        }
                    }
                });
            } else {
                Toast.makeText(this, "sdcard不存在", Toast.LENGTH_SHORT).show();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 如果不存在sdcard就保存到data目录下
     */
    public void saveSdcardOrDataDir() {
        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
            saveAndLoadExternalCacheDir(); //或者saveAndLoadExtranalFileDir();
        } else {
            saveAndLoadCacheDir();  //或者saveAndLoadFilesDir();
        }
    }
}


AndroidMenifest.xml

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

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

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

</manifest>


源码下载:http://download.csdn.net/detail/u013042707/9645244



作者:u013042707 发表于2016/10/3 17:16:09 原文链接
阅读:89 评论:0 查看评论

Android中子线程真的不能更新UI吗?

$
0
0

Android的UI访问是没有加锁的,这样在多个线程访问UI是不安全的。所以Android中规定只能在UI线程中访问UI。

但是有没有极端的情况?使得我们在子线程中访问UI也可以使程序跑起来呢?接下来我们用一个例子去证实一下。

新建一个工程,activity_main.xml布局如下所示:

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

    <TextView
        android:id="@+id/main_tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="18sp"
        android:layout_centerInParent="true"
        />

</RelativeLayout>

很简单,只是添加了一个居中的TextView

MainActivity代码如下所示:

public class MainActivity extends AppCompatActivity {

    private TextView main_tv;

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

        main_tv = (TextView) findViewById(R.id.main_tv);

        new Thread(new Runnable() {

            @Override
            public void run() {
                main_tv.setText("子线程中访问");
            }
        }).start();

    }

}

也是很简单的几行,在onCreate方法中创建了一个子线程,并进行UI访问操作。

点击运行。你会发现即使在子线程中访问UI,程序一样能跑起来。结果如下所示:
这里写图片描述

咦,那为嘛以前在子线程中更新UI会报错呢?难道真的可以在子线程中访问UI?

先不急,这是一个极端的情况,修改MainActivity如下:

public class MainActivity extends AppCompatActivity {

    private TextView main_tv;

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

        main_tv = (TextView) findViewById(R.id.main_tv);

        new Thread(new Runnable() {

            @Override
            public void run() {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                main_tv.setText("子线程中访问");
            }
        }).start();

    }

}

让子线程睡眠200毫秒,醒来后再进行UI访问。

结果你会发现,程序崩了。这才是正常的现象嘛。抛出了如下很熟悉的异常:

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6581)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:924)

……

作为一名开发者,我们应该认真阅读一下这些异常信息,是可以根据这些异常信息来找到为什么一开始的那种情况可以访问UI的。那我们分析一下异常信息:

首先,从以下异常信息可以知道

at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6581)

这个异常是从android.view.ViewRootImpl的checkThread方法抛出的。

这里顺便铺垫一个知识点:ViewRootImpl是ViewRoot的实现类。

那现在跟进ViewRootImpl的checkThread方法瞧瞧,源码如下:

void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}

只有那么几行代码而已的,而mThread是主线程,在应用程序启动的时候,就已经被初始化了。

由此我们可以得出结论:
在访问UI的时候,ViewRoot会去检查当前是哪个线程访问的UI,如果不是主线程,那就会抛出如下异常:

Only the original thread that created a view hierarchy can touch its views

这好像并不能解释什么?继续看到异常信息

at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:924)

那现在就看看requestLayout方法,

@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

这里也是调用了checkThread()方法来检查当前线程,咦?除了检查线程好像没有什么信息。那再点进scheduleTraversals()方法看看

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

注意到postCallback方法的的第二个参数传入了很像是一个后台任务。那再点进去

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}

找到了,那么继续跟进doTraversal()方法。

void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

        if (mProfile) {
            Debug.startMethodTracing("ViewAncestor");
        }

        performTraversals();

        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false;
        }
    }
}

可以看到里面调用了一个performTraversals()方法,View的绘制过程就是从这个performTraversals方法开始的。PerformTraversals方法的代码有点长就不贴出来了,如果继续跟进去就是学习View的绘制了。而我们现在知道了,每一次访问了UI,Android都会重新绘制View。这个是很好理解的。

分析到了这里,其实异常信息对我们帮助也不大了,它只告诉了我们子线程中访问UI在哪里抛出异常。
而我们会思考:当访问UI时,ViewRoot会调用checkThread方法去检查当前访问UI的线程是哪个,如果不是UI线程则会抛出异常,这是没问题的。但是为什么一开始在MainActivity的onCreate方法中创建一个子线程访问UI,程序还是正常能跑起来呢??
唯一的解释就是执行onCreate方法的那个时候ViewRootImpl还没创建,无法去检查当前线程。

那么就可以这样深入进去。寻找ViewRootImpl是在哪里,是什么时候创建的。好,继续前进

在ActivityThread中,我们找到handleResumeActivity方法,如下:

final void handleResumeActivity(IBinder token,
        boolean clearHide, boolean isForward, boolean reallyResume) {
    // If we are getting ready to gc after going to the background, well
    // we are back active so skip it.
    unscheduleGcIdler();
    mSomeActivitiesChanged = true;

    // TODO Push resumeArgs into the activity for consideration
    ActivityClientRecord r = performResumeActivity(token, clearHide);

    if (r != null) {
        final Activity a = r.activity;

        //代码省略

            r.activity.mVisibleFromServer = true;
            mNumVisibleActivities++;
            if (r.activity.mVisibleFromClient) {
                r.activity.makeVisible();
            }
        }

      //代码省略    
}

可以看到内部调用了performResumeActivity方法,这个方法看名字肯定是回调onResume方法的入口的,那么我们还是跟进去瞧瞧。

public final ActivityClientRecord performResumeActivity(IBinder token,
        boolean clearHide) {
    ActivityClientRecord r = mActivities.get(token);
    if (localLOGV) Slog.v(TAG, "Performing resume of " + r
            + " finished=" + r.activity.mFinished);
    if (r != null && !r.activity.mFinished) {
    //代码省略
            r.activity.performResume();

    //代码省略

    return r;
}

可以看到r.activity.performResume()这行代码,跟进 performResume方法,如下:

final void performResume() {
    performRestart();

    mFragments.execPendingActions();

    mLastNonConfigurationInstances = null;

    mCalled = false;
    // mResumed is set by the instrumentation
    mInstrumentation.callActivityOnResume(this);

    //代码省略

}

Instrumentation调用了callActivityOnResume方法,callActivityOnResume源码如下:

public void callActivityOnResume(Activity activity) {
    activity.mResumed = true;
    activity.onResume();

    if (mActivityMonitors != null) {
        synchronized (mSync) {
            final int N = mActivityMonitors.size();
            for (int i=0; i<N; i++) {
                final ActivityMonitor am = mActivityMonitors.get(i);
                am.match(activity, activity, activity.getIntent());
            }
        }
    }
}

找到了,activity.onResume()。这也证实了,performResumeActivity方法确实是回调onResume方法的入口。

那么现在我们看回来handleResumeActivity方法,执行完performResumeActivity方法回调了onResume方法后,
会来到这一块代码:

r.activity.mVisibleFromServer = true;
mNumVisibleActivities++;
if (r.activity.mVisibleFromClient) {
    r.activity.makeVisible();
}

activity调用了makeVisible方法,这应该是让什么显示的吧,跟进去探探。

void makeVisible() {
    if (!mWindowAdded) {
        ViewManager wm = getWindowManager();
        wm.addView(mDecor, getWindow().getAttributes());
        mWindowAdded = true;
    }
    mDecor.setVisibility(View.VISIBLE);
}

往WindowManager中添加DecorView,那现在应该关注的就是WindowManager的addView方法了。而WindowManager是一个接口来的,我们应该找到WindowManager的实现类才行,而WindowManager的实现类是WindowManagerImpl。这个和ViewRoot是一样,就是名字多了个impl。

找到了WindowManagerImpl的addView方法,如下:

@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    mGlobal.addView(view, params, mDisplay, mParentWindow);
}

里面调用了WindowManagerGlobal的addView方法,那现在就锁定
WindowManagerGlobal的addView方法:

public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {

    //代码省略  


    ViewRootImpl root;
    View panelParentView = null;

    //代码省略

        root = new ViewRootImpl(view.getContext(), display);

        view.setLayoutParams(wparams);

        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);
    }

    // do this last because it fires off messages to start doing things
    try {
        root.setView(view, wparams, panelParentView);
    } catch (RuntimeException e) {
        // BadTokenException or InvalidDisplayException, clean up.
        synchronized (mLock) {
            final int index = findViewLocked(view, false);
            if (index >= 0) {
                removeViewLocked(index, true);
            }
        }
        throw e;
    }
}

终于击破,ViewRootImpl是在WindowManagerGlobal的addView方法中创建的。

回顾前面的分析,总结一下:
ViewRootImpl的创建在onResume方法回调之后,而我们一开篇是在onCreate方法中创建了子线程并访问UI,在那个时刻,ViewRootImpl是没有创建的,无法检测当前线程是否是UI线程,所以程序没有崩溃一样能跑起来,而之后修改了程序,让线程休眠了200毫秒后,程序就崩了。很明显200毫秒后ViewRootImpl已经创建了,可以执行checkThread方法检查当前线程。

这篇博客的分析如题目一样,Android中子线程真的不能更新UI吗?在onCreate方法中创建的子线程访问UI是一种极端的情况,这个不仔细分析源码是不知道的。我是最近看了一个面试题,才发现这个。

从中我也学习到了从异常信息中跟进源码寻找答案,你呢?

作者:xyh269 发表于2016/10/3 17:58:44 原文链接
阅读:151 评论:0 查看评论

Android动画之属性动画(上)

$
0
0

前言

       在前面的文章中我们讲述了Android动画之视图动画学习了怎么对一个view实现动画,可以实现动画包括平移,旋转,缩放,渐变和帧动画,这一篇我们来学习一个新的动画实现方式。

属性动画

为什么需要属性动画?

       Android在3.0引入了属性动画,既然之前已经有属性动画了,为什么还要引入属性动画呐?视图动画不好用吗?还是视图动画不能够实现功能?随着系统的迭代更新,和设计的发展,以前做app都是比较简单的,但是现在的app越来越复杂,功能越来越多,动画效果也更加的复杂,更阿基要求实时性和界面的绚丽,因此老的系统很多实现都已经改变,对新的功能也力不从心,所以才引入属性动画来改善动画的效果, 主要有如下原因:

1,视图动画是非交互动画,仅仅只是改变了显示位置
2,视图动画种类有限,复杂的效果力不从心
3,视图动画调用onDraw来重绘界面,耗费资源
4,视图动画只能针对view实现动画,对非view不能实现

       我们分别解释一下上面的四点:

视图动画是非交互动画,仅仅只是改变了显示位置

我们在上一章讲述了视图动画是非交互动画,这个虽然不是一个很大的缺陷,但是在真的需要交互的时候就力不从心了,要实现额外的代码才能实现其效果,

视图动画种类有限,复杂的效果力不从心

其次在现阶段很多app都有非常复杂的效果,仅仅依靠视图动画的组合往往很难实现,就算实现了效果,代码也非常的多,可能难以维护。

视图动画调用onDraw来重绘界面,耗费资源

onDraw来返回重绘界面,这样就导致耗费资源和性能,流畅性是一个app非常重要的体验,虽然单一一个动画不能特别明显的拖慢性能,但是各个地方差值综合就会导致体验很差

视图动画只能针对view实现动画,对非view不能实现

视图动画的种类仅仅只有四种,并且只能针对view实现,如果只是针对一个view的背景颜色,或者其他的属性来实现动画,就不能实现。

属性动画的特点

       我们在上面大书特书了视图动画的不足,那是不是属性动画就完全弥补了视图动画的不足?答案是的:

1,属性动画真正改变的是对象的属性
2,属性动画是可以交互的
3,属性动画针对view与非view都能实现动画
4,如果属性不存在,还可以自定义属性来实现动画

视图动画的使用场景

       虽然属性动画有这样那样的优点,是不是视图动画我们就抛诸脑后,不在使用?是,也不是?如果你新开发一个app,并且不是支持3.0一下的版本,那最好是直接使用属性动画,那什么时候使用视图动画?

1,现有代码已经实现效果
2,视图动画能实现所需要的效果
3,视图动画只需要更少的代码来实现

代码结构

  • Animator
    • ValueAnimator
      • ObjectAnimator
      • TimeAnimator
    • AnimatorSet

       这里我们主要注意一下视图动画与属性动画的异同。代码结构很类似,但是根节点是不一样的,视图动是Animation,属性动画是Animator,视图动画有四个子类,平移,旋转,缩放,渐变,这里完全用ObjectAnimator来代替了。

动画实现方式

xml实现

在res/animator下定义动画文件(ValueAnimator-animator,ObjectAnimator
–objectAnimator, AnimatorSet-set ) 代码中采用AnimatorInflater().
loadAnimator()加载

代码实现

采用ObjectAnimator或者ValueAnimator实现

动画实现

ObjectAnimator

       我们上面讲述了两种动画的实现方式。这里我们先讲述一下代码实现,我们先来看看在视图动画的一个截图,当时实现了平移,旋转,缩放,渐变,那这里我们用属性动画来实现一遍:

这里写图片描述

       在实现这四种动画之前,我们先来讲解一下使用方式,我们先来看看可以使用的api有哪些?

这里写图片描述

       我们从图片可以到,of类型的函数有很多种,有ofArgb,ofFloat,ofInt等,这里有这么多api,那我们使用哪一个?这里我们主要使用ofFloat,因为这个函数是从3.0引入的,这个函数比较通用,其他的函数有的是4.0引入的,5.0引入的。因此这里我们主要讲解ofFloat:

ObjectAnimator.ofFloat(Object target, String propertyName, float... values)

       各个属性的解释如下:

1,target作用的对象
2,propertyName作用的属性,需要有public的setName函数
3,values可变参数,表示动画执行的一组值

       渐变

private void alpha() {
    ObjectAnimator alpha = ObjectAnimator.ofFloat(love, "alpha", 1.0f, 0.0f);
    alpha.setDuration(1000);
    alpha.start();
}

       这里使用的属性是alpha,1.0f表示完全不透明,0.0f表示完全透明。

       平移

private void translate() {
    ObjectAnimator translate = ObjectAnimator.ofFloat(love, "translationX", 200f);
    translate.setDuration(1000);
    translate.start();
}

       这里使用的是translationX属性,只平移了x轴,Y轴不做变化。

       缩放

private void scale() {
ObjectAnimator scaleX = ObjectAnimator.ofFloat(love, “scaleX”, 1.0f, 2.0f);
scaleX.setDuration(1000);
scaleX.start();
ObjectAnimator scaleY = ObjectAnimator.ofFloat(love, “scaleY”, 1.0f, 2.0f);
scaleY.setDuration(1000);
scaleY.start();
}
       这里使用了scaleX与scaleY属性,1.0f表示原始大小,2.0f表示放大一倍。默认缩放为中心缩放。
       旋转

private void rotate() {
    ObjectAnimator rotate = ObjectAnimator.ofFloat(love, "rotation", 0f, 360f);
    rotate.setDuration(1000);
    rotate.start();
}

       这里使用的是rotation属性,旋转了360度,默认的旋转中心为视图中心。

       上面我使用了一系列属性,我怎么知道哪些属性可以用哪些属性不能用。这里我们去查找对应view所包含的属性就知道了。

没有对应属性

       上面我们都是使用已知的属性,如果一个view我们想要实现的效果没有对应的属性怎么办?

直接添加set函数
包装原有类,添加set函数
采用ValueAnimator实现

       这里我们解释以下上面的情况,第一种如果你有权限,或者是自定义的view,如果没有对应属性,我们可以手动添加对应的属性,第二种情况,我们可以包装现有类,添加对应的属性,第三种我们可以采用ValueAnimator来实现,这里我们来包装一下原有类,后面我们会讲解ValueAnimator的实现。

       我们就以上述缩放效果来演示,上面我们使用了两个属性scaleX,scaleY来缩放一个view,这里我们用一个属性scale属性来缩放,但是ImageView没有缩放这个属性,那怎么办?

public class WrapperView {

    public ImageView target;

    private float scale;

    public WrapperView(ImageView target) {
        this.target = target;
    }

    public float getScale() {
        return scale;
    }

    public void setScale(float scale) {
        this.scale = scale;
        target.setScaleX(scale);
        target.setScaleY(scale);
    }
}

       我们包装对应的imageview,给他添加对应的scale属性,之后在代码中使用。动画代码如下:

private void addProperty() {

    WrapperView view = new WrapperView(love);
    ObjectAnimator scale = ObjectAnimator.ofFloat(view, "scale", 1.0f, 2.0f);
    scale.setDuration(1000);
    scale.start();
}

       这里我们使用的属性就是上述添加的scale属性。这样我们就实现了缩放的效果。

组合动画

       我们在视图动画时讲过,动画有并行,串行,属性动画也有这些组合。那我们接下来就讲述一下这些效果。

并行动画

       并行动画,就是说多个动画同时执行,比如我们在旋转的同时缩放一个view,这里主要有三种方式实现:

       1:多个ObjectAnimator实现

private void togetherAnim1() {
    ObjectAnimator rotate = ObjectAnimator.ofFloat(love, "rotation", 0f, 360f).setDuration(1000);
    ObjectAnimator scaleX = ObjectAnimator.ofFloat(love, "scaleX", 1.0f, 2.0f).setDuration(1000);
    ObjectAnimator scaleY = ObjectAnimator.ofFloat(love, "scaleY", 1.0f, 2.0f).setDuration(1000);
//        rotate.start();
//        scaleX.start();
//        scaleY.start();
    AnimatorSet set = new AnimatorSet();
    set.playTogether(rotate, scaleX, scaleY);
    set.setDuration(1000);
    set.start();
}

       这里我们定义了三个ObjectAnimator,之后用AnimatiorSet来进行执行。调用playTogether来同时执行这个三个动画,还有一种方式是,可以分别start三个动画也能实现效果。

这里有人可能会有疑问,连续三个start那不应该是串行的吗?怎么会是并行的呐?其实这里的start,动画并没有立即开始执行,而是等待下一个刷新时间戳过来才执行的。

       2:一个ObjectAnimator实现

private void togetherAnim2() {
    PropertyValuesHolder rotate = PropertyValuesHolder.ofFloat("rotation", 0f, 360f);
    PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 1.0f, 2.0f);
    PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 1.0f, 2.0f);
    ObjectAnimator together = ObjectAnimator.ofPropertyValuesHolder(love, rotate, scaleX, scaleY);
    together.setDuration(1000);
    together.start();
}

       这里我们使用PropertyValuesHolder来实现,定义三个PropertyValuesHolder,它与ObjectAnimator的区别是没有target属性,之后将PropertyValuesHolder放置到ObjectAnimator里面,之后start ObjectAnimator动画。

       3:采用ViewPropertyAnimator实现

private void togetherAnim3() {
    love.animate().rotation(360f).scaleX(2.0f).scaleY(2.0f).setDuration(1000).start();
}

       该方式系统已经实现了一个帮助方法,animate,之后可以链式的执行各个属性。该方式代码量是最少了的。

串行动画

       前面已经实现了并行,我们调用了playTogether,这里我们只要换成另外一种方法就可以实现串行。

private void sequenceAnim() {
    ObjectAnimator rotate = ObjectAnimator.ofFloat(love, "rotation", 0f, 360f).setDuration(1000);
    ObjectAnimator scaleX = ObjectAnimator.ofFloat(love, "scaleX", 1.0f, 2.0f).setDuration(1000);
    ObjectAnimator scaleY = ObjectAnimator.ofFloat(love, "scaleY", 1.0f, 2.0f).setDuration(1000);
    AnimatorSet set = new AnimatorSet();
    set.playSequentially(rotate, scaleX, scaleY);
    set.setDuration(1000);
    set.start();
}

       只需要将playTogether换成为playSequentially就可以了。

排列多个动画

       前面我们实现了并行与串行动画,但是如果一个动画既有串行又有并行怎么办?代码如下:

private void compositeAnim() {
    ObjectAnimator translateY = ObjectAnimator.ofFloat(love, "translationY", -200f).setDuration(1000);
    ObjectAnimator scaleX = ObjectAnimator.ofFloat(love, "scaleX", 1.0f, 2.0f).setDuration(1000);
    ObjectAnimator scaleY = ObjectAnimator.ofFloat(love, "scaleY", 1.0f, 2.0f).setDuration(1000);
    ObjectAnimator rotate = ObjectAnimator.ofFloat(love, "rotation", 0f, -45f).setDuration(1000);
    AnimatorSet set = new AnimatorSet();
    set.play(scaleX).with(scaleY).with(rotate);
    set.play(translateY).after(scaleX);
    set.start();
}

       这里还是采用AnimatorSet来实现,with表示同时执行动画。before方法,before里面的代码后执行,after方法,after里面的动画先执行。

ValueAnimator

       前面我们已经讲解了ObjectAnimator,我们前面的代码结构中还有ValueAnimator,那ValueAnimator是什么呐?

1,ValueAnimator是一个数值发生器
2,ValueAnimator不作用于任何属性
3,需要在onAnimationUpdate获取当前时间点发生的值

       我们解释一下上面的要点,首先ValueAnimator是一个数值发生器,就是他只产生数值,不对任何属性起作用,因此他与ObjectAnimator不同,他不能产生动画,动画需要我们手动来处理。处理的方式就是在onAnimationUpdate中获取产生的值。

       那我们就来用ValueAnimator来实现一个动画,我们实现一个倒计时的动画:

private void countDown() {
    ValueAnimator countDown = ValueAnimator.ofInt(10, 0);
    countDown.setDuration(10000);
    countDown.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            textView.setText("" + animation.getAnimatedValue());
        }
    });
    countDown.start();
}

       这里我们用的ofInt函数,因为倒计时是整数,所以这里用ofInt,我们在对动画设置执行时长,最后在回调中获取产生的值,animation.getAnimatedValue()就是当前产生的值。我们使用这个产生的值,实现倒计时动画效果。

动画监听

       我们已经学习了怎么用ObjectAnimator和ValueAnimator来处理动画,但是如果需要监听动画的开始和结束又怎么办?
我们可以添加监听:


private void alpha() {
    ObjectAnimator alpha = ObjectAnimator.ofFloat(love, "alpha", 1.0f, 0.0f);
    alpha.setDuration(1000);
    alpha.addListener(new Animator.AnimatorListener() {
        @Override
        public void onAnimationStart(Animator animation) {
            Toast.makeText(ObjectAnimationActivity.this, "start", Toast.LENGTH_LONG).show();
            ;
        }

        @Override
        public void onAnimationEnd(Animator animation) {

        }

        @Override
        public void onAnimationCancel(Animator animation) {

        }

        @Override
        public void onAnimationRepeat(Animator animation) {

        }
    });
    alpha.start();
}

       我们可以对动画添加Animator.AnimatorListener监听,有四个回调函数,这样我们就可以在动画开始,结束进行处理。

       不过上诉有四个回调,我们往往是不需要处理这么多回调,仅仅只是想处理需要关注的回调,这种情况有解吗?我们可以采用如下的方式进行处理:

private void translate() {
    ObjectAnimator translate = ObjectAnimator.ofFloat(love, "translationX", 200f);
    translate.setDuration(1000);
    translate.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator animation) {
            super.onAnimationEnd(animation);
            //TODO
        }
    });
    translate.start();
}

       对动画设置AnimatorListenerAdapter,这里我们只需要关注需要关注的回调。

       还有一种监听方式我们前面已经使用过了。就是ValueAnimator中添加监听,我们可以添加ValueAnimator.AnimatorUpdateListener监听,在onAnimationUpdate回调中进行处理。

插值器

       在视图动画中,我们已经讲解过插值器,这里的插值器与视图动画中的插值器没有什么区别。因此就不在进行讲述了。

总结

       这里我们已经讲述完成了属性动画的实现,还有其他的属性没有实现,在接下来的一篇中我们继续讲述剩下的属性。

作者:xueshanhaizi 发表于2016/10/3 19:08:50 原文链接
阅读:110 评论:0 查看评论

主界面加载数据流程图

$
0
0

转载本专栏每一篇博客请注明转载出处地址,尊重原创。此博客转载链接地址:点击打开链接    http://blog.csdn.net/qq_32059827/article/details/52729134

最近在开发一款App,其中搭建的主界面加载服务器数据流程框架如下。因为只是图片,又是个人做的笔记可能基本看不懂讲什么。。。PS:此文仅博主个人笔记。


作者:qq_32059827 发表于2016/10/3 19:49:28 原文链接
阅读:112 评论:0 查看评论

Android官方开发文档Training系列课程中文版:线程执行操作之线程池操作

$
0
0

原文地址:http://android.xsoftlab.net/training/multiple-threads/run-code.html#StopThread

上节课我们学习了如何定义一个类用于管理线程以及任务。这节课将会学习如何在线程池中运行任务。要做到这一点,只需要往线程池的工作队列中添加任务即可。当一条线程处于闲置状态时,那么ThreadPoolExecutor会从任务队列中取出一条任务并放入该线程中运行。

这节课还介绍了如何停止一个正在运行中的任务。如果在任务开始后,可能发现这项任务并不是必须的,那么就需要用到任务取消的功能了。这样可以避免浪费处理器的时间。举个例子,如果你正从网络上下载一张图像,如果侦测到这张图像已经在缓存中了,那么这时就需要停止这项网络任务了。

在线程池中的线程内运行任务

为了在指定的线程池中启动一项线程任务,需要将Runnable对象传给ThreadPoolExecutor的execute()方法。这个方法会将任务添加到线程池的工作队列中去。当其中一个线程变为闲置状态时,那么线程池管理器会从队列中取出一个已经等待了很久的任务,然后放到这个线程中运行:

public class PhotoManager {
    public void handleState(PhotoTask photoTask, int state) {
        switch (state) {
            // The task finished downloading the image
            case DOWNLOAD_COMPLETE:
            // Decodes the image
                mDecodeThreadPool.execute(
                        photoTask.getPhotoDecodeRunnable());
            ...
        }
        ...
    }
    ...
}

当ThreadPoolExecutor启动一个Runnable时,它会自动调用Runnable的run()方法。

中断执行中的代码

如果要停止一项任务,那么需要中断该任务所在的线程。为了可以预先做到这一点,那么需要在任务创建时存储该任务所在线程的句柄:

class PhotoDecodeRunnable implements Runnable {
    // Defines the code to run for this task
    public void run() {
        /*
         * Stores the current Thread in the
         * object that contains PhotoDecodeRunnable
         */
        mPhotoTask.setImageDecodeThread(Thread.currentThread());
        ...
    }
    ...
}

我们可以调用Thread.interrupt()方法来中断一个线程。这里要注意Thread对象是由系统控制的,系统会在应用进程的范围之外修改它们。正因为这个原因,在中断线程之前,需要对线程的访问加锁。通常需要将这部分代码放入同步代码块中:

public class PhotoManager {
    public static void cancelAll() {
        /*
         * Creates an array of Runnables that's the same size as the
         * thread pool work queue
         */
        Runnable[] runnableArray = new Runnable[mDecodeWorkQueue.size()];
        // Populates the array with the Runnables in the queue
        mDecodeWorkQueue.toArray(runnableArray);
        // Stores the array length in order to iterate over the array
        int len = runnableArray.length;
        /*
         * Iterates over the array of Runnables and interrupts each one's Thread.
         */
        synchronized (sInstance) {
            // Iterates over the array of tasks
            for (int runnableIndex = 0; runnableIndex < len; runnableIndex++) {
                // Gets the current thread
                Thread thread = runnableArray[taskArrayIndex].mThread;
                // if the Thread exists, post an interrupt to it
                if (null != thread) {
                    thread.interrupt();
                }
            }
        }
    }
    ...
}

在多数情况下,Thread.interrupt()会使线程立刻停止。然而,它只会将那些正在等待的线程停下来,它并不会中止CPU或网络任务。为了避免使系统变慢或卡顿,你应当在开始任意一项操作之前测试是否有中断请求:

/*
 * Before continuing, checks to see that the Thread hasn't
 * been interrupted
 */
if (Thread.interrupted()) {
    return;
}
...
// Decodes a byte array into a Bitmap (CPU-intensive)
BitmapFactory.decodeByteArray(
        imageBuffer, 0, imageBuffer.length, bitmapOptions);
...
作者:u011064099 发表于2016/10/3 19:49:43 原文链接
阅读:132 评论:0 查看评论

Android 开发中Layout_Margin与padding的区别以及Layout_gravity与gravity的区别

$
0
0

Layout_Margin与padding的区别以及Layout_gravity与gravity的区别

平时开发中这几个属性是我们经常使用的几个属性,偶尔脑子一糊涂,就容易弄混这些属性,下面,我就仔细介绍一下这几个属性:


1.首先介绍Layout_Margin与padding:

1.1.不设置任何Layout_Margin或者padding属性

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    android:layout_height="match_parent"
    android:layout_width="match_parent"
    android:id="@+id/groupView"
    android:background="#22aaa4"
    android:orientation="vertical"
    xmlns:android="http://schemas.android.com/apk/res/android">
    <Button
        android:background="#9944aa"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:text="hehe"
        android:id="@+id/btn"/>
    <Button
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:text="hehe2"/>
</LinearLay

上面xml中没有设置任何Layout_Margin或者padding属性,显示效果如下:

1.2设置padding属性:

<LinearLayout
    android:layout_height="match_parent"
    android:layout_width="match_parent"
    android:id="@+id/groupView"
    android:background="#22aaa4"
    <span style="color:#ff0000;">android:padding="50dp"</span>
    android:orientation="vertical"
    xmlns:android="http://schemas.android.com/apk/res/android">
    <Button
        android:background="#9944aa"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:text="hehe"
        android:id="@+id/btn"/>
    <Button
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:text="hehe2"/>
</LinearLayout>

上面再LinearLayout里设置了padding属性,显示效果如下:



可以看见LinearLayout内部的控件全部都离边框一段距离了

下面继续:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    android:layout_height="match_parent"
    android:layout_width="match_parent"
    android:id="@+id/groupView"
    android:background="#22aaa4"
   <span style="color:#ff0000;"> android:padding="50dp"</span>
    android:orientation="vertical"
    xmlns:android="http://schemas.android.com/apk/res/android">
    <Button
        android:background="#9944aa"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:text="hehe"
        android:id="@+id/btn"/>
    <Button
        android:layout_width="match_parent"
        android:layout_height="100dp"
       <span style="color:#ff0000;"> android:paddingTop="40dp"</span>
        android:text="hehe2"/>
</LinearLayout>

在之前的基础上,又在第二个Button中添加了一个padding属性,显示效果如下:



清晰了没有?原来padding就是会影响你设置了padding属性的这个控件的内部的状态,。

1.3.设置Layout_Margin:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    android:layout_height="match_parent"
    android:layout_width="match_parent"
    android:id="@+id/groupView"
    android:background="#22aaa4"

    android:orientation="vertical"
    xmlns:android="http://schemas.android.com/apk/res/android">
    <Button
        android:background="#9944aa"
        android:layout_width="match_parent"
        android:layout_height="100dp"
       <span style="color:#ff0000;"> android:layout_margin="100dp"</span>
        android:text="hehe"
        android:id="@+id/btn"/>
    <Button
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:text="hehe2"/>
</LinearLayout>
上面就在第一个Button中设置了margin值,看显示效果与第一张没设置任何东西得图作对比哦:




在设置一个看看:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    android:layout_height="match_parent"
    android:layout_width="match_parent"
    android:id="@+id/groupView"
    android:background="#22aaa4"
    android:orientation="vertical"
    xmlns:android="http://schemas.android.com/apk/res/android">
    <Button
        android:background="#9944aa"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        <span style="color:#ff0000;">android:layout_margin="100dp"</span>
        android:text="hehe"
        android:id="@+id/btn"/>
    <Button
        android:layout_width="match_parent"
       <span style="color:#ff0000;"> android:layout_marginTop="100dp"</span>
        android:layout_height="100dp"
        android:text="hehe2"/>
</LinearLayout>

显示效果如下:



看出来没有?layout_Margin就是指你设置了这个属性的控件,在他的父控件里与其他的控件之间的位置,千万不要以为这个是就是子控件在父控件里的相对位置哦!这样表达式不正确的啊,第二幅图就直接说明了这一点,是控件之间的关系,


总结:padding是指设置了这个属性的控件内部的位置变化关系,比如TextView中或者Button中设置了的话,那么这些控件上面显示的text位置就会发生相应的变化,如果在父控件中设置了这个属性,比如LinearLayout中设置了这个属性,那么它内部的子控件的位置就会发生变化!
          Layout_Margin是指设置了这个属性的控件,与他平级的控件之间的位置关系,(Ps:padding是内部的位置关系)!


2.Gravity与Layout_gravity之间的关系:

2.1不设置任何东西:

<LinearLayout
    android:layout_height="match_parent"
    android:layout_width="match_parent"
    android:background="#44aa77"
    xmlns:android="http://schemas.android.com/apk/res/android">
    <TextView
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:text="hehe"
        android:textSize="30sp"
        android:background="#aa2288"/>
</LinearLayout>



设置gravity:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    android:layout_height="match_parent"
    android:layout_width="match_parent"
    android:background="#44aa77"
    xmlns:android="http://schemas.android.com/apk/res/android">
    <TextView
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:text="hehe"
        <span style="color:#ff0000;">android:gravity="center"</span>
        android:textSize="30sp"
        android:background="#aa2288"/>
</LinearLayout>


gravity属性,会影响设置了这个属性的控件的内部的状态



2.只设置Layout_gravity:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    android:layout_height="match_parent"
    android:layout_width="match_parent"
    android:background="#44aa77"
    xmlns:android="http://schemas.android.com/apk/res/android">
    <TextView
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:text="hehe"
        <span style="color:#ff0000;">android:layout_gravity="center"</span>
        android:textSize="30sp"
        android:background="#aa2288"/>
</LinearLayout>



设置了layout_gravity这个属性,会影响设置了这个属性的控件相对他的父控件里的位置变化。


结论:gravity影响控件内部状态,Layout_gravity会影响这个控件在父控件 里的状态。


作者:zhangtao19931104 发表于2016/10/3 19:51:11 原文链接
阅读:89 评论:0 查看评论

Android动画之属性动画(下)

$
0
0

前言

       前面我们已经完整的讲述了属性动画的实现,我们已经学会了怎么实现动画,如果没有属性我们也学会了怎么添加属性,还学习了用ValueAnimator来实现动画。

Evaluator

       这里我们来学习剩下的属性,首先我们来看看Evaluator,Evaluator是什么?他有什么用?

       Evaluator翻译为求值器,或者表达式,反正只可意会不可言传,就是给一个表达式,计算对应的值,他主要有以下属性:

1, 返回动画当前时间点的属性值
2,全部继承自TypeEvaluator
3,系统实现了IntEvaluator,FloatEvaluator,ArgbEvaluator
4,Api21实现了PointFEvaluator

       我们解释一下上面的属性,首先表达式可以计算当前时间点的值,并且所有的Evaluator都继承自TypeEvaluator,从这一点就可以知道,我们可以自己来实现Evaluator,目前已经有几个默认的实现,我们前面所实现的动画都是ofFloat,ofInt没有实现过其他类型的动画,这是因为float,int的表达式系统已经默认实现,如果有一个新的表达式不属于上面的任何属性,我们怎么办?

       我们可以自定义Evaluator,我们接下来实现一个自定义的Evaluator。我们来实现一个抛物线的效果,效果如下图:

这里写图片描述

       一个view从0这个位置移动到600这个位置,一个抛物线的效果,表达式如下:

y=1150x2+4x

       我们根据上述的表达式来实现动画,动画下过如下:
private void setCustomEvaluator() {
    ValueAnimator animator = new ValueAnimator();
    float offsetX = love.getX();
    float offsetY = love.getY();
    animator.setDuration(2000);
    animator.setObjectValues(new PointF(offsetX, offsetY), new PointF(TRANS_X + offsetX, offsetY));
    animator.setEvaluator(new TypeEvaluator<PointF>() {
        @Override
        public PointF evaluate(float fraction, PointF startValue, PointF endValue) {
            PointF pointF = new PointF();
            float d = fraction * TRANS_X;
            pointF.x = startValue.x + d;
            pointF.y = startValue.y + (1.0f / 150f) * d * d - 4 * d;
            return pointF;
        }
    });
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            PointF pointF = (PointF) animation.getAnimatedValue();
            love.setX(pointF.x);
            love.setY(pointF.y);
        }
    });
    animator.start();
}

       这里我们需要注意的几点是第一个设置好Evaluator,第二设置ObjectValues,之后在onAnimationUpdate回调中获取得到的值,Evaluator中就是我们根据我们的函数计算得到的值。这样我们的自定义动画就已经实现完成了。

Keyframe

       这里的第二个需要讲解的知识点是Keyframe,Keyframe的意思如下:

1, KeyFrame是一个时间/值对
2, KeyFrame之间可以定义不同的Interpolator

       这里我们用Keyframe来实现动画:

private void keyFrame() {
    Keyframe keyframe1 = Keyframe.ofFloat(0f, 0f);
    Keyframe keyframe2 = Keyframe.ofFloat(0.5f, 360f);
    Keyframe keyframe3 = Keyframe.ofFloat(1.0f, 0f);
    PropertyValuesHolder pvh = PropertyValuesHolder.ofKeyframe("rotation", keyframe1, keyframe2, keyframe3);
    ObjectAnimator rotate = ObjectAnimator.ofPropertyValuesHolder(love, pvh);
    rotate.setDuration(2000);
    rotate.start();
}

       我们先定义几个Keyframe,最后将Keyframe使用到PropertyValuesHolder中,最后又将PropertyValuesHolder使用到ObjectAnimator中,上诉定义了三个Keyframe,比如我们实现一个旋转的动画,第一个关键帧为Keyframe.ofFloat(0f, 0f)表示第0时角度为0,剩下的一半的时间角度360,最终位置的角度为0度。

       其实这里的Keyframe我们前面已经有过使用,比如:

ObjectAnimator scaleX = ObjectAnimator.ofFloat(love, "scaleX", 1.0f, 2.0f);

       这里的1.0f与2.0f就表示开始的关键帧与结束的关键帧,只是这里只有两帧。

XML实现

       前面我们已经完全用代码实现了动画,但是动画有两种实现方式,还有一种实现方式就是用xml来实现。接下来我们就用xml来实现一个动画:

       1:首先定义一个动画资源,放置到res/animator文件夹下,定义一个trans_scale.xml:

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

    <objectAnimator
        android:duration="2000"
        android:propertyName="translationY"
        android:valueTo="-200f"
        android:valueType="floatType"></objectAnimator>

    <set android:ordering="together">
        <objectAnimator
            android:duration="2000"
            android:propertyName="scaleX"
            android:valueTo="2.0f"
            android:valueType="floatType"></objectAnimator>
        <objectAnimator
            android:duration="2000"
            android:propertyName="scaleY"
            android:valueTo="2.0f"
            android:valueType="floatType"></objectAnimator>

    </set>

</set>

       这里如果是set我们需要设置ordering属性,主要有together并且与sequentially串行,每一属性需要设置valueType。

       2:代码中进行加载:

private void compositeAnimXml(){
    Animator animator = AnimatorInflater.loadAnimator(this, R.animator.trans_scale);
    animator.setTarget(love);
    animator.start();
}

       这样我们就用xml实现了动画。

Layout Animations

       Layout Animations为布局动画,控制view加入与删除自身的动画与周围view的动画。主要有以下五种效果:

1, LayoutTransition.APPEARING,对出现的view设置动画
2, LayoutTransition.CHANGE_APPEARING,其他view设置动画
3, LayoutTransition.DISAPPEARING,对消失的view设置动画
4, LayoutTransition. CHANGE_DISAPPEARING ,其他view设置动画
5, LayoutTransition. CHANGE ,其他view设置动画

       上面的五种效果,后面都已经分别做了解释,这里我们用一个demo来演示一下:

       定义布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    android:id="@+id/root"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.netease.study.ui.animation.LayoutAnimationActivity">

    <Button
        android:id="@+id/add_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="addView"/>

</LinearLayout>

       代码实现如下:

public class LayoutAnimationActivity extends AppCompatActivity implements View.OnClickListener {

    public static void start(Context context) {
        Intent intent = new Intent();
        intent.setClass(context, LayoutAnimationActivity.class);
        context.startActivity(intent);
    }

    LinearLayout root;

    int i = 0;

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

    private void findViews() {
        root = (LinearLayout) findViewById(R.id.root);
        root.setLayoutTransition(getLayoutTransition());
    }

    private LayoutTransition getLayoutTransition() {
        LayoutTransition transition = new LayoutTransition();
        transition.setAnimator(LayoutTransition.APPEARING, transition.getAnimator(LayoutTransition.APPEARING));
        transition.setAnimator(LayoutTransition.CHANGE_APPEARING, transition.getAnimator(LayoutTransition
                .CHANGE_APPEARING));
        transition.setAnimator(LayoutTransition.CHANGE_DISAPPEARING, transition.getAnimator(LayoutTransition
                .CHANGE_DISAPPEARING));
        transition.setAnimator(LayoutTransition.DISAPPEARING, transition.getAnimator(LayoutTransition.DISAPPEARING));
        transition.setAnimator(LayoutTransition.CHANGING, transition.getAnimator(LayoutTransition.CHANGING));
        return transition;
    }

    private void setViewListener() {
        findViewById(R.id.add_view).setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.add_view:
                addViews();
                break;
        }
    }

    private void addViews() {
        final Button button = new Button(this);
        button.setText("button=" + i++);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                root.removeView(button);
            }
        });
        root.addView(button);
    }
}

private void compositeAnimXml(){
    Animator animator = AnimatorInflater.loadAnimator(this, R.animator.love_f);
    animator.setTarget(love);
    animator.start();
}

       我们对线性布局添加button,对布局设置LayoutTransition,这样就实现了布局动画,最常用的效果就是比如listview中加载item时设置动画。

转场动画

       在文章的最后我们来讲述一下转场动画,什么是转场动画,通俗的来说,就是页面切换的效果。

为什么要设置?

       由于不同的手机打开关闭的效果不一致,各个手机都有不同的效果,效果不是很好,我们可以统一来控制页面打开关闭的效果,保证所有手机效果的统一性,提升用户体验。

怎么设置?

       首先定义进入退出的动画,第一个种方式就是采用style来实现,主要是来复写android:windowAnimationStyle节点,系统默认有如下可以设置的属性:

<style name="Animation.Activity">
    <item name="activityOpenEnterAnimation">@anim/activity_open_enter</item>
    <item name="activityOpenExitAnimation">@anim/activity_open_exit</item>
    <item name="activityCloseEnterAnimation">@anim/activity_close_enter</item>
    <item name="activityCloseExitAnimation">@anim/activity_close_exit</item>
    <item name="taskOpenEnterAnimation">@anim/task_open_enter</item>
    <item name="taskOpenExitAnimation">@anim/task_open_exit</item>
    <item name="launchTaskBehindTargetAnimation">@anim/launch_task_behind_target</item>
    <item name="launchTaskBehindSourceAnimation">@anim/launch_task_behind_source</item>
    <item name="taskCloseEnterAnimation">@anim/task_close_enter</item>
    <item name="taskCloseExitAnimation">@anim/task_close_exit</item>
    <item name="taskToFrontEnterAnimation">@anim/task_open_enter</item>
    <item name="taskToFrontExitAnimation">@anim/task_open_exit</item>
    <item name="taskToBackEnterAnimation">@anim/task_close_enter</item>
    <item name="taskToBackExitAnimation">@anim/task_close_exit</item>
    <item name="wallpaperOpenEnterAnimation">@anim/wallpaper_open_enter</item>
    <item name="wallpaperOpenExitAnimation">@anim/wallpaper_open_exit</item>
    <item name="wallpaperCloseEnterAnimation">@anim/wallpaper_close_enter</item>
    <item name="wallpaperCloseExitAnimation">@anim/wallpaper_close_exit</item>
    <item name="wallpaperIntraOpenEnterAnimation">@anim/wallpaper_intra_open_enter</item>
    <item name="wallpaperIntraOpenExitAnimation">@anim/wallpaper_intra_open_exit</item>
    <item name="wallpaperIntraCloseEnterAnimation">@anim/wallpaper_intra_close_enter</item>
    <item name="wallpaperIntraCloseExitAnimation">@anim/wallpaper_intra_close_exit</item>
    <item name="fragmentOpenEnterAnimation">@animator/fragment_open_enter</item>
    <item name="fragmentOpenExitAnimation">@animator/fragment_open_exit</item>
    <item name="fragmentCloseEnterAnimation">@animator/fragment_close_enter</item>
    <item name="fragmentCloseExitAnimation">@animator/fragment_close_exit</item>
    <item name="fragmentFadeEnterAnimation">@animator/fragment_fade_enter</item>
    <item name="fragmentFadeExitAnimation">@animator/fragment_fade_exit</item>
</style>

       这里定义了页面进入的动画,页面退出的动画等等,由于不同的手机默认实现的效果不同,因此我们可以复写上述这些属性,达到统一的效果。

       我们首先定义如下三个动画:
       水平进入的动画base_slide_right_in.xml

<?xml version="1.0" encoding="utf-8"?>
<set
  xmlns:android="http://schemas.android.com/apk/res/android">
    <translate android:interpolator="@android:anim/decelerate_interpolator" android:duration="300"
               android:fromXDelta="100.0%" android:toXDelta="0.0%" />
</set>

       水平退出的动画base_slide_right_out.xml

<?xml version="1.0" encoding="utf-8"?>
<set
    xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="300"
        android:fromXDelta="0.0%"
        android:interpolator="@android:anim/accelerate_interpolator"
        android:toXDelta="100.0%"/>
</set>

       原地不动的动画base_stay_orig.xml

<?xml version="1.0" encoding="utf-8"?>
<set
    xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="300"
        android:fromXDelta="0.0%"
        android:interpolator="@android:anim/decelerate_interpolator"
        android:toXDelta="0.0%"/>
</set>

       之后我们复写android:windowAnimationStyle属性,我们定义一个新的style,设置到android:windowAnimationStyle中:

<style name="WindowAnimation" parent="@android:style/Animation">
    <item name="android:activityOpenEnterAnimation">@anim/base_slide_right_in</item>
    <item name="android:activityOpenExitAnimation">@anim/base_stay_orig</item>
    <item name="android:activityCloseEnterAnimation">@anim/base_stay_orig</item>
    <item name="android:activityCloseExitAnimation">@anim/base_slide_right_out</item>
    <item name="android:taskOpenEnterAnimation">@anim/base_slide_right_in</item>
    <item name="android:taskOpenExitAnimation">@anim/base_stay_orig</item>
    <item name="android:taskCloseEnterAnimation">@anim/base_stay_orig</item>
    <item name="android:taskCloseExitAnimation">@anim/base_slide_right_out</item>
    <item name="android:taskToFrontEnterAnimation">@anim/base_slide_right_in</item>
    <item name="android:taskToFrontExitAnimation">@anim/base_stay_orig</item>
    <item name="android:taskToBackEnterAnimation">@anim/base_stay_orig</item>
    <item name="android:taskToBackExitAnimation">@anim/base_slide_right_out</item>
</style>

       这样页面转场的动画就已经实现完成了,不过遗憾的是,有的手机不起作用。。。。我们就需要采用另外一种方式来实现了,我们调用overridePendingTransition:

       overridePendingTransition函数如下:

public void overridePendingTransition(int enterAnim, int exitAnim) {
    try {
        ActivityManagerNative.getDefault().overridePendingTransition(
                mToken, getPackageName(), enterAnim, exitAnim);
    } catch (RemoteException e) {
    }
}

       定义一个进入的动画,和一个退出的动画

这里需要注意的是overridePendingTransition有非常严格的调用条件,它必须在startActivity或者finish之后调用

       调用方式如下:

@Override
public void finish() {
    super.finish();
    customExit();
}

private void customExit() {
    overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
}

总结

       到此属性动画就已经实现完成了,我们学习了动画怎么实现,怎么自定义Evaluator,怎么实现布局动画。

作者:xueshanhaizi 发表于2016/10/3 20:21:28 原文链接
阅读:99 评论:0 查看评论

Android热修复(Hot Fix)案例全剖析(二)

$
0
0

    在上篇博客中,我们初步了解了Android热修复的基本流程,具体可以看我的博客Android热修复(Hot Fix)案例全剖析(一),那么本篇博客,我将为大家全面剖析Android热修复的实现案例。

1.将下载的修复补丁拷贝到应用的内部缓存目录中

    在上一篇文章中,我们已经生成了用于修复Bug的classes2.dex补丁包,通常我们会在APP后台子线程中自动调用热修复接口,并下载修复补丁,这里为了方便演示,我们把已经下载好的dex补丁文件放到SD卡中,然后将下载的修复补丁拷贝到应用的内部缓存目录中cacheDir,之所以这样做是因为下一步我们需要使用类加载器ClassLoader在内部缓存中加载classese.dex包。下面是我写的一个将classes2.dex包拷贝到内部缓存目录中的方法。

/**
 * 修复方法
 */
private void castielFixMethod() {
        // 创建一个内部缓存目录,把我们SD卡中的"classes2.dex"文件拷贝到内部缓存目录中cache
        File fileSDir = getDir(MyConstants.DEX_DIR, Context.MODE_PRIVATE);
        String name = "classes2.dex";
        String filePath = fileSDir.getAbsolutePath() + File.separator + name;
        File file = new File(filePath);
        if (file.exists()) {// 判断是否已经存在dex文件
            Log.i("WY", "已经存在dex文件");
            file.delete();
        }
        // 通过IO流将dex文件写到我们的缓存目录中去
        InputStream is = null;
        FileOutputStream fos = null;
        // 版权所有,未经许可请勿转载:猴子搬来的救兵http://blog.csdn.net/mynameishuangshuai
        try {
            is = new FileInputStream(Environment.getExternalStorageDirectory());
            fos = new FileOutputStream(filePath);
            int len = 0;
            byte[] buffer = new byte[1024];
            while ((len = is.read(buffer)) != -1) {
                fos.write(buffer, 0, len);
            }
            File f = new File(filePath);
            Log.i("WY", "filePath:" + f.getAbsolutePath());
            if (f.exists()) {
                Toast.makeText(this, "新的dex文件已经覆盖", Toast.LENGTH_LONG).show();
            }
            // 动态加载修复dex包 
            FixDexUtils.loadFixedDex(this);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                fos.close();
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

2.实现热修复工具类

这里首先给大家普及一下类加载的原理:
    在Android系统启动的时候会创建一个Boot类型的ClassLoader实例,用于加载一些系统Framework层级需要的类。由于Android应用里也需要用到一些系统的类,所以APP启动的时候也会把这个Boot类型的ClassLoader传进来。此外,APP也有自己的类,这些类保存在APK的dex文件里面,所以APP启动的时候,也会创建一个自己的ClassLoader实例,用于加载自己dex文件中的类。
    ClassLoader去加载Dex文件,首先Dex文件是放在/data/apk/packagename~1/base/apk,由于apk是一个类似于压缩包的东西,Android其实是使用一个优化的临时缓存目录optimizeDir(dex),专门把Dex文件解压进去,这样以后就从这个临时缓存目录中加载,提高效率。
    在1代码中我们提到了loadFixedDex()方法,便是我们的核心热修复工具类,我给大家具体讲一下:
    ClassLoader有一个简单的实现类-PathClassLoader。该类作为Android的默认的类加载器,本身继承自BaseDexClassLoader,BaseDexClassLoader重写了findClass方法,该方法是ClassLoader的核心。
    每个ClassLoader有一个pathList变量,是标识dex文件的路径,我们通过该路径加载dex文件,默认不分包的时候只有一个dex文件,当然谷歌在顶层设计时允许我们有多个dex文件。
    ClassLoader去找optimizeDir(dex)目录,然后把目录添加到pathList里面去,接着去找目录下面的所有的dex文件,把这些dex文件当做一个数组放到dexElements中去,这样就可以有多个dex文件。

pathList{
    dexElements{
        [classes.dex,classes2.dex]
    }
}

    ClassLoader每加载一个类,它会先找classes.dex,如果找不到就去classes2.dex中找,如果里面又一个dex有问题,比如说classes2.dex出问题了,我们就需要弄一个修复的新的classes2.dex文件放到数组中去,替换掉有问题的;但是classes2.dex中可能有多个类,除了有问题的类,也可能有很多正确的类,我们在替换时没必要把所有的类都替换掉,所以我们只要替换有问题的类。
    为此,我们可以采用一个策略,把新的替换的dex文件放到数组的最前面,最终数组的形态为:

[classes2.dex,classes.dex,classes2.dex]

    这里解释下,ClassLoader类加载器先加载我们修复的正确的dex文件,然后顺序加载数组中其他的dex元素,到了最后加载到旧的classes2.dex元素,由于前面已经加载了更新的classes2.dex(更新的dex文件中只包含修复的class),那么旧的classes2.dex元素中的有Bug的class就不会再加载,而是只加载其余的没有错误的class。
    整个流程其实非常简单,但是如果我们要实现这个过程却有个障碍,那就是由于我们的APK程序可能正在运行,谷歌并没有提供相关的接口方法去实现这一步骤,为此,我们需要使用反射的手段去实现。
1.首先需要反射ClassLoader类,找到里面的pathList变量,然后找到dexElements[]数组,该数组在修复之前只有两个元素,分别是classes.dex和classes2.dex(出错的),假设值数组1;
2.接着我们要往dexElements[]数组中添加classes2.dex文件。
Android中要想实现加载dex文件,需要使用DexClassLoader类加载classes2.dex(补丁),加载到dexElements[]数组中去,假设值数组2。
3.最后,我们需要把两个dexElements[]数组合并,作为一个新数组dexElements[],该数组中包含元素为classes2.dex(补丁),classes.dex和classes2.dex(出错的),完成后将数组返回赋值给系统的ClassLoader。

最后贴出热修复工具类源码

public class FixDexUtils {

    private static HashSet<File> loadedDex = new HashSet<File>();

    public static void loadFixedDex(Context context) {
        if (context == null) {
            return;
        }
        // 首先拿到缓存目录
        File fileSDir = context.getDir(MyConstants.DEX_DIR,
                Context.MODE_PRIVATE);
        File[] listFils = fileSDir.listFiles();
        // 遍历缓存文件
        for (File file : listFils) {
            // 如果文件是以"classes"开始或者以".dex"结尾,说明这是从SDK中拷贝回来的修复包
            if (file.getName().startsWith("classes")
                    || file.getName().endsWith(".dex")) {
                Log.i("WY", "当前dexName:" + file.getName());
                loadedDex.add(file);
            }
        }
        doDexInject(context, fileSDir);
    }

    private static void doDexInject(Context context, File fileDir) {
        if (Build.VERSION.SDK_INT >= 23) {
            Log.i("WY", "Unable to do dex inject on SDK"
                    + Build.VERSION.SDK_INT);
        }
        // .dex 的加载需要一个临时目录
        String optimizeDir = fileDir.getAbsolutePath() + File.separator
                + "opt_dex";
        File fopt = new File(optimizeDir);
        if (!fopt.exists())
            fopt.mkdirs();
        try {
            // 根据.dex 文件创建对应的DexClassLoader 类
            for (File file : loadedDex) {// 循环迭代,用于多个修复包同时注入
                DexClassLoader classLoader = new DexClassLoader(
                        file.getAbsolutePath(), fopt.getAbsolutePath(), null,
                        context.getClassLoader());
                // 注入
                inject(classLoader, context);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static void inject(DexClassLoader classLoader, Context context) {

        // 获取到系统的DexClassLoader 类
        PathClassLoader pathLoader = (PathClassLoader) context.getClassLoader();
        try {
            Object dexElements = combineArray(
                    getDexElements(getPathList(classLoader)),
                    getDexElements(getPathList(pathLoader)));
            Object pathList = getPathList(pathLoader);
            setField(pathList, pathList.getClass(), "dexElements", dexElements);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 通过反射获取DexPathList中dexElements
     */
    private static Object getDexElements(Object paramObject)
            throws IllegalArgumentException, NoSuchFieldException,
            IllegalAccessException {
        return getField(paramObject, paramObject.getClass(), "dexElements");
    }

    /**
     * 通过反射获取BaseDexClassLoader中的PathList对象
     */
    private static Object getPathList(Object baseDexClassLoader)
            throws IllegalArgumentException, NoSuchFieldException,
            IllegalAccessException, ClassNotFoundException {
        return getField(baseDexClassLoader,
                Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
    }

    /**
     * 通过反射获取指定字段的值
     */
    private static Object getField(Object obj, Class<?> cl, String field)
            throws NoSuchFieldException, IllegalArgumentException,
            IllegalAccessException {
        Field localField = cl.getDeclaredField(field);
        localField.setAccessible(true);
        return localField.get(obj);
    }

    /**
     * 通过反射设置字段值
     */
    private static void setField(Object obj, Class<?> cl, String field,
            Object value) throws NoSuchFieldException,
            IllegalArgumentException, IllegalAccessException {

        Field localField = cl.getDeclaredField(field);
        localField.setAccessible(true);
        localField.set(obj, value);
    }

    /**
     * 合并两个数组
     */
    private static Object combineArray(Object arrayLhs, Object arrayRhs) {
        Class<?> localClass = arrayLhs.getClass().getComponentType();
        int i = Array.getLength(arrayLhs);
        int j = i + Array.getLength(arrayRhs);
        Object result = Array.newInstance(localClass, j);
        for (int k = 0; k < j; ++k) {
            if (k < i) {
                Array.set(result, k, Array.get(arrayLhs, k));
            } else {
                Array.set(result, k, Array.get(arrayRhs, k - i));
            }
        }
        return result;
    }
}
作者:mynameishuangshuai 发表于2016/10/3 21:32:17 原文链接
阅读:118 评论:0 查看评论

ContentProvider简单使用 -- Android学习之路

$
0
0

ContentProvider

sky-mxc 总结 转载注明:https://sky-mxc.github.io


内容提供者,应用程序间的数据交互,是为存储和获取数据提供的统一接口。
Contentprovider为应用间数据交互提供了安全的环境,它允许把自己的应用数据开放给其他应用进行 CRUD。怎么样进行操作可以自己规定,不用担心权限的问题。

当然如果不想被被人读取自己应用的数据就不需要这个内容提供者。

ContentResolver来访问和操作我们的数据。
ContentResolver 通过我们注册的uri就可以知道我们开放的数据。

关于uri 参考这篇文章:http://blog.csdn.net/dlutbrucezhang/article/details/8917303

创建一个ContentProvider

创建自己的内容提供程序 只需继承ContentProvider即可。
这里就以insert 和 query 为例


package com.skymxc.demo.contentprovider.util;

import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.support.annotation.Nullable;
import android.text.TextUtils;

/**
 * Created by sky-mxc
 */

public class StudentProvider extends ContentProvider {

    private DBHelper dbHelper;
    private UriMatcher uriMatcher;

    //匹配结果是一张表
    private static final int STUDENTS = 1;
    //匹配结果是一个条数据
    private static final int STUDENT = 2;
    //一般是包名 避免重复
    private static final String AUTHORITY = "com.skymxc.demo";

    @Override
    public boolean onCreate() {
        dbHelper = new DBHelper(getContext());
        //初始化 uri匹配者   UriMatcher.NO_MATCH:匹配不上时返回
        uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

        //添加能够匹配的uri格式 参数1authorities 匹配住机。  参数2 匹配路径   参数3 code : match success  return this code;
        // 代表这个uri 操作的是一个表,匹配码是 STUDENTS
        uriMatcher.addURI(AUTHORITY,"student", STUDENTS);
        //代表这个uri 操作的是一条数据 匹配成功后返回 STUDENT
        uriMatcher.addURI(AUTHORITY,"student/#",STUDENT);
        return true;
    }

    /**
     *  查询操作
     * @param uri
     * @param projection 要查询的列
     * @param condition 查询条件
     * @param values 查询参数
     * @param sortOrder 排序
     * @return
     */
    @Nullable
    @Override
    public Cursor query(Uri uri, String[] projection, String condition, String[] values, String sortOrder) {
        Cursor cursor =null;
        SQLiteDatabase db= dbHelper.getDB();
        //匹配这个uri 要查询一张表还是 某条数据
        switch (uriMatcher.match(uri)){
            case STUDENT:
                //查询某条数据  ContentUris  :工具类 可以解析出id
                long id= ContentUris.parseId(uri);
                String where ="_id ="+id+" ";
                if (!TextUtils.isEmpty(condition)){
                    where+= " and "+condition;
                }
               cursor= db.query(DBHelper.TABLE_NAME,projection,where,values,null,null,sortOrder);
                break;
            case STUDENTS:
                //查询一张表
                cursor = db.query(DBHelper.TABLE_NAME,projection,condition,values,null,null,sortOrder);
                break;
            default:
                throw new IllegalArgumentException("match fail 。uri:"+uri+"");

        }
        return cursor;
    }

    @Nullable
    @Override
    public String getType(Uri uri) {
        String type="unKnow";
        switch (uriMatcher.match(uri)){
            case STUDENT:
                type="vnd.android.cursor.item/student";
                break;
            case STUDENTS:
                type= "vnd.android.cursor.dir/student";
                break;
        }
        return null;
    }

    @Nullable
    @Override
    public Uri insert(Uri uri, ContentValues values) {
        SQLiteDatabase db = dbHelper.getDB();
        switch (uriMatcher.match(uri)){
            case STUDENT:
                break;
            case STUDENTS:
                db.insert(DBHelper.TABLE_NAME,"_id",values);
                break;
        }
        return uri;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        return 0;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        return 0;
    }
}

创建完之后还需要在manifest文件中注册 才能被其他应用看到,通过 元素注册一个内容提供者

<!--
    android:exported 设置此provider是否可以被其他应用使用。
    android:readPermission 该provider的读权限的标识
    android:writePermission 该provider的写权限标识
    android:permission provider读写权限标识
    android:grantUriPermissions 临时权限标识

-->
<provider
    android:authorities="com.skymxc.demo"
    android:name=".util.StudentProvider"
    android:exported="true"/>

关于临时权限标识 grantUriPermissions :true时,意味着该provider下所有数据均可被临时使用;false时,则反之,但可以通过设置标签来指定哪些路径可以被临时使用。这么说可能还是不容易理解,我们举个例子,比如你开发了一个邮箱应用,其中含有附件需要第三方应用打开,但第三方应用又没有向你申请该附件的读权限,但如果你设置了此标签,则可以在start第三方应用时,传入FLAG_GRANT_READ_URI_PERMISSION或FLAG_GRANT_WRITE_URI_PERMISSION来让第三方应用临时具有读写该数据的权限。

> 到这里 一个简单的内容提供者就创建完成了

ContentResolver

可以看做是客户端 与ContentProvider 对应 ,ContentProvider 负责提供数据操作接口 ,ContentResolver 可以调用ContentProvider的数据接口对数据进行操作

为了测试上面定义的ContentProvider ,另创建一个Module 进行读取

    private void read() {
        ContentResolver resolver= getContentResolver() ;
        String uriStr ="content://com.skymxc.demo/student";
       Cursor cursor= resolver.query(Uri.parse(uriStr),new String[]{"_id","name","age"},null,null,"age");
        StringBuffer sb = new StringBuffer("============student==================\n");

            while (cursor !=null &&cursor.moveToNext()){

                long id = cursor.getLong(cursor.getColumnIndex("_id"));
                String name = cursor.getString(cursor.getColumnIndex("name"));
                int age = cursor.getInt(cursor.getColumnIndex("age"));
                sb.append("==="+id+"===\n");
                sb.append("name:"+name+"\n");
                sb.append("age:"+age+"\n");
            }
        sb.append("============================");
            tv.setText(sb.toString());
        if (cursor != null){
            cursor.close();
        }
    }

insert


private void insert() {
    String name = etName.getText().toString();
    String  age = etAge .getText().toString();

    ContentResolver resolver = getContentResolver();
    String uriStr="content://com.skymxc.demo/student";
    ContentValues cv = new ContentValues();
    cv.put("name",name);
    cv.put("age",age);
    resolver.insert(Uri.parse(uriStr),cv);
}

ContentResolver 还可以用来操作 短信,联系人,多媒体等 数据,这里写个读取短信的实例

读取短信的权限
“`xml

““

/**
 * 短信查询
 */
private void querySms() {
    String[] projection = new String[]{"_id","address","person","body","type"};
    StringBuffer sb = new StringBuffer("短信数据=============\n");
    ContentResolver resolver= getContentResolver();
    Cursor cursor = resolver.query(Uri.parse("content://sms/"),projection,null,null,null);
    while (cursor != null && cursor.moveToNext()){
        sb.append("id:"+cursor.getInt(cursor.getColumnIndex("_id")));
        sb.append("\naddress:"+cursor.getString(cursor.getColumnIndex("address")));
        sb.append("\nperson:"+cursor.getString(cursor.getColumnIndex("person")));
        sb.append("\nbody:"+cursor.getString(cursor.getColumnIndex("body")));
        sb.append("\ntype:"+cursor.getString(cursor.getColumnIndex("type")));
        sb.append("\n=================================================");
    }

    tv.setText(sb.toString());
}

ContentObserver

内容观察者,可以给某些数据注册观察者,当数据改变时做出有些操作

初始化观察者

private ContentObserver  observer = new ContentObserver(new Handler()) {
   @Override
   public void onChange(boolean selfChange) {
       super.onChange(selfChange);
       Log.e("MainActivity","======数据改变了===");
   }
};
````

> 注册观察者




<div class="se-preview-section-delimiter"></div>

```java

    Uri uri = Uri.parse("content://"+StudentProvider.AUTHORITY+"/student");
    //为student 注册观察者
    /**
     * parameter1 观察的uri
     * parameter2 uri的后代是否连带 观察
     * parameter3 observer
     */
    getContentResolver().registerContentObserver(uri,true,observer);

内容改变时 通知观察者

系统会首先查找 uri 扫描(手机上)所有的注册的observer 的uri 匹配之后执行 observer的onChange 方法

@Nullable
@Override
public Uri insert(Uri uri, ContentValues values) {
    SQLiteDatabase db = dbHelper.getDB();
    int line=0;
    switch (uriMatcher.match(uri)){
        case STUDENT:
            break;
        case STUDENTS:
          line= (int) db.insert(DBHelper.TABLE_NAME,"_id",values);


            break;
    }
    if (line>0){
        getContext().getContentResolver().notifyChange(uri,null);
    }

    return uri;
}

当在另一个应用插入数据时 change()调用

E/MainActivity: ======数据改变了===

作者:MXiaoChao 发表于2016/10/3 22:28:34 原文链接
阅读:93 评论:0 查看评论

[Android测试] AS+Appium+Java+Win 自动化测试之八:使用PageObject模式和重封装

$
0
0

一、 What? 什么是PageObject?

简称PO,这是一个设计模式,其实设计模式就是代码的架构,一个整体的框架。例如mvc 就是模型-视图-控制的一个代码架构,mvp就是-模型-视图-主持 这样的一个架构。PageObject翻译过来就是页面对象的意思,就是把页面对象和逻辑操作分开。结合封装,更加方便使用(不明白? 下面看demo)

二、 PageObject的好处

做UI自动化时定位特别依赖页面,一旦页面发生变更就不得不跟着去修改页面定位。
,假设你想对一个元素定位操作,你可能会编写下面的代码:

driver.findElement(By.id("comit")).click();

于是问题就出来了,这样代码冗余就高了。都写一块了,后期难维护。你有10个地方对这个元素做了这个操作,哪天这个控件的元素变了,你就不得不去修改10个地方。
那么po的好处就出来了,方便维护、减少代码冗余、逼格高

三、PO模式重封装

1.目录格式结构

  • base //放封装好的基类
  • cases //用例
  • opeartion //放逻辑处理
  • pages //页面元素处理

这里写图片描述

2. 重新封装

每层一个父类,driver使用静态,每个用例可以连续执行。封装类里面用到的Builder和Assertion在之前的文章已经贴出来了,这里就不再次贴了。

用例基类 InitAppium.java,每个用例继承这个类,直接加注解@Test,调用operate 层的方法即可

package com.example.base;

import org.apache.http.util.TextUtils;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.testng.annotations.AfterClass;
import org.testng.annotations.AfterTest;
import org.testng.annotations.BeforeSuite;
import org.testng.annotations.Listeners;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import io.appium.java_client.android.AndroidDriver;
import io.appium.java_client.android.AndroidElement;

/**
 * 测试用例的父类
 * Created by LITP on 2016/9/7.
 */
@Listeners({com.example.base.AssertionListener.class})
public class InitAppium {
    //调试设备名字
    public static String deviceName = "minote";
    //调试设备系统版本
    public static String platformVersion = "4.4.2";
    //app路径
    public static String appPath = System.getProperty("user.dir") + "/src/main/java/apps/shouhu2.2.3.apk";

    //包名
    public static String appPackage = "com.minstone.mdoctor";

    //是否需要重新安装
    public static String noReset = "True";

    //是否不重新签名
    public static String noSign = "True";

    //是否使用unicode输入法,真是支持中文
    public static String unicodeKeyboard = "True";

    //是否祸福默认呢输入法
    public static String resetKeyboard = "True";

    //要启动的Activity
    //public static String appActivity = appPackage + ".activity.login.WelcomeActivity";
    public static String appActivity = "";

    public static  AndroidDriver<AndroidElement> driver = null;


    //构造方法
    public InitAppium() {
        this(new Builder());
    }

    public InitAppium(Builder builder) {

        appActivity = builder.appActivity;
        appPackage = builder.appPackage;
        appPath = builder.appPath;
        deviceName = builder.deviceName;
        noReset = builder.noReset;
        noSign = builder.noSign;
        unicodeKeyboard = builder.unicodeKeyboard;
        resetKeyboard = builder.resetKeyboard;
    }

    /**
     * appium启动参数
     *
     * @throws MalformedURLException
     */
    @BeforeSuite
    public void beforeSuite() throws MalformedURLException {


        DesiredCapabilities capabilities = new DesiredCapabilities();

        capabilities.setCapability("deviceName", deviceName);
        capabilities.setCapability("platformVersion", platformVersion);
        capabilities.setCapability("app", new File(appPath).getAbsolutePath());
        capabilities.setCapability("appPackage", appPackage);
        //支持中文
        capabilities.setCapability("unicodeKeyboard", unicodeKeyboard);
        //运行完毕之后,变回系统的输入法
        capabilities.setCapability("resetKeyboard", resetKeyboard);
        //不重复安装
        capabilities.setCapability("noReset", noReset);
        //不重新签名
        capabilities.setCapability("noSign", noSign);
        //打开的activity
        if(!TextUtils.isEmpty(appActivity)){
            capabilities.setCapability("appActivity", appActivity);
        }

        //启动Driver
        driver = new AndroidDriver<>(new URL("http://127.0.0.1:4723/wd/hub"), capabilities);

    }


    @AfterTest
    public void afterTest() {
        driver.quit();
    }

    @AfterClass
    public void afterClass(){
        //每一个用例完毕结束这次测试
        //driver.quit();
    }

    /**
     * 打印字符
     *
     * @param str 要打印的字符
     */
    public <T> void print(T str) {
        if (!TextUtils.isEmpty(String.valueOf(str))) {
            System.out.println(str);
        } else {
            System.out.println("输出了空字符");
        }
    }

}

逻辑操作基类OperateAppium.java:,逻辑功能类继承这个类,在这里获取页面控件使用page层的对象

package com.example.base;

import org.apache.http.util.TextUtils;
import org.openqa.selenium.By;
import org.openqa.selenium.TimeoutException;

import java.util.List;
import java.util.concurrent.TimeUnit;

import io.appium.java_client.MultiTouchAction;
import io.appium.java_client.TouchAction;
import io.appium.java_client.android.AndroidDriver;
import io.appium.java_client.android.AndroidElement;

import static com.example.base.InitAppium.appPackage;
import static io.appium.java_client.android.AndroidKeyCode.BACKSPACE;
import static io.appium.java_client.android.AndroidKeyCode.KEYCODE_MOVE_END;

/**
 * 逻辑处理父类
 * Created by LITP on 2016/9/22.
 */

public class OperateAppium {

    AndroidDriver driver;

    //单个触摸操作类
    TouchAction touchAction;

    //多个触摸操作时间
    MultiTouchAction multiTouchAction;

    private static int WAIT_TIME = 10;    //默认的等待控件时间

    private final int SWIPE_DEFAULT_PERCENT = 5;   //默认滑动百分比

    public final String SWIP_UP = "UP",
            SWIP_DOWN = "DOWN",
            SWIP_LEFT = "LEFT",
            SWIP_RIGHT = "RIGHT";


    public OperateAppium(AndroidDriver androidDriver) {
        this.driver = androidDriver;
    }


    /**
     * 打印字符
     *
     * @param str 要打印的字符
     */
    public <T> void print(T str) {
        if (!TextUtils.isEmpty(String.valueOf(str))) {
            System.out.println(str);
        } else {
            System.out.println("输出了空字符");
        }
    }

    /**
     * Click点击空格键
     *
     * @param ae 要点击的控件
     * @return 返回是否点击
     */
    public boolean clickView(AndroidElement ae) {
        return clickView(ae, "");
    }


    /**
     * Click点击控件
     *
     * @param ae  控件
     * @param str 控件的文字描述,供错误时候输出
     * @return 返回是否存在控件
     */
    public boolean clickView(AndroidElement ae, String str) {
        if (ae != null) {
            ae.click();
            return true;
        } else {
            print(str + "为空,点击错误");
        }
        return false;
    }


    /**
     * 线程休眠秒数,单位秒
     *
     * @param s 要休眠的秒数
     */
    public void sleep(long s) {
        try {
            Thread.sleep(s);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


    /**
     * 获取当前的activity,返回文件名
     *
     * @return
     */
    public String getCurrActivity() {
        String str = driver.currentActivity();
        return str.substring(str.lastIndexOf(".") + 1);
    }

    /**
     * 获取触摸实例
     *
     * @return
     */
    public TouchAction getTouch() {
        if (driver == null) {
            print("单点触摸时候driver为空");
            return null;
        } else {
            if (touchAction == null) {
                return new TouchAction(driver);
            } else {
                return touchAction;
            }

        }
    }


    /**
     * 获取多点触摸实例
     *
     * @return
     */
    public MultiTouchAction getMultiTouch() {
        if (driver == null) {
            print("多点触摸时候driver为空");
            return null;
        } else {
            if (multiTouchAction == null) {
                return new MultiTouchAction(driver);
            } else {
                return multiTouchAction;
            }

        }
    }

    /**
     * 往控件输入字符串
     *
     * @param ae  要输入的控件
     * @param str 要输入的字符串
     */
    public void input(AndroidElement ae, String str) {
        if (ae == null) {
            print("控件为空,输入内容失败:" + str);
        } else {
            ae.sendKeys(str);
        }

    }

    public void swipeToUp(int during) {
        swipeToUp(during, SWIPE_DEFAULT_PERCENT);
    }

    /**
     * 向上滑动,
     *
     * @param during
     */
    public void swipeToUp(int during, int percent) {
        int width = getScreenWidth();
        int height = getScreenHeight();
        driver.swipe(width / 2, height * (percent - 1) / percent, width / 2, height / percent, during);
    }

    public void swipeToDown(int during) {
        swipeToDown(during, SWIPE_DEFAULT_PERCENT);
    }

    /**
     * 向下滑动,
     *
     * @param during 滑动时间
     */
    public void swipeToDown(int during, int percent) {
        int width = getScreenWidth();
        int height = getScreenHeight();
        driver.swipe(width / 2, height / percent, width / 2, height * (percent - 1) / percent, during);
    }


    public void swipeToLeft(int during) {
        swipeToLeft(during, SWIPE_DEFAULT_PERCENT);
    }

    /**
     * 向左滑动,
     *
     * @param during  滑动时间
     * @param percent 位置的百分比,2-10, 例如3就是 从2/3滑到1/3
     */
    public void swipeToLeft(int during, int percent) {
        int width = getScreenWidth();
        int height = getScreenHeight();
        driver.swipe(width * (percent - 1) / percent, height / 2, width / percent, height / 2, during);
    }


    public void swipeToRight(int during) {
        swipeToRight(during, SWIPE_DEFAULT_PERCENT);
    }

    /**
     * 向右滑动,
     *
     * @param during  滑动时间
     * @param percent 位置的百分比,2-10, 例如3就是 从1/3滑到2/3
     */
    public void swipeToRight(int during, int percent) {
        int width = getScreenWidth();
        int height = getScreenHeight();
        driver.swipe(width / percent, height / 2, width * (percent - 1) / percent, height / 2, during);
    }


    /**
     * 显示等待,等待Id对应的控件出现time秒,一出现马上返回,time秒不出现也返回
     */
    public AndroidElement waitAuto(By by, int time) {
        try {
            return new AndroidDriverWait(driver, time)
                    .until(new ExpectedCondition<AndroidElement>() {
                        @Override
                        public AndroidElement apply(AndroidDriver androidDriver) {
                            return (AndroidElement) androidDriver.findElement(by);
                        }
                    });
        } catch (TimeoutException e) {
            print("查找元素超时!! " + time + " 秒之后还没找到元素 [" + by.toString() + "]");
            return null;
        }
    }

    public AndroidElement waitAutoById(String id) {
        return waitAutoById(id, WAIT_TIME);
    }

    public AndroidElement waitAutoById(String id, int time) {
        return waitAuto(By.id(id), time);
    }

    public AndroidElement waitAutoByName(String name) {
        return waitAutoByName(name, WAIT_TIME);
    }

    public AndroidElement waitAutoByName(String name, int time) {
        return waitAuto(By.name(name), time);
    }


    public AndroidElement waitAutoByXp(String xPath) {
        return waitAutoByXp(xPath, WAIT_TIME);
    }

    public AndroidElement waitAutoByXp(String xPath, int time) {
        return waitAuto(By.xpath(xPath), time);
    }

    public void waitAuto() {
        waitAuto(WAIT_TIME);
    }

    /**
     * ,隐式等待,如果在指定时间内还是找不到下个元素则会报错停止脚本
     * 全局设定的,find控件找不到就会按照这个事件来等待
     *
     * @param time 要等待的时间
     */
    public void waitAuto(int time) {
        driver.manage().timeouts().implicitlyWait(time, TimeUnit.SECONDS);
    }

    /**
     * 打开Activity
     *
     * @param activityName activity的名字
     */
    public void startActivity(String activityName) {
        driver.startActivity(appPackage, activityName);
    }


    /**
     * 获取当前界面的所有EditText,并依次输入内容
     *
     * @param str 要输入的数组
     */
    public void inputManyText(String... str) {
        List<AndroidElement> textFieldsList = driver.findElementsByClassName("android.widget.EditText");
        for (int i = 0; i < str.length; i++) {
            textFieldsList.get(i).click();
            clearText(textFieldsList.get(i));   //清除内容
            textFieldsList.get(i).sendKeys(str[i]);
        }
    }

    /**
     * 点击屏幕中间
     */
    public void press() {
        press(getScreenWidth() / 2, getScreenHeight() / 2);
    }


    /**
     * 点击某个控件
     *
     * @param ae 要点击的控件
     */
    public void press(AndroidElement ae) {
        try {
            getTouch().tap(ae).perform();
        } catch (Exception e) {
            print("tab点击元素错误" + e.getMessage());
            e.printStackTrace();
        }
    }


    /**
     * 点击某个坐标
     *
     * @param x
     * @param y
     */
    public void press(int x, int y) {
        try {
            driver.tap(1, x, y, 500);
            //getTouch().tap(x, y).perform();
            print("tab点击位置(" + x + "," + y + ")");
        } catch (Exception e) {
            print("tab点击元素位置异常" + e.getMessage());
            e.printStackTrace();
        }
    }


    /**
     * 长按某个控件
     *
     * @param ae 要点击的控件
     */
    public void longPress(AndroidElement ae) {
        try {
            getTouch().longPress(ae).release().perform();
        } catch (Exception e) {
            print("长按点击元素错误" + e.getMessage());
            e.printStackTrace();
        }
    }


    /**
     * 长按某个坐标
     *
     * @param x
     * @param y
     */
    public void longPress(int x, int y) {
        try {
            getTouch().longPress(x, y).release().perform();
        } catch (Exception e) {
            print("长按点击元素错误" + e.getMessage());
            e.printStackTrace();
        }
    }


    /**
     * 在控件上滑动
     *
     * @param element   要滑动的控件
     * @param direction 方向,事件不设置默认1秒
     */
    public void swipOnElement(AndroidElement element, String direction) {
        swipOnElement(element, direction, 1000);  //不设置时间就为2秒
    }

    /**
     * 在某一个控件上滑动
     *
     * @param element   在那个元素上滑动
     * @param direction 方向,UP  DOWN LEFT RIGHT
     */
    public void swipOnElement(AndroidElement element, String direction, int duration) {
        //获取元素的起初xy,在左上角
        int x = element.getLocation().getX();
        int y = element.getLocation().getY();
        //获取元素的宽高
        int width = element.getSize().getWidth();
        int height = element.getSize().getHeight();

        switch (direction) {
            case SWIP_UP:
                int startX = x + width / 2;
                //在4/5的底部的中间向上滑动
                driver.swipe(startX, y + height * 4 / 5, startX, y + height / 5, duration);
                break;
            case SWIP_DOWN:
                startX = x + width / 2;
                //在4/5的底部的中间向上滑动
                driver.swipe(startX, y + height / 5, startX, y + height * 4 / 5, duration);
                break;

            case SWIP_LEFT:
                int startY = y + width / 2;
                driver.swipe(x + width * 4 / 5, startY, x + width / 5, startY, duration);
                break;
            case SWIP_RIGHT:
                startY = y + width / 2;
                driver.swipe(x + width / 5, startY, x + width * 4 / 5, startY, duration);
                break;
        }
    }

    /**
     * 在某个方向上滑动
     *
     * @param direction 方向,UP DOWN LEFT RIGHT
     * @param duration  持续时间
     */
    public void swip(String direction, int duration) {
        switch (direction) {
            case "UP":
                swipeToUp(duration);
                break;
            case "DOWN":
                swipeToDown(duration);
                break;
            case "LEFT":
                swipeToLeft(duration);
                break;
            case "RIGHT":
                swipeToRight(duration);
                break;
        }
    }


    /**
     * 在指定次数的条件下,某个方向滑动,直到这个元素出现
     *
     * @param by         控件
     * @param direction  方向,UP DOWN  LEFT RIGHT
     * @param duration   滑动一次持续时间
     * @param maxSwipNum 最大滑动次数
     */
    public void swipUtilElementAppear(By by, String direction, int duration, int maxSwipNum) {
        int i = maxSwipNum;
        Boolean flag = true;
        while (flag) {
            try {
                if (i <= 0) {
                    flag = false;
                }
                driver.findElement(by);
                flag = false;
            } catch (Exception e) {
                i--;
                swip(direction, duration);
            }
        }
    }

    /**
     * 在某个方向滑动直到这个元素出现
     *
     * @param by        控件
     * @param direction 方向,UP DOWN  LEFT RIGHT
     * @param duration  滑动一次持续时间
     */
    public void swipUtilElementAppear(By by, String direction, int duration) {
        Boolean flag = true;
        while (flag) {
            try {
                driver.findElement(by);
                flag = false;
            } catch (Exception e) {
                swip(direction, duration);
            }
        }
    }


    /**
     * 获取屏幕的宽高
     *
     * @return 返回宽高的数组
     */
    public int[] getScreen() {
        int width = driver.manage().window().getSize().getWidth();
        int height = driver.manage().window().getSize().getHeight();
        return new int[]{width, height};
    }

    /**
     * 获取屏幕宽度
     *
     * @return
     */
    public int getScreenWidth() {
        return driver.manage().window().getSize().getWidth();
    }

    /**
     * 获取屏幕高度
     *
     * @return
     */
    public int getScreenHeight() {
        return driver.manage().window().getSize().getHeight();
    }


    /**
     * 逐字删除编辑框中的文字
     *
     * @param element 文本框架控件
     */
    public void clearText(AndroidElement element) {
        String text = element.getText();
        //跳到最后
        driver.pressKeyCode(KEYCODE_MOVE_END);
        for (int i = 0; i < text.length(); i++) {
            //循环后退删除
            driver.pressKeyCode(BACKSPACE);
        }

    }



}

页面元素基类PageAppium.java:, 界面元素的存放获取类继承这个类,

package com.example.base;

import org.apache.http.util.TextUtils;
import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.TimeoutException;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

import io.appium.java_client.android.AndroidDriver;
import io.appium.java_client.android.AndroidElement;

import static com.example.base.InitAppium.appPackage;

/**
 * 页面UI获取定位父类,供给Page层使用
 * Created by LITP on 2016/9/23.
 */

public class PageAppium {

    AndroidDriver driver;

    private static int WAIT_TIME = 3;    //默认的等待控件时间



    public PageAppium(AndroidDriver androidDriver) {
        this.driver = androidDriver;
        waitAuto(WAIT_TIME);
    }

    public boolean isIdElementExist(String id) {
        return isIdElementExist(id, 0);
    }

    public boolean isIdElementExist(String id,int timeOut) {
        return isIdElementExist(id,timeOut ,false);
    }

    /**
     * 根据id判断当前界面是否存在并显示这个控件
     *
     * @param id     要查找的id
     * @param isShow 是否判断控件显示
     * @return 返回对应的控件
     */
    public boolean isIdElementExist(String id,int timeOut, boolean isShow) {
        return isElementExist(By.id(appPackage + ":id/" +id),timeOut,isShow);
    }

    /**
     * 选择当前界面的有这个文字的控件
     *
     * @param name
     * @return
     */
    public boolean isNameElementExist(String name) {
        return isNameElementExist(name, 0);
    }

    public boolean isNameElementExist(String name, int timeOut) {
        return isNameElementExist(name, timeOut,false);
    }

    public boolean isNameElementExist(String name, int timeOut, boolean isShow) {
        return isElementExist(By.name(name),timeOut, isShow);
    }



    /**
     * 判断当前界面有没有这个字符串存在
     *
     * @param text 要判断的字符串
     * @return 存在返回真
     */
    public boolean isTextExist(String text) {
        String str = driver.getPageSource();
        print(str);
        return str.contains(text);
    }


    /**
     * 判断当前界面有没有这个Xpath控件存在
     *
     * @param text 要判断的字符串
     * @return 存在返回真
     */
    public boolean isXpathExist(String text) {
        return isXpathExist(text,0);
    }

    public boolean isXpathExist(String text,int timeOut) {
        return isXpathExist(text,timeOut, false);
    }


    public boolean isXpathExist(String text,int timeOut,boolean isShow) {
        ////android.widget.TextView[@text='"+text+"']
        return isElementExist(By.xpath(text), timeOut,isShow);

    }



        /**
         * 判断控件时候存在
         *
         * @param by      By
         * @param timeout 等待的事件
         * @return
         */
    public boolean isElementExist(By by, int timeout,boolean isShow) {
        try {
            AndroidElement element = waitAuto(by, timeout);
            if(element == null){
                return false;
            }else{
                if(isShow){
                    return element.isDisplayed();
                }
            }

            return true;
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * 获取当前的activity,返回文件名
     *
     * @return
     */
    public String getCurrActivity() {
        String str = driver.currentActivity();
        return str.substring(str.lastIndexOf(".") + 1);
    }


    /**
     * 根据id获取当前界面的一个控件
     *
     * @param id 要查找的id
     * @return 返回对应的控件
     */
    public AndroidElement findById(String id,String desc) {
        return findElementBy(By.id(id),desc);
    }

    public AndroidElement findById(String id) {
        return findElementBy(By.id(id),"");
    }


    public AndroidElement findByFullId(String id) {
        try {
            if (driver != null) {
                return (AndroidElement) driver.findElement(By.id(id));
            } else {
                print("driver为空");
            }
        } catch (NoSuchElementException e) {
            print("找不到控件:" +" 异常信息:"+ e.getMessage());

        }

        return null;
    }


    /**
     * 选择当前界面的有这个文字的控件
     *
     * @param name 内容
     * @return 找到的控件
     */
    public AndroidElement findByName(String name,String desc) {
        return findElementBy(By.name(name),desc);
    }
    public AndroidElement findByName(String name) {
        return findByName(name,"");
    }

    /**
     * 根据id获取当前界面的一个控件
     *
     * @param name 要查找的控件的类名
     * @return 返回对应的控件
     */
    public AndroidElement findByClassName(String name,String desc) {
        return findElementBy(By.className(name),desc);
    }
    public AndroidElement findByClassName(String name) {
        return findByClassName(name,"");
    }


    public AndroidElement findByXpath(String str) {
        return findByXpath(str,"");
    }
    public AndroidElement findByXpath(String str,String desc) {
        return findElementBy(By.xpath(str),desc);
    }


    public AndroidElement findElementBy(By by){
        return findElementBy(by,"");
    }

        /**
         * 获取控件
         * @param by by
         * @param str 报错提示信息
         * @return
         */
    public AndroidElement findElementBy(By by,String str){
        try {
            if (driver != null) {
                return (AndroidElement) driver.findElement(by);
            } else {
                print("driver为空");
            }
        } catch (NoSuchElementException e) {
            print("找不到控件:" +str+" 异常信息:"+ e.getMessage());

        }

        return null;
    }

    /**
     * 打印字符
     *
     * @param str 要打印的字符
     */
    public <T> void print(T str) {
        if (!TextUtils.isEmpty(String.valueOf(str))) {
            System.out.println(str);
        } else {
            System.out.println("输出了空字符");
        }
    }


    /**
     * 线程休眠秒数,单位秒
     *
     * @param s 要休眠的秒数
     */
    public void sleep(long s) throws InterruptedException {
        Thread.sleep(s);
    }


    /**
     * 显示等待,等待Id对应的控件出现time秒,一出现马上返回,time秒不出现也返回
     */
    public AndroidElement waitAuto(By by, int time) {
        try {
            return new AndroidDriverWait(driver, time)
                    .until(new ExpectedCondition<AndroidElement>() {
                        @Override
                        public AndroidElement apply(AndroidDriver androidDriver) {
                            return (AndroidElement) androidDriver.findElement(by);
                        }
                    });
        } catch (TimeoutException e) {
            return null;
        }
    }

    public AndroidElement waitAutoById(String id) {
        return waitAutoById(id, WAIT_TIME);
    }

    public AndroidElement waitAutoById(String id, int time) {
        return waitAuto(By.id(id), time);
    }

    public AndroidElement waitAutoByName(String name) {
        return waitAutoByName(name, WAIT_TIME);
    }

    public AndroidElement waitAutoByName(String name, int time) {
        return waitAuto(By.name(name), time);
    }


    public AndroidElement waitAutoByXp(String xPath) {
        return waitAutoByXp(xPath, WAIT_TIME);
    }

    public AndroidElement waitAutoByXp(String xPath, int time) {
        return waitAuto(By.xpath(xPath), time);
    }

    public void waitAuto() {
        waitAuto(WAIT_TIME);
    }

    /**
     * ,隐式等待,如果在指定时间内还是找不到下个元素则会报错停止脚本
     * 全局设定的,find控件找不到就会按照这个事件来等待
     *
     * @param time 要等待的时间
     */
    public void waitAuto(int time) {
        driver.manage().timeouts().implicitlyWait(time, TimeUnit.SECONDS);
    }

    /**
     * 获取屏幕的宽高
     *
     * @return 返回宽高的数组
     */
    public int[] getScreen() {
        int width = driver.manage().window().getSize().getWidth();
        int height = driver.manage().window().getSize().getHeight();
        return new int[]{width, height};
    }

    /**
     * 获取屏幕宽度
     *
     * @return
     */
    public int getScreenWidth() {
        return driver.manage().window().getSize().getWidth();
    }

    /**
     * 获取屏幕高度
     *
     * @return
     */
    public int getScreenHeight() {
        return driver.manage().window().getSize().getHeight();
    }


    /**
     * 根据ClassName获取多个控件
     *
     * @param className 控件的类名字,例如 android.widget.EditText
     * @param num       返回的数量
     * @return
     */
    public List<AndroidElement> getManyElementByClassName(String className, int num) {
        List<AndroidElement> textFieldsList = driver.findElementsByClassName(className);
        List<AndroidElement> list = new ArrayList<>();
        try{
            for(int i=0; i<num; i++){
                list.add(textFieldsList.get(i));
            }
            return list;
        }catch (Exception e){
            print("获取多个控件异常"+e.getMessage());
        }
        return null;

    }

    /**
     * 根据Id获取多个控件
     *
     * @param id 控件的类名字,例如 android.widget.EditText
     * @param num       返回的数量
     * @return
     */
    public List<AndroidElement> getManyElementById(String id, int num) {
        if(driver != null){
            List<AndroidElement> textFieldsList = driver.findElementsById(id);
            List<AndroidElement> list = new ArrayList<>();
            try{
                for(int i=0; i<num; i++){
                    list.add(textFieldsList.get(i));
                }
                return list;
            }catch (Exception e){
                print("获取多个控件异常"+e.getMessage());
            }
        }else{
            print("获取多个控件"+id+"时候driver为空");
        }

        return null;

    }

    /**
     * 获取同id的list的控件
     * @param id id
     * @param num 取那一个控件
     * @return
     */
    public AndroidElement getListOneElementById(String id,int num){
        if(driver != null){
            try{
                return (AndroidElement) driver.findElementsById(appPackage+":id/"+id).get(num);
            }catch (Exception e){
                print("getListOneElementById找不到第"+num+"个控件"+id);
                return null;
            }
        }else{
            print("getListOneElementById:"+id+" 时候driver为空");
            return null;
        }
    }

}

下一篇讲解案例

作者:niubitianping 发表于2016/10/3 23:05:12 原文链接
阅读:101 评论:0 查看评论

Android开源项目及库

$
0
0

目录

具体内容 =============================

UI
卫星菜单
  • android-satellite-menu - 点击主按钮,会弹出多个围绕着主按钮排列的子按钮,从而形成一个弹出式菜单。子按钮弹出和消失的动画效果都很棒。这种弹出式菜单按钮应用在Path app中。
  • ArcMenu - 实现弹出式按钮群(菜单)。点击主按钮,会在住按钮旁边弹出多个按钮(菜单)。弹出的按钮有两种排列形式,一种是围绕着主按钮成圆弧形排列,一种是和主按钮并排成一字型排列, 仿Path 2.0 (for iOS)。
  • Radial Menu Widget - 实现各种圆形或者半圆形菜单,以及圆形进度条。
  • android-circlebutton - 圆形按钮,有动画点击效果。
  • CircularFloatingActionMenu - 卫星菜单。
  • ElasticDownload - 挺酷的下载进度条。
  • android-snake-menu - 仿 Tumblr 的 Android 可拖拽蛇形动画菜单。
节选器
  • SegmentView - 类似iOS的Segment Control控件,第一种方式是使用 RadioGroup 实现,O网页链接。
  • SHSegmentControl - 类似iOS的Segment Control控件,此种方式的可定制化更好。
  • android-segmentedradiobutton - 在Android中实现类似iOS的分段单选按钮(segmented control),本人以前项目一直使用,值得拥有。
  • android-segmented-control - RadioGroup实现类似ios的分段选择(UISegmentedControl)控件。
下拉刷新
  • Android-Ptr-Comparison - Android 下拉刷新开源库对比,非常nice!!
  • Android-PullToRefresh - 最经典、最多人用的下拉刷新、加载更多。
  • PullDownListView - 一个下拉刷新的控件,实现了仿微信下拉中眼睛动画的效果。
  • DragTopLayout - 实现整个layout下拉刷新。
  • ZrcListView - 一个顺滑又漂亮的Android下拉刷新与加载更多列表组件,增加下拉刷新及滚动到底部自动加载的功能;增加越界回弹效果;增加自定义列表项动画的功能。
  • TwitterCover-Android - Twitter Android客户端的下拉封面模糊效果。
  • android-Ultra-Pull-To-Refresh - 实现整个layout下拉刷新,没有加载更过,Demo, 源码分析
  • StikkyHeader - 【Android控件源码:头部固定的控件列表效果】这是一个可以支持头部固定的控件列表功能,源码StikkyHeader,StikkyHeader是一个可以在滚动的时候将头部固定的控件,还可以将动画效果和StikkyHeader一起使用,api非常简单, 支持ListView,RecyclerView,ScrollView。支持2.3一下设备使用的StikkyHeader
  • PullDownListView - 实现了模仿微信眼睛下拉效果,源码PullDownListView,下拉刷新,上拉加载,模仿微信眼睛。
  • CircleRefreshLayout - 又一个下拉刷新的实现,水滴效果。
  • BGARefreshLayout-Android - 多种下拉刷新效果、上拉加载更多、可配置自定义头部广告位,目前已经实现了四种下拉刷新效果:新浪微博下拉刷新风格、慕课网下拉刷新风格、美团下拉刷新风格、类似qq好友列表黏性下拉刷新风格。
  • Pull-to-Refresh.Rentals-Android - 提供一个简单可以自定义的下拉刷新实现。
  • Pull-to-Refresh.Tours - Taurus,很精美的下拉刷新。
  • ParallaxListView - 模仿Path的下拉刷新,Head头部图片下拉放大。
  • WaveRefreshForAndroid - 下拉刷新水波纹动画。
  • CoordinatorLayoutDemos - 收集了不少资源写了一个基于CoordinatorLayout实现的下拉刷新效果。
  • Android_PullToRefreshLibrary_Collection - 下拉刷新开源库集锦 。
  • HitBlockRefresh - 下拉刷新:打砖块和打坦克。
模糊效果
HUD与Toast
  • android-UCToast - 在不申请任何权限的情况下在 Android 应用中弹出悬浮窗,实现文档
  • sweet-alert-dialog - sweet-alert-dialog是一款清新文艺的 Android 弹窗, 灵感来自于 JS 版的 SweetAlert。
进度条
  • easyloadingbtn - 模仿了一个Dribbble上的Material Design效果,环形loading, 进度条、进度圈。
  • android-square-progressbar - 一个不错的方形进度条。
  • Radial Menu Widget - 实现各种圆形或者半圆形菜单,以及圆形进度条。
  • AnimatedCircleLoadingView - 一个有限/无限加载动画效果。基于Nils Banner的android-watch-loading-animation设计图。该设计本来是针对智能手表的。
  • circular-progress-button - 带动态效果的Button(按钮)可要比静态的按钮炫酷的多了,大家看到效果图就知道了。
  • CircularBarPager - Android实现的动态效果,一个数字圆圈进度效果,源码CircularBarPager,material 风格的数字圆圈进度显示库(api10 +)。
  • dotted-progress-bar - 一个小清新的进度条。
  • WhorlView - 一个炫酷的漩涡加载效果自定义View。
  • AVLoadingIndicatorView - AVLoadingIndicatorView整合了一些漂亮的 Android 动画加载效果。
  • MagicProgressWidget - 渐变的圆形进度条与轻量横向进度条。
  • GBSlideBar - GBSlideBar类似uber/滴滴等app的滑动选择工具条。
  • GifLoadingView - 一些好看的 loadingview。
  • HouseLoading - 一个有趣的android加载loading动画。实现原理

UI其他
  • MixtureTextView - 富文本,支持Android图文混排、文字环绕图片等效果。
  • android-ActionQueue - Action Queue 用于执行有次序的队列操作,比如按次序弹出对话框,这在 Android 中尤其有用。
  • WheelView-Android - WheelView-Android 是一款开源的 Android 滚动选择控件, 适用于不少应用场景。
  • Android Wheel - 带有刻度的旋转器:日历、三级联动。
  • CharacterPickerView - 可实现三级联动的选择器,高仿iOS的滚轮控件,可实现单项选择,并支持一二三级联动效果。
  • Highlight - Highlight一款可应用于 Android 应用上的指向性功能高亮的库, 可以快速的给应用添加上应用引导的效果。
  • HeaderAndFooterRecyclerView - 支持addHeaderView、 addFooterView、分页加载的RecyclerView解决方案 。
  • CleverRecyclerView - 是一个基于RecyclerView的扩展库,提供了与ViewPager类似的滑动效果并且添加了一些有用的特性。
  • drag-select-recyclerview - 实现了类似 Google Photos 风格的图片多选效果。
  • FlycoTabLayout - 一个Android TabLayout库,目前有两个TabLayout:SlidingTabLayout、CommonTabLayout。
  • AndroidChangeSkin - 一种完全无侵入的 Android 应用换肤方式,支持插件式和应用内换肤,无需重启 Activity。
  • Lobsterpicker - Lobsterpicker 为 Android 开发者提供了满足 Material Design 风格的颜色选择器。
  • FlycoRoundView - 一个扩展原生控件支持圆角矩形框背景的库,可以减少相关shape资源文件使用。
  • FlowingDrawer - FlowingDrawer 一个弹性效果的抽屉菜单,图片是概念图,实际效果实现了70%(侧滑菜单)。
  • TextSurface -是用 Java 写的一款借助酷炫的动画效果来完成消息展示的微型动画框架。
  • android-animate-RichEditor -android-animate-RichEditor是一款支持图片插入动画效果的 Android 富文本编辑器。
  • FlycoPageIndicator - android-animate-RichEditor是一款支持图片插入动画效果的 Android 富文本编辑器。
  • AndroidMosaicLayout - 马赛克效果 Layout,磁片风格View 自适应大小。
  • DropDownMenu - 一个实用的多条件筛选菜单,在很多App上都能看到这个效果,如美团,爱奇艺电影票等。
  • Swipe-Deck - 仿 Tinder 的可以左右滑动消除卡片效果的自定义控件。
  • IntlPhoneInput - 一个支持国际化的电话号码输入的自定义控件。
  • AndroidUI4Web - AndroidUI4Web是一个高性能的WebApp框架, 在移动浏览器上有与原生App一致的体验。
  • SmoothCheckBox - SmoothCheckBox带有切换动画的CheckBox。
  • AndroidTimelineViewx - AndroidTimelineViewx仿微信朋友圈 时间轴。
  • CityPicker - CityPicker仿美团等选择城市列表。
  • material-intro - Material Design 风格的引导页。
  • EmphasisTextView - 支持部分文字高亮的 TextView。
  • greedo-layout-for-android - 深度定制的 LayoutManager,在显示网格布局的时候会考虑屏幕宽高比。
  • Rosie - 可以让你创建遵循 Clean Architecture 的应用的框架。
  • CreditCardView - 一个交互很赞的信用卡自定义 View。
  • android-md-core - Material风格bootstrap的框架。
  • SwipeCardView - 一个带渐变层叠动画的左右滑动效果(类似于探探左右刷脸)。
  • SwipeSelector - 可以左右滑动切换 item 的 Selector。
  • ForegroundViews - 类似于 FrameLayout 的支持的前景自定义 View。
  • android-material-chips - Material Design 的 Chips 控件实现。
  • XhsEmoticonsKeyboard - 表情键盘解决方案。
  • JKeyboardPanelSwitch - 一套 Android 键盘面板冲突, 布局闪动的处理方案。
  • GestureLibray - 九宫格解锁。
  • RecyclerItemDecoration - RecyclerView相关的ItemDecorstion仍然保持高度定制性,易用性。
  • materiallogindemo - 一个炫酷的Material Design 风格的登录和注册页面 。教程

动画

  • Android应用开发之所有动画使用详解 - Android应用开发之所有动画使用详解。
  • 动画特效大全 - Android 动画特效大全。
  • SwitchLayout - 国内开发者, Android的Activity切换动画特效库SwitchLayout,视图切换动画库,媲美IOS。
  • ActivityOptionsICS - 一个低版本activity动画兼容库——ActivityOptionsICS,可以很好的实现MD的动画效果。
  • SwipeBack - 一个可以通过手势返回到上一个Activity的开源库,支持上下左右四个方向返回,支持多个View为Child。
  • SpringIndicator - 模仿Morning Routine的引导页效果SpringIndicator;基于模仿红点拖拽的Demo实现:BezierDemo;sample中使用到 快速创建ViewPager和ListView等的第三方库:MultipleModel
  • XhsWelcomeAnim - 国内开发者, 华丽酷炫欢迎引导界面 动画没有之一。
  • Material-Animations - Material风格动画,可以定义两个Activity之间的动画。
  • android-shapeLoadingView - android-shapeLoadingView实现高仿新版58 加载动画,loading。
  • 一个绚丽的loading - 一个绚丽的loading动效分析与实现。
  • TransitionPlayer - 一个 Transition 动画控制控制库,可以让你很轻松的创建一个可交互的动画。
  • loading-balls - loading-balls 一款支持高度配置的 Android 加载进度球。
  • SogoLoading - 仿搜狗浏览器加载动画,实现说明
  • ExplosionField - Android中View 炸裂特效的实现分析
  • AZExplosion - AZExplosion:模仿ExplosionField的粒子破碎效果。
  • BrokenView -玻璃碎裂动画效果。
  • SwipeCardView - SwipeCardView一个带渐变层叠动画的左右滑动效果(类似于探探左右刷脸)。类似SwipeCard
  • CRAudioVisualizationView - 水波纹效果的声音可视化自定义 View。
  • LoadingDrawable - 一些酷炫的加载动画, 可以与任何View配合使用,作为加载动画或者Progressbar, 此外很适合与RecyclerRefreshLayout 配合使用作为刷新的loading 动画。
  • Depth-LIB-Android- - 一款酷炫的 Android 界面过渡动画效果。

网络相关

网络连接
  • ion - 一个异步网络请求和图片加载的库,一个库能搞定几乎所有的网络请求。
  • 多线程下载 - Android 实现多线程下载 完美代码。
  • opandroid - android p2p的开源实现。
  • okio - square出的Okio这个库,尤其擅长处理二进制数据。如果觉得Java的输入输出流实在太复杂啰嗦,不妨试试Okio。
  • okhttp - square出的okhttp库。
  • OkHttpPlus - OkHttp 的一个工具类开源项目OkHttpPlus——支持GET、POST、UI线程回调、JSON格式解析、链式调用、文件上传下载 ,OkHttpPlus介绍
  • Android-Download-Manager-Pro - 一个下载管理库,如果你的 App 有大量的下载工作,这个库能帮到你。
  • FileDownloader - 文件下载引擎,稳定、高效、简单易用。
  • jchat-android - 一个聊天 App,具有完备的即时通讯功能,JChat 的功能基于极光 JMessage SDK 来开发。

网络测试
  • augmented-traffic-control - Facebook宣布开源移动网络测试工具ATC,该工具支持利用Wi-Fi网络模拟2G、2.5G、3G以及LTE 4G移动网络环境,让测试工程师们能够快速对智能手机和App在不同国家地区和应用环境下的性能表现进行测试。

图像获取

响应式编程

地图

  • 百度地图 - Android百度地图 线路规划,模拟运动轨迹,及全景效果。
  • AirMapView - 支持多个本地地图提供者包括谷歌地图V2和亚马逊地图V2。如果设备没有任何受支持的本地地图提供者,AirMapView会回退到基于web的地图提供者(目前谷歌地图)。

数据库

  • ORMLite - ORMLite做的最棒但是学习成本有点儿高,ORMLite的文档有点儿烂。
  • SugarORM - SugarORM比较轻便, 支持Has a 和 Has many映射,但无法保存集合,没有映射关系。
  • GreenDAO - GreenDAO要先建立一个java项目来生成对应的表,一变动又要生成,很不方便。
  • ActiveDriod - ActiveDriod也不错 官网
  • ORMDroid - ormdroid 。
  • sqlbrite - 良心企业Square的又一开源项目,当你不想给用ContentProvider,只想简单监听SQLite表增删改的数据变更时可以试试它。
  • sqlbrite - DBExecutor android ORM数据库 1.使用了读写锁,支持多线程操作数据。 2.支持操作多个数据库 3.支持事务 4.缓存Sql,缓存表结构。
  • Iron - 一个快速和易用的 NoSQL 数据存储框架。
  • hawk - 一个快速和易用的键值对数据存储框架,支持AES加密,支持SharedPreferences或Sqlite存储,支持Gson解析。
  • AndroidKeyValueStore - 一个基于 SQLite 的 Key/Value 存储框架。
  • DBFlow - 一个速度极快,功能强大,而且非常简单的 Android 数据库 ORM 库,为你编写数据库代码,DBFlow 已被证明是最好的解决方案。5 个顶级 Android 开源库

图像浏览及处理

  • MPAndroidChart - MPAndroidChart是一个功能强大的图表开源类库:曲线图、柱形图、环形图。
  • XCL-Charts - (国人开发)基于Android Canvas来绘制各种图表,使用简便,定制灵活。
  • WilliamChart - 绘制图表的库,支持LineChartView、BarChartView和StackBarChartView三中图表类型,并且支持 Android 2.2及以上的系统。
  • CropImageView - 原生ImageView只支持centerCrop,这里有支持9个方向裁剪的ImageView。
  • SimpleCropView - 一个Android的图片裁剪库,使用简单,易于定制。
  • DrawableView - DrawableView实现画板功能,可以改变画笔粗细,颜色,支持撤销功能。
  • ImageCoverFlow - ImageCoverFlow效果不错的画廊控件 可以设置画廊一次可见图片的张数,和其他第三方Gallery控件不同的是,该控件直接继承自View,而不是sdk中的Gallery控件。
  • FancyCoverFlow - 支持Item切换动画效果的类似Gallery View。改进版本可以无限轮播,可以选择自动轮播或者 手动滑动。
  • BGABanner-Android - demo中演示了引导页、以及通过fresco、android-async-http、gson实现广告条的自动轮播效果(splash 、 ViewPager切换动画) 。
  • RecyclerViewPager - 重写后的 RecyclerViewPager 完全继承自RecyclerView,可以自定义触发翻页的距离,可自定义翻页速度,支持VerticalViewPager,支持Fragment。
  • StickerCamera - 可以说是一个完整的相机、图片编辑的 APP,集成了大部分市面上有的同类 APP 的功能,裁剪、滤镜、贴纸应有尽有。
  • demo6_PhotoRiver - 图片流动显示的demo,可以点击流动中的图片放大显示,双击空白处图片以九宫格排列。
  • glide-transformations - 一个基于Glide的transformation库,拥有裁剪,着色,模糊,滤镜等多种转换效果。
  • ColoringLoading - 一个用纯代码实现自动绘画效果动画的项目。
  • SmartDrawing - 一个轻量级的手绘板,加入了一点截图功能。这只是一个Demo。并不能作为类库,也不是完整的项目工程,仅供学习或参考使用。
  • SlidingCard - 漂亮的卡片滑动翻页特效。
  • LargeImage - 加载大图 可以高清显示10000*10000像素的图片。
  • GalleryFinal - 自定义相册,实现了拍照、图片选择(单选/多选)、 裁剪(单/多裁剪)、旋转、ImageLoader无绑定任由开发者选 择、功能可配置、主题样式可配置。GalleryFinal为你定制相册。
  • AndroidAlbum - AndroidAlbum图片选择器:1、MVP结构设计;2、工厂模式对载图框架进行封装抽象,方便替换其他载图框架;3、闪退日志的搜集,方便揪BUG。
  • uCrop - uCrop 是 Yalantis 推出的又一款力作, 用于裁剪 Android 系统上的图片, 致力于打造最佳的图片裁剪体验。
  • crop-image-layout - crop-image-layout:图片裁切布局。
  • RenderscriptHistogramEqualization - RenderScript :简单而快速的图像处理

视频音频处理

  • ijkplayer - B站开源的视频播放器,支持Android和iOS。
  • DanmakuFlameMaster - 这里是Android上最好的开源弹幕引擎·烈焰弹幕使。
  • YouTubePlayerActivity - 一个可以播放YouTube视频的Activity,支持屏幕旋转、声音控制、播放失败处理、可以自定义Activity关闭动画以及在横屏播放的时候自动隐藏status bar。
  • AndroidVideoPlayer - 开源的 Android 视频播放器,支持 DLNA。
  • Hide-Music-Player - Hide音乐播放器。
  • JamsMusicPlayer - 是一个功能强大的 Android 开源播放器, 作者将原本收费的项目拿出来开源, 实在令人敬佩。
  • RxAndroidAudior - RxAndroidAudior目前最鲁棒的Android声音录制和播放封装库了,说明
  • Timber - 一款遵循了Material Design并且设计精美的播放器 Timber Music Player,已经在google play上架。
  • LandscapeVideoCamera - 一款功能强大的 Android 视频录制库, 仅允许横屏录制, 提供细粒度控制视频的质量与文件大小。

测试及调试

  • DevelopQuickSetting - 快速开启关闭开发者设置的工具,提供了app界面和桌面widget,能快速打开关闭overdraw,layout border,gpu rendering,adb wifi,不保存activity实例等功能。
  • decompileandroid - 在线反编译apk文件。
  • jadx - 一个Android反编译神器,不同于常见的dex2jar,这个反编译器生成代码的try/catch次数更少,View也不再是数字id了,可读性更高。
  • Androguard - Androguard使用Python写的一系列逆向工具集,功能很强大哦,对逆向工程感兴趣的小伙伴可以这个系列,教程
  • logger - 一个简单、漂亮、功能强大的Android日志程序。
  • stf - WEB 端批量移动设备管理控制工具 STF 的环境搭建和运行,使用说明 。
  • DecompileApk - 一键反编译 APK,输出所有反编译后的代码、资源,使用方便。
  • AppCrashTracker - 一个异常追踪器,可以生成一个 JSON 格式的日志并可以上传到服务器。

动态更新热更新

消息推送与及时通讯

客户端
服务器端

完整项目

  • SuesNews新闻客户端 - 腾飞新闻,一个符合 Google Material Design 的 Android 校园新闻客户端 ,新闻客户端说明 。
  • 新闻客户端 - Android应用源码比较不错的新闻客户端,本项目启动引导登录注册用户中心列表显示文章分页下拉刷新文章收藏更新反馈等新闻客户端常见的功能都有,项目分层合理,代码质量较高。
  • materialistic - Material Desgin风格的Hacker News客户端。
  • Telegram - Telegram 是一款专注于速度、安全的短信息应用,快速、简单、免费。Telegram 支持群组聊天,最高200人,最高支持分享1GB的视频,其它图片等等更是不在话下。而且所有信息全部支持同步。由于频发的隐私问题,所以 Telegram 也很注重通信安全。
  • SuZhouTong-client-for-android - 苏州通android客户端,非常多的UI效果。
  • ele_demo - 仿【饿了么】订餐软件的一个demo。
  • MD-BiliBili - Material Design 版 BiliBili Android 客户端。
  • AisenWeiBo - Aisen微博是新浪微博的第三方客户端,UI遵循Material Design:遵循Material Design、发布多图、离线下载、私信(触屏版、颜色主题切换、手势返回,4.4、5.0状态栏变色、离线编辑,定时发布多图、gif、长微博预览。FrescoDemo 。
  • 快递查询 - 使用了爱查快递www.ickd.cn的api接口,可以查询申通、EMS、顺风、圆通、中通、韵达、天天、汇通、全锋、德邦、宅急送等11种快递的单号信息,支持手动输入单号和扫描单号(红米测试的时候扫描单号有点问题),可以保存单号查询记录方便下次查询,,另外还包括了网络状态判断、快递自动更新、软件更新等功能,项目完美运行,有很详细的中文注释和逻辑分层。
  • SmartCall - SmartCall Android 企业通讯录。
  • Android-高仿大众点评客户端源码 - Android-高仿大众点评客户端源码。
  • 八个Android项目源码 - 八个Android项目源码,大部分功能相信可以在实战项目中直接使用,供大家下载学习,大部分项目是基于Android Studio开发,IDE为Eclipse的童鞋可通过网上教程自行转换,这里就不多说了。大家可以下载下来学习看看! 百度云盘下载地址
  • minicat - 一个简洁的饭否App,支持Android 4.0以上版本。
  • SimplifyReader - 一款基于Google Material Design设计开发的Android客户端,包括新闻简读,图片浏览,视频爽看 ,音乐轻听以及二维码扫描五个子模块。
  • GithubTrends - 是一个用来看查看 GitHub 热门项目的 Android App, 遵循 Material Design, 支持订阅 50 多种编程语言, 9 种颜色主题切换, 可在上面收藏喜欢的项目。
  • jianshi - jianshi简诗是国人开发的一个用于记录文字信息的 Android 完整应用, 作者仅用了一天便将其开发出来, 并将开发的流程记录成文放到了简书上。
  • BuildingBlocks - 积木: 一个以知乎日报作为数据展现内容;以抽屉菜单作为功能扩展入口;依循 Material Design 作为主导设计 UI 的应用。
  • Douya - 开源的 Material Design 豆瓣客户端。
  • TranslateApp - 一个实现『划词翻译』功能的 Android 开源应用。

插件

  • Android Studio 插件和工具 - 5个 推荐几个有用的 Android Studio 插件和工具(ButterKnife、selectorChapek、GsonFormat、ParcelableGenerator、LeakCanary)。
  • 8 个最优秀的 Android Studio 插件 - 8 个最优秀的 Android Studio 插件(H.A.X.M(硬件加速执行管理器)、Genymotion、Android Drawable Importer、Android ButterKnife Zelezny、Android Holo Colors Generator、Robotium Recorder、jimu Mirror、Strings-xml-tools)。
  • smalidea - 一款 IntelliJ IDEA/Android Studio 的 smali 插件~ ,Smalidea 无源码调试 Android 应用
  • gradle-fir-plugin - 一个上传apk到fir的gradle插件,使用说明
  • android-butterknife-zelezny - 一个ButterKnife的Android Studio插件, 该插件可以让你手动生成上述注入代码。
  • GradleDependenciesHelperPlugin - Gradle 依赖自动补全插件。
  • android-selector-intellij-plugin - 可以根据指定颜色生成Selector Drawable的插件。
  • 7个最佳的Android模拟器 - 7个最佳的Android模拟器。
  • gradle-android-javadoc-plugin - 可以生成 java doc 的 Gradle 插件。
  • gradle-android-junit-jacoco-plugin - 可以生成代码单元测试覆盖率报告的 Gradle 插件。
  • gradle-android-apk-size-plugin - 可以将 Apk 大小记录到 CSV 文件的 Gradle 插件。
  • 几款实用的Android Studio 插件 - 几款实用的Android Studio 插件:1、android-butterknife-zelezny;2、Gsonformat:可根据json数据快速生成java实体类;3、Android Postfix Completion;4、AndroidAccessors;5、Lifecycle Sorter:根据Activity或者fragment的生命周期对其生命周期方法位置进行先后排序,快捷键Ctrl + alt + K;6、JsonOnlineViewer;7、CodeGlance;8、findBugs-IDEA:帮你一起找bug的;9、ADB WIFI:使用wifi无线调试你的app,无需root权限。
  • Leisure - 闲暇(Leisure)是一款集"知乎日报"、“果壳科学人”、“新华网新闻”以及“豆瓣图书”于一体的阅读类Android应用。 果壳、知乎和豆瓣在国内拥有大量用户,这些社区的用户每天都产生很多高质量内容。闲暇以其简介的风格将这几大社区 的优质内容整合于一体,使得用户能有效地获取这些内容,大大节省了用户的时间。酷安下载地址.
  • LayoutFormatter插件 - 可自动将乱序的布局文件进行重新属性排序并格式化,比如 style 和 android:id 必须排在前面,紧接着 layout、padding,而值设定如 text 只能排在最后。

出名框架

  • xUtils - xUtils 包含了很多实用的android工具。支持大文件上传,更全面的http请求协议支持(10种谓词),拥有更加灵活的ORM,更多的事件注解支持且不受混淆影响。最低兼容android 2.2 (api level 8)。目前xUtils主要有四大模块:DbUtils模块、ViewUtils模块、HttpUtils模块、BitmapUtils模块。
  • afinal - Afinal是一个android的ioc,orm框架,内置了四大模块功能:FinalAcitivity,FinalBitmap,FinalDb,FinalHttp。
  • ButterKnife - ButterKnife是一个专注于Android系统的View注入框架,让你从此从这些烦人臃肿的代码中解脱出来,ButterKnife--View注入框架5 个顶级 Android 开源库
  • EventBus - EventBus是一款针对Android优化的发布/订阅事件总线。主要功能是替代Intent,Handler,BroadCast在Fragment,Activity,Service,线程之间传递消息.优点是开销小,代码更优雅。以及将发送者和接收者解耦。xBus - xBus - 简洁的EventBus实现。
  • Small - 做最轻巧的跨平台插件化框架,目前已支持Android、iOS以及html5插件。并且三者之间可以通过同一套javascript接口进行通信。
  • LayoutCast - LayoutCast可以在应用不重启的情况下,将res文件夹下的改动直接同步到手机上。使用LayoutCast,可以节约Android开发者的大量编译等待时间,非常适合真机调试界面的时候使用,推荐每一位开发者安装该利器。BUCK很快,但入侵性强,项目改动大,LayoutCast对项目改动小。
  • retrofit - retrofit将 REST API 转换为 Java 接口。5 个顶级 Android 开源库
  • Dagger2 - Dagger 2 是著名的依赖注入库 Dagger 的继承者,我们强烈推荐它。文档5 个顶级 Android 开源库

其他

  • java-zhconverter - java-zhconverter是一个简繁体中文互换的Java开源类库。
  • joda-time-android - 一个超赞的时间处理的库,Joda-Time ! 他能帮你轻松处理时区,处理时间加减,计算到期时间等等场景下的问题。java版本
  • AssistiveTouch - 配合Android手机沉浸式隐藏虚拟按键后快捷操作 (Nexus5屏幕变大了)。
  • S-Tools - S-Tools一个可以实时查看的CPU状态和手机各类传感器数据,还有一些例如颜色选择、指南针和设备信息等功能。
  • JsBridge - 模仿微信webview的JsBridge,安全方便的实现js和Java的互相调用,主要通过loadUrl和shouldOverrideUrl实现。
  • Sample Of All Samples - 提供大部分Android5.0组件的示例应用。
  • Android-Package-Channel - 美团网做的把Android多渠道打包工具,打包时间缩短到一分钟,python脚本。
  • fast-apk-packaging - Android不需要重新编译打渠道包。
  • android_gradle_script - gradle批量打包脚本,用txt配置一下,就可以支持多个渠道打包,适合国内这种动不动上百个渠道包的环境。目前有个问题,一次打包脚本超过80个就会GC问题。
  • BatchPackApk - 免签名直接打包工具。
  • Android多渠道打包工具Gradle插件 - Android多渠道打包工具Gradle插件。
  • Gradle-Plugin-User-Guide-Chinese-Verision - Gradle插件使用指南中文版。
  • gradle-guide.books - Android Gradle 插件中文指南(GitBook)。
  • Android-package_tool - 该工程用于编译多渠道Android应用,替换相应的标签,然后重新打包,用perl脚本实现。
  • 兰贝壳儿 - Android多渠道打包解决方案(兰贝壳儿),eclipse插件。
  • Algorithms - 常见算法问题的Java实现。
  • java-design-patterns - 一个常见设计模式的java实现。
  • PreferenceInjector - SharedPreference注入开源库,SharedPreference key与某个变量绑定、监听key变化、初始化key都可以通过注解完成。
  • prettytime - 一个实用的人性化的时间显示,比如:几分钟前,几天前。
  • Material-Movies - Material Design 下的Movie App(电影展示),可供学习,或者直接二次开发。
  • Clean-Contacts - 充满技术含量的一个 Contact App(联系人)。
  • RedEnvelopeAssistant - 完全免费开源的抢红包软件、做这个软件纯粹是发现Android的模拟点击十分好玩,然后顺道写了一个,有此基础,可以再扩展其他的很多模拟点击程序 。
  • superCleanMaster - 一键清理开源版,包括内存加速,缓存清理,自启管理,软件管理等。
  • LoadViewHelper - 切换加载中,加载失败,加载成功布局,定义一个LoadViewHelper所有界面通用。
  • android-best-practices - android最佳实践
  • Android最佳实践 - 安卓最佳实践(1):安卓开发--中文。
  • Android最佳实践 - 从Futurice公司Android开发者中学到的经验。 遵循以下准则,避免重复发明轮子。若您对开发iOS或Windows Phone 有兴趣, 请看iOS Good Practices 和 Windows client Good Practices 这两篇文章。
  • 如何安装ACRA - 如何安装ACRA-一个Android应用Crash跟踪系统—在自己的服务器上。
  • Android ocr识别文字介绍 - Android ocr识别文字介绍 。
  • DaVinci - DaVinci是一个适用于Android Wear平台的图片下载和缓存library。
  • Point-of-Android - Android 一些重要知识点解析整理 。
  • AppStoreLibrary - 检测是否在appstore安装了应用,搜索应用。
  • LeakCanary - 利用此类库,排查内存泄露变得非常简单,LeakCanary 中文使用说明LeakCanary: 让内存泄露无所遁形 。
  • anko - 快速开发框架。
  • CommonAdapter - 通过对于原生Adapter的封装,产生了支持ListView,GridView,RecyclerView的简单通用的Adapter。这种方式将item变成独立的“视图”对象,方便操作,又增加了可扩展性。
  • MVPAndroidBootstrap - 一个Android MVP 模式实例项目。
  • json2notification - 一个多功能方便好用的notification通知栏通知开源库。
  • barcodescanner - 一个封装好的基于zxing二维码扫描库。
  • BGAQRCode-Android - 一个可高度定制二维码扫描界面、生成二维码、识别图片二维码库。
  • mqtt - MQTT 协议 3.1.1 中文翻译版。
  • Droid Plugin - DroidPlugin 是360手机助手在Android系统上实现了一种新的插件机制:它可以在无需安装、修改的情况下运行APK文件,此机制对改进大型APP的架构,实现多团队协作开发具有一定的好处。
  • JsonAnnotation - 利用注解自动生成Gson‘s Model的库。
  • WeChatLuckyMoney - 微信抢红包插件。
  • android-support-23.2-sample - support 库在 23.2 版本新增内容示例项目。
  • Sunoath - 基于MVP+Retrofit+Material Design的Demo。
  • ActivityRouter - 一个url打开activity的Router库,支持指定参数类型,支持参数transfer,支持callback。

好的文章

收集android上开源的酷炫的交互动画和视觉效果

交互篇

1.SlidingUpPanelLayout 项目介绍:他的库提供了一种简单的方式来添加一个可拖动滑动面板(由谷歌音乐推广,谷歌地图和Rdio)你的Android应用程序。 项目地址:https://github.com/umano/AndroidSlidingUpPanel

2.FoldableLayout 项目介绍:折叠展开点击的ITEM 项目地址:https://github.com/alexvasilkov/FoldableLayout

3.android-flip 项目介绍:折叠翻页效果 项目地址:https://github.com/openaphid/android-flip

4.SwipeBackLayout 项目介绍:拖动关闭当前活动窗体 项目地址:https://github.com/ikew0ng/SwipeBackLayout

5.AndroidImageSlider 项目介绍:一个漂亮的Slider,可以通过自定义达到更好的效果 项目地址:https://github.com/daimajia/AndroidImageSlider

6.Android-ParallaxHeaderViewPager 项目介绍:栏目展示动画,自动播放,滚动下方列表时候,收缩效果 项目地址:https://github.com/kmshack/Android-ParallaxHeaderViewPager

7.FragmentTransactionExtended 项目介绍: 项目地址:https://github.com/DesarrolloAntonio/FragmentTransactionExtended

8.FragmentTransactionExtended 项目介绍:Android按钮可以化身进度 项目地址:https://github.com/dmytrodanylyk/circular-progress-button

9.floatlabelededittext 项目介绍:简单的实现浮动标签EditText:Android视图使用EditText之上,并提示EditText时填充文本。 项目地址:https://github.com/wrapp/floatlabelededittext

10.QuickReturn 项目介绍:Showcases QuickReturn view as a header, footer, and both header and footer. 给几乎所有可以滑动的 View 加上快速返回的 Header 或者 Footer,使用非常方便。 项目地址:https://github.com/lawloretienne/QuickReturn

11.VNTNumberPickerPreference 项目介绍:这是一个易于使用的自定义偏好,打开一个对话框中有许多选择。的值被自动保存,你可以设置默认,min -和maxValue方便地在XML。 项目地址:https://github.com/vanniktech/VNTNumberPickerPreference

12.CircularFloatingActionMenu 项目介绍:动画,可定制的圆形浮动菜单为Android, 项目地址:https://github.com/oguzbilgener/CircularFloatingActionMenu

13.NiftyDialogEffects 项目介绍:Dialog的各种打开动画,Nifty Modal Dialog Effects look like this(Nifty Modal Window Effects) 项目地址:https://github.com/sd6352051/NiftyDialogEffects

14.material-menu 项目介绍:变形安卓菜单,返回和删除按钮 项目地址:https://github.com/balysv/material-menu

15.AndroidViewHover 项目介绍:我们需要一个悬停视图,显示菜单,显示消息。 项目地址:https://github.com/daimajia/AndroidViewHover

16.PagedHeadListView 项目介绍:图片轮转切换 项目地址:https://github.com/JorgeCastilloPrz/PagedHeadListView

17.android-movies-demo 项目介绍:电影列表3级联动,交互 项目地址:https://github.com/dlew/android-movies-demo

18.NiftyNotification 项目介绍:提示通知栏的各种动画 项目地址:https://github.com/sd6352051/NiftyNotification

19.SwipeBack 项目介绍:拖动关闭,模范:kicker app(https://play.google.com/store/apps/details?id=com.netbiscuits.kicker) 项目地址:https://github.com/sockeqwe/SwipeBack

20.AndroidSwipeLayout 项目介绍:类似微信的测拉菜单 项目地址:https://github.com/daimajia/AndroidSwipeLayout

21.SnackBar 项目介绍: 项目地址:https://github.com/MrEngineer13/SnackBar

22.Swipecards 项目介绍:A Tinder-like cards effect as of August 2014. You can swipe left or right to like or dislike the content. The library creates a similar effect to Tinder's swipable cards with Fling animation. 项目地址:https://github.com/Diolor/Swipecards

23.LDrawer 项目介绍:Android抽屉与材料设计动画图标 项目地址:https://github.com/keklikhasan/LDrawer

视觉篇

1.android-stackblur 项目介绍:毛玻璃,朦胧美 项目地址:https://github.com/kikoso/android-stackblur
DEMO演示:

2.BlurEffectForAndroidDesign 项目介绍:实现模糊图形技巧 项目地址:https://github.com/PomepuyN/BlurEffectForAndroidDesign

3.Shimmer-android 项目介绍:闪动的文字 项目地址:https://github.com/RomainPiel/Shimmer-android

4.WizardPager 项目介绍:它提供了一个示例实现的Android手机上安装一个向导界面 项目地址:https://github.com/TechFreak/WizardPager

5.FloatingActionButton 项目介绍:浮动的按钮 项目地址:https://github.com/FaizMalkani/Fabulous

6.JumpingBeans 项目介绍:跳动的文本 项目地址:https://github.com/frakbot/JumpingBeans

7.android_maskable_layout 项目介绍:可屏蔽的布局 项目地址:https://github.com/christophesmet/android_maskable_layout

8.activityanimation 项目介绍:Activit之间切换动画 项目地址:https://github.com/flavienlaurent/activityanimation

9.android-shape-imageview 项目介绍:提供了一组自定义形状的android imageview组件,和一个框架来定义更多的形状。实现着色器和位图基于掩模图像视图。 项目地址:https://github.com/siyamed/android-shape-imageview

10.RippleView 项目介绍:认为模仿的连锁反应在单击推出了Android L 项目地址:https://github.com/siriscac/RippleView

11.android-ui 项目介绍:一个小部件可以定义的行为之间的动态变化 项目地址:https://github.com/markushi/android-ui

12.FlatUI 项目介绍: 项目地址:https://github.com/eluleci/FlatUI

UI资源

  • fontawesome - Font-Awesome图标。
  • material-design-responsive-design - 深聊Material Design复杂响应式设计,comprehensive-material-design-note - 帮你全面彻底搞定Material design的学习笔记。
  • Iconics - 这是一个可以让你在你的项目中使用几乎任何字体图标的库。默认包含 FontAwesome 和 Material Design Icons 还包含 Meteocons 插件。你甚至可以添加任何你自定义的字体图标(typeface)。

开发资源

他人开源总结
  • awesome-java - java库列表,中文版
  • material design 的android开源代码整理 - material design 的android开源代码整理。
  • Android开源项目分类汇总 - Trinea 国内最多好的开源库总结。 Android 开源库获取途径整理
  • Android开源库源码分析 - Trinea 我们从 Android 开始建了了协作项目,从简介、总体设计、流程图、详细设计全方面分析开源库源码。目前第一期完成,包括10个开源库及5个公共技术点的全面介绍。 在线网页

  • 年薪30万的Android程序员必须知道的帖子 - Android开源项目汇总,带效果gif图。

  • Android官方培训课程中文版 - Google Android官方培训课程中文版。

  • GitHub优秀Android开源项目 - GitHub 优秀的 Android 开源项目,很多中文现成项目。

  • Android开发工具及文档 - androiddevtools,收集整理Android开发所需的Android SDK、开发中用到的工具、Android开发教程、Android设计规范,免费的设计素材等。

  • material_design - eoeAndroid Material Design 中文协同翻译。

  • Android Design Support Library - Android Design Support Library 的 代码实验——几行代码,让你的 APP 变得花俏。
  • Android-Open-Sourse-Library - eoeAndroid 开源组件深度剖析: 1.Http请求组件:Volley\android-async-http\okhttp 2.json数据解析组件:Gson\fast-json\json-smart\Jackson。
  • wiki-eoeandroid - wiki-eoeandroid : Android Develop - 开发技术、Android Design - 设计规范、Android Distribute - 软件发布。

  • Java资源大全 - 国外程序员整理的Java资源大全。

  • Android开发技术前线 - Android开发技术前线 ( android-tech-frontier ),一个定期翻译、发布国内外Android优质的技术、开源库、软件架构设计、测试等文章的开源项目,让我们的技术跟上国际步伐。。

  • 10个常用工具类 - Android快速开发系列 10个常用工具类:1、日志工具类L.java;2、Toast统一管理类;3、SharedPreferences封装类SPUtils;4、单位转换类 DensityUtils;5、SD卡相关辅助类 SDCardUtils;6、屏幕相关辅助类 ScreenUtils;7、App相关辅助类;8、软键盘相关辅助类KeyBoardUtils;9、网络相关辅助类 NetUtils;10、Http相关辅助类 HttpUtils。
  • 19个Android开发工具 - 19个Android 开发工具:1、XAppDbg;2、ChkBugReport;3、APKAnalyser;4、AppXplore;5、Memory Analyzer(MAT);6、Eclipse插件SQLiteManger;7、Robotium;8、ACRA;9、Android Layout Binder;10、Spoon;11、Android Content Provider代码生成器;12、AndroidKickStartR;13、Android Holo颜色生成器;14、ActionBar风格生成器;15、Asset Studio;16、little eye labs;17、Droid Inspector;18、Android Button Maker;19、jsonschema2pojo。

  • apkbus - code4app 与 apkbus整理的Android开源资源分类, Android源代码

  • open-source-android-apps - 他人收集的开源代码:Android Wear、Communication 、Education、Finance、Game、Multi-Media、News & Magazines、Personalization、Productivity、Social Network、Tools、Travel & Local。
  • android-developer-tools-list - Android 常用开发工具 (Android Studio 插件、Android 网站、Android 系统性能调优工具、Android测试工具)。
  • Android平台上最好的几款免费的代码编辑器 - Android平台上最好的几款免费的代码编辑器:Quoda、DroidEdit、AWD、AIDE、CppDroid。
  • androidweekly - android技术开发周报,干货很多。
  • awesome-rails-gem - 收集了很多在平时使用 Rails 开发网站时经常会使用到的 Gem, 当中包括的有用户认证系统, API 接口开发, 文件上传, 站内搜索等优秀的 Gem 源。
  • Android_Data - 一份旨在帮助 Android 初学者快速入门以及找到适合自己学习的资料。
  • Android通用流行框架大全 - Android通用流行框架大全。
中文开发博客列表


来自:https://github.com/dreamlivemeng/TimLiu-Android
作者:lipengshiwo 发表于2016/10/4 22:21:39 原文链接
阅读:93 评论:0 查看评论

Android开发——View绘制过程源码解析(一)

$
0
0

0. 前言  

View的绘制流程从ViewRootperformTraversals开始,经过measurelayoutdraw三个流程,之后就可以在屏幕上看到View了。其中measure用于测量View的宽和高layout用于确定View在父容器中放置的位置draw则用于将View绘制到屏幕上

本文原创,转载请注明出处:SEU_Calvin的CSDN博客


1. MeasureSpec

说到measure那么就不得不提MeasureSpec,它是由ViewLayoutParams和父容器(顶级View则为屏幕尺寸)一起决定的,一旦确定了MeasureSpec,在onMeasure()中就可以确定View的宽高

MeasureSpec的值由SpecSize(测量值)和SpecMode(测量模式)共同组成。

SpecMode一共有三种类型:

1EXACTLY表示设置了精确的值,一般当childView设置其宽高为精确值、match_parent(同时父容器也是这种模式)时,ViewGroup会将其设置为EXACTLY

2AT_MOST表示子布局被限制在一个最大值内(即SpecSize),一般当childView设置其宽高为wrap_contentmatch_parent(同时父容器也是这种模式)时,ViewGroup会将其设置为AT_MOST

3UNSPECIFIED表示View可以设置成任意的大小,没有任何限制。这种情况比较少见。

 

2. MeasureSpec的生成过程

2.1 顶级ViewMeasureSpec

// desiredWindowWidth和desiredWindowHeight为屏幕尺寸
// lp.width和lp.height都等于MATCH_PARENT
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);  
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); 
//…
private int getRootMeasureSpec(int windowSize, int rootDimension) {  
    int measureSpec;  
    switch (rootDimension) {  
    case ViewGroup.LayoutParams.MATCH_PARENT:  
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);  
        break;  
    case ViewGroup.LayoutParams.WRAP_CONTENT:  
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);  
        break;  
    default:  
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);  
        break;  
    }  
    return measureSpec;  
}  

从源码中可以看出,这里使用了MeasureSpec.makeMeasureSpec()方法来组装一个MeasureSpecrootDimension参数等于MATCH_PARENTMeasureSpecSpecModeEXACTLY。并且MATCH_PARENTWRAP_CONTENT时的SpecSize都等于windowSize的,也就意味着根视图总是会充满全屏的。

 

2.2 普通ViewMeasureSpec

在对子元素进行measure之前,会先调用getChildMeasureSpec方法得到子元素的MeasureSpec。上面也提到过了,子元素MeasureSpec与父容器的MeasureSpec和子元素本身的LayoutParams有关。源码介绍如下:

//参1为父容器的MeasureSpec
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

其中上述源码总结如下:

1)当View为固定宽高时,MeasureSpecEXACTLY模式/LayoutParams中的大小。

2)当ViewWRAP_CONTENT时,MeasureSpecAT_MOST模式/父容器剩余空间大小。

3)当ViewMATCH_PARENT时,若父容器为EXACTLY模式,那么ViewMeasureSpecEXACTLY模式/父容器的剩余大小。若父容器为AT_MOST模式,那么ViewMeasureSpecAT_MOST模式/父容器的剩余大小。


3. Measure过程

3.1 普通ViewMeasure过程

Viewmeasure()方法是final的,因此我们无法在子类中去重写这个方法,在该方法内部会调用onMeasure()方法,源码如下所示。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // setMeasuredDimension设置视图的大小,这样就完成了一次measure的过程
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),    
    getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));    
}
//这个方法就是近似的返回spec中的specSize,除非你的specMode是UNSPECIFIED
//UNSPECIFIED 这个一般都是系统内部测量才用的到
public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
}

onMeasure()方法中调用setMeasuredDimension()方法来设定测量出的大小,这样一次measure过程就结束了。

 

getDefaultSize方法中,从上面源码的11-20行,结合2.2中的结论可以知道:

(1)如果设置了固定宽高,View是EXACTLY模式并且传入MeasureSpec的大小就是自定义的宽高,上述11行、18行和19行代码表示这种设置会显示正常。

(2)如果设置了MATCH_PARENT,View的MODE会有两种情况,不过不管是哪一种,结果都是从MeasureSpec中获取大小,通过2.2中的结论可知为父容器剩余大小,因此这种设置逻辑上也会显示正常。

(3)如果设置了WRAP_CONTENT,View的MODE一定会是AT_MOST,结果是从MeasureSpec中获取大小,通过2.2中的结论可知为父容器剩余大小,逻辑上就会显示不正常,包裹内容效果会失效。这里就不贴实例了,网上有很多,有兴趣可以查看这一篇


说了这么多,我们得出的结论就是:直接继承View的自定义控件需要重写onMeasure()并设置WRAP_CONTENT时自身大小。

解决方式就是在onMeasure里针对WRAP_CONTENT来做特殊处理,比如通过setMeasuredDimension()指定一个默认的宽高。逻辑如下:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //默认值
    int desiredWidth = 100;
    int desiredHeight = 100;

    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    int width;
    int height;

    //Measure Width
    if (widthMode == MeasureSpec.EXACTLY) {
        //Must be this size
        width = widthSize;
    } else if (widthMode == MeasureSpec.AT_MOST) {
        //Can't be bigger than...
        width = Math.min(desiredWidth, widthSize);
    } else {
        //Be whatever you want
        width = desiredWidth;
    }

    //Measure Height
    if (heightMode == MeasureSpec.EXACTLY) {
        //Must be this size
        height = heightSize;
    } else if (heightMode == MeasureSpec.AT_MOST) {
        //Can't be bigger than...
        height = Math.min(desiredHeight, heightSize);
    } else {
        //Be whatever you want
        height = desiredHeight;
    }

    //MUST CALL THIS
    setMeasuredDimension(width, height);
}

我翻了所有的资料,重写该方法时貌似默认AT_MOST就等于WRAP_CONTENT我们知道顶级容器默认是EXACTLY模式,所以在这篇博客里的例子中上述代码可以解决WRAP_CONTENT失效的问题。但是如果布局参数写为MATCH_PARENT但是父容器为AT_MOST模式时,得出的子View也是AT_MOST模式,那么上述代码好像是有逻辑漏洞的。想了想,好像确实很难出现这种情况,具体不太清楚,有清楚的朋友可以留言交流一下。

 

3.2 ViewGroupMeasure过程

因为一个布局中一般都会包含多个子视图,因此每个视图都需要经历一次measure过程。

ViewGroup是没有onMeasure方法的,这个方法是交给子类自己实现的。不同的ViewGroup子类布局都不一样,那么onMeasure索性就全部交给他们自己实现好了。

ViewGroup中定义了一个measureChildren()方法来遍历测量子视图的大小,如下所示:

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {  
    final int size = mChildrenCount;  
    final View[] children = mChildren;  
    for (int i = 0; i < size; ++i) {  
        final View child = children[i];  
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {  
            measureChild(child, widthMeasureSpec, heightMeasureSpec);  
        }  
    }  
} 

里面循环调用了measureChild,其实现为:

protected void measureChild(View child, int parentWidthMeasureSpec,int parentHeightMeasureSpec) {    
       //通过子布局参数和父容器MeasureSpec得到childMeasureSpec
       //过程略..
       //所以这其实是个递归调用,不断的去测量设置子视图的大小,直至完成整个View数测量遍历 
       child.measure(childWidthMeasureSpec, childHeightMeasureSpec);    
}


以上就是关于measure过程的一些解析,后面会更新另外layout和draw过程的解析。

本文原创,转载请注明出处SEU_Calvin的CSDN博客

最后如果有同学清楚3.1最后提出的那个小疑问,请留言交流,谢谢。


作者:SEU_Calvin 发表于2016/10/4 22:29:19 原文链接
阅读:982 评论:0 查看评论

微信小程序开发常见问题分析

$
0
0

距离微信小程序内测版发布已经有十天的时间了,网上对微信小程序的讨论也异常火爆,从发布到现在微信小程序一直占领着各种技术论坛的头条,当然各种平台也对微信小程序有新闻报道,毕竟腾讯在国内影响力还是很大的。我们都知道微信小程序第一天发布内测版,并没有公开官方开发文档和开发工具,但是这阻止不了技术人的好奇心,通过破解以及先安装旧版本再用新版本覆盖安装一系列流程,即可体验微信小程序的魅力,当时为了使更少的人少走弯路,于是自己就写了微信小程序开发环境搭建一文。不过在文章发布第二天微信官方正式发布了官方文档,并且更新了开发工具,无·appid也可以体验小程序的开发。
因为自己对小程序也是很有兴趣的,感觉是很有意思的一个东西,所以以QQ练手,做一个高仿QQ的微信小程序,由于本人是Android开发者,平时很少接触前端的一些东西,水平有限,所以代码很多地方是不规范的,做的过程也就是一个学习的过程,一个提高的过程。
这篇文章主要写我在SmallAppForQQ这个项目进展的过程中遇到的一些问题。如果阅读此文的你有一定帮助,很是欣慰,欢迎star项目


SmallAppForQQ源码

开发工具

官方demo


项目结构

文章开头,先简单介绍下项目结构,若没有安装开发工具,可去GitHub:https://github.com/xiehui999/SmallAppForQQ下载。微信小程序项目结构主要有四个文件类型,如下

  • WXML(WeiXin Markup Language)是框架设计的一套标签语言,结合基础组件、事件系统,可以构建出页面的结构。内部主要是微信自己定义的一套组件。

  • WXSS(WeiXin Style Sheets)是一套样式语言,用于描述 WXML 的组件样式,

  • js 逻辑处理,网络请求

  • json 小程序设置,如页面注册,页面标题及tabBar。

注意:为了方便开发者减少配置项,规定描述页面的这四个文件必须具有相同的路径与文件名。

在根目录下用app来命名的这四中类型的文件,就是程序入口文件。

  • app.json
    必须要有这个文件,如果没有这个文件,项目无法运行,因为微信框架把这个作为配置文件入口,整个小程序的全局配置。包括页面注册,网络设置,以及小程序的window背景色,配置导航条样式,配置默认标题。
  • app.js
    必须要有这个文件,没有也是会报错!但是这个文件创建一下就行 什么都不需要写
    以后我们可以在这个文件中监听并处理小程序的生命周期函数、声明全局变量。

  • app.wxss
    全局配置的样式文件,项目非必须。

知道小程序基本文件结构,就可以开始研究官方demo了,研究过程中如果有不明白的地方可以去官方文档寻求答案,如果找不到答案或者有疑问,可再此博客留言,相互交流。下面介绍下出现概率较高的几个问题。

常见问题

rpx(responsive pixel)

微信小程序新定义了一个尺寸单位,可以适配不同分辨率的屏幕,它规定屏幕宽为750rpx,如在 iPhone6 上,屏幕宽度为375px,共有750个物理像素,则750rpx = 375px = 750物理像素,1rpx = 0.5px = 1物理像素。

这里写图片描述

这个项目我用的都是rpx尺寸单位,期间遇到一个很奇葩的问题。在相邻的两条信息直接都会有一个分割线,我将线的高度都设置成1rpx,但是不有个别分割线是不显示的,如下图

这里写图片描述

看到没在第一条和第二条直接并没有现实这条线,但是其他的都展示了,分割线的属性是一样的,而且在不同的手机上(分辨率不同)不显示的分割线也是不同的,有的分辨率好几条分割线都不显示,不知道这是模拟器的bug还是rpx的bug。最后分割线的高度尺寸单位使用了px,解决了这个问题。

40013错误

这里写图片描述

在微信小程序刚出来的时候如果输入AppID提示这个信息就表示没有破解,但是现在官方软件更新可以选择无AppID开发,如下图,我们之间选择无AppID,即可解决此错误。建议安装官方开发工具。可去此处找下载链接

这里写图片描述

-4058错误

微信小程序创建项目时选择无AppID,创建项目时会生成app.json,app.josn是程序启动最重要的文件,程序的页面注册,窗口设置,tab设置及网络请求时间设置都是在此文件下的。如果你创建的项目目录下没有app.json文件就会报下面的错误。

这里写图片描述

我们看到上面的错误信息中有个数字-4058,这应该是初入微信小程序遇到最多的错误了,这种一般都是文件缺失,后面有个path,可以对着该路径看看是否存在这个文件。造成这种错误的原因一般都是创建项目选择的目录不正确,或者在app.json注册了一个不存在的页面。
当然还有一种情况就是在app.json文件的pages注册的页面是没有创建的,或者你删除了某个页面,但是没有取消注册也会是-4058错误。

Page注册错误

这里写图片描述

这个错误可能很容易理解,页面注册错误。页面是通过Page对象来渲染的,每个页面对应的js文件必须要创建page,最简单的方式就是在js文件下写入Page({}),在page中有管理页面渲染的生命周期,以及数据处理,事件都在这完成。这个错误引起的原因一般都是刚创建页面,js文件还有有处理或者忘了处理。所以要养成创建页面的同时在js文件先创建Page的习惯.

Page route错误

这里写图片描述

字面意思就是页面路由错误,在微信中有两种路由方式一种是在wxml文件使用组件,一种是调用wx.navigateTo。
如下代码:

wxml文件:

<navigator url="search/search">
<view class="serach_view_show" bindtap="bindtap"> 搜索</view>
</navigator>

js文件事件处理函数:

  bindtap:function(event){
wx.navigateTo({
  url: "search/search"
})
  }

如果你这样写的话,恭喜你,你就会看到上面提示的错误,这是因为重复调用路由引起的,处理方法就是删除一个路由,删除组件或者删除wx.navigateTo。除了上面说的可能导致路由错误外,还有一种情况,类似于下面的代码

<navigator url="search/search">
<navigator url="search/search">
<view class="serach_view_show" bindtap="bindtap"> 搜索</view>
</navigator>
</navigator>

这种也是不允许的,也就是说组件内部不能再嵌套组件。它只能是单层存在的。

Do not have * handler in current page.

这里写图片描述

大概意思就是当前页面没有此处理,让确定是否已经定义,还指出了错误出现的可能位置pages/message/message,其实这种问题出现一般就是我们在wxml定义了一些处理事件,但是在js文件中没有实现这个时事件的处理方法,就会出现这个错误。那么我们按提示在js文件加上事件处理,如下代码,加上后就不会再有此错误提示。

  bindtap:function(event){
wx.navigateTo({
  url: "search/search"
})
  },

tabBar设置不显示

对于tabBar不显示,原因有很多,查找这个错误直接去app.json这个文件,最常见的也是刚学习微信小程序最容易犯的错误无外乎下面几种

注册页面即将页面写到app.json的pages字段中,如

 "pages":[

    "pages/message/message",
    "pages/contact/contact",
    "pages/dynamic/dynamic",
     "pages/dynamic/music/music",
    "pages/index/index",
    "pages/logs/logs"
  ]
  • tabBar写法错误导致的不显示,将其中的大写字母B写成小写,导致tabBar不显示。

  • tabBar的list中没有写pagePath字段,或者pagePath中的页面没有注册

  • tabBar的list的pagePath指定的页面没有写在注册页面第一个。微信小程序的逻辑是”pages”中的第一个页面是首页,也就是程序启动后第一个显示的页面,如果tabBar的list的pagePath指定的页面都不是pages的第一个,当然也就不会电视tabBar了。

  • tabBar的数量低于两项或者高于五项,微信官方中明确规定tabBar的至少两项最多五项。超过或者少于都不会显示tabBar。

这里写图片描述

通过这个动态图你应该发现问题了,当点击音乐进入音乐界面时,title先显示了WeChatForQQ然后显示的音乐,这个体验肯定是难以接受的,原因是音乐界面的title是在js文件中page的生命周期方法中设置的。
若你不了解生命周期,可以点击查看

Page({
  data:{
    // text:"这是一个页面"
  },
  onLoad:function(options){
    // 页面初始化 options为页面跳转所带来的参数

  },
  onReady:function(){
    // 页面渲染完成
    //NavigationBarTitle如果此处和json文件都设置,最后展示此处的标题栏
wx.setNavigationBarTitle({
  title: '音乐'
})
  },
  onShow:function(){
    // 页面显示
  },
  onHide:function(){
    // 页面隐藏
  },
  onUnload:function(){
    // 页面关闭
  }
})

通过注释你应该明白了,设置标题写在了onReady方法中,也就是页面已经渲染完成了,在onReady之前显示的title就是json文件(覆盖关系,如果在子页面json文件设置title会覆盖app.json全局设置)中的title。可能你会说将wx.setNavigationBarTitle写在onLoad函数中,不过如果这样设置是不对的,因为onLoad执行过后才渲染页面,在渲染页面时title会从json文件中读取,导致onLoad设置的title会只在页面渲染之前展示,之后就显示json文件的tile,所以现在你应该明白ttle设置最优的地方就是给子文件写一个json文件,在文件中写入,如果想改变颜色直接在文件中添加就可以,该文件所写的属性值会覆盖app.json中设置的值。

{
    "navigationBarTitleText": "音乐"
}
作者:xiehuimx 发表于2016/10/4 22:47:39 原文链接
阅读:175 评论:0 查看评论

android之Toolbar使用详解

$
0
0

Toolbar简介

Toolbar是在 Android 5.0 开始推出的一个 Material Design 风格的导航控件 ,Google 非常推荐大家使用 Toolbar 来作为Android客户端的导航栏,以此来取代之前的 Actionbar 。与 Actionbar 相比, Toolbar 明显要灵活的多。它不像 Actionbar 一样,一定要固定在Activity的顶部,而是可以放到界面的任意位置。除此之外,在设计 Toolbar 的时候,Google也留给了开发者很多可定制修改的余地,这些可定制修改的属性在API文档中都有详细介绍。

前面提到 Toolbar 是在 Android 5.0 才开始加上的,Google 为了将这一设计向下兼容,自然也少不了要推出兼容版的 Toolbar 。为此,我们需要在工程中引入 appcompat-v7 的兼容包,使用 android.support.v7.widget.Toolbar 进行开发。

Toolbar基本使用

效果图:
这里写图片描述

修改样式,这里使用没有导航栏NoAcionBar的主题样式,不过我在网上看的时候,发现有的文章说,这样太麻烦了,直接在BaseActivity中调用 supportRequestWindowFeature(Window.FEATURE_NO_TITLE) 去掉了默认的导航栏(注意,BaseActivity是继承了AppCompatActivity的,如果是继承Activity就应该调用 requestWindowFeature(Window.FEATURE_NO_TITLE),这样做虽然也达到了效果,但是如果你在Activity中设置 setSupportActionBar(toolbar),那么将会报错。虽然你可以不用把Toolbar设置为ActionBar,去继续使用ActionBar的一些属性。但是Googl官方还是建议设置Toolbar支持ActionBar的一些属性。所以这里就是用没有导航栏的Theme。

<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
    <!-- Customize your theme here. -->
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="colorAccent">@color/colorAccent</item>
    <!--设置Toolbar上字体的颜色,以及返回和更多按钮的颜色-->
    <item name="android:textColorPrimary">@color/white</item>

</style>

我们在使用Toolbar时,为了方便通常创建一个独立的布局来放Toolbar,当在其它布局中使用到时直接即可,可以减少布局代码的冗余。Toolbar支持自定义的添加一些控件。

首先创建Toolbar的布局:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.Toolbar
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/toolbar"
    android:layout_width="match_parent"
    android:layout_height="?attr/actionBarSize"
    android:background="@color/colorPrimary"
    android:elevation="4dp"
    app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
    app:theme="@style/ThemeOverlay.AppCompat.ActionBar">
    <!--custom view-->
    <TextView
        android:id="@+id/tv_custom"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Custom View"
        android:textColor="@color/white"
        />
</android.support.v7.widget.Toolbar>

在activity_main中引入Toolbar:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.listenergao.toolbardemo.MainActivity">

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

</LinearLayout>

在MainActivity中具体使用Toolbar的一些属性,首先我们通过findViewById找到Toolbar控件,
通过 setSupportActionBar(toolbar); 将Toolbar设置给ActionBar,并支持ActionBar的一些属性。

//主要代码
toolbar.setTitle("Title");  //设置Title
toolbar.setSubtitle("subTitle");    //设置subTitle
toolbar.setNavigationIcon(R.mipmap.ic_search);  // 设置导航栏图标
toolbar.setLogo(R.mipmap.ic_favorite);      // 设置Logo
setSupportActionBar(toolbar);   // 设置Toolbar支持ActionBar的一些属性

如果你想修改标题和子标题的字体大小、颜色等,可以调用 setTitleTextColor 、 setTitleTextAppearance 、 setSubtitleTextColor 、 setSubtitleTextAppearance 这些API;

自定义的View位于 title 、 subtitle 和 actionmenu 之间,这意味着,如果 title 和 subtitle 都在,且 actionmenu选项 太多的时候,留给自定义View的空间就越小;

导航图标和 app logo 的区别在哪?如果你只设置 导航图标 ( or app logo ) 和 title 、 subtitle ,会发现 app logo 和 title 、 subtitle 的间距比较小,看起来不如 导航图标 与 它们两搭配美观;

Toolbar使用系统提供的 返回箭头 按钮(必须设置Toolbar支持ActionBar属性才可以)

效果图:
这里写图片描述
主界面进入FirstActivity界面,点击按钮进入FirstActivity的子页面。

  1. 当我们创建Activity时,需要指明Activity的子父关系。设置android:parentActivityName=”“属性,指明子父关系。需要注意的是:android:parentActivityName=”“该属性在Android4.1(API Level 16)中才引入。不过Google为了支持老的设备,也提供了 name-value,name是android.support.PARENT_ACTIVITY,value是父Activity的名称。

Toolbar使用Action Menu

效果图:
这里写图片描述
1. 在res下创建menu文件夹,创建Action Menu文件,关于有些属性就不细说了,因为有些我也不是很明白,就不误导大家了,这里可以暂时忽略 搜索 的那个item,下面会细说。

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

    <item
        android:id="@+id/action_favorite"
        android:icon="@mipmap/ic_favorite"
        android:title="@string/action_favorite"
        app:showAsAction="ifRoom"/>

    <item
        android:id="@+id/action_search"
        android:icon="@mipmap/ic_search"
        android:title="搜索"
        app:showAsAction="ifRoom|collapseActionView"
        app:actionViewClass="android.support.v7.widget.SearchView" />



    <item
        android:id="@+id/action_settings"
        android:title="@string/action_settings"
        app:showAsAction="never"/>

    <item
        android:id="@+id/action_about"
        android:title="@string/action_about"
        app:showAsAction="never"/>

    </menu>
  1. 在Activity中初始化Action menu

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.toolbar_menu, menu);
    
        return super.onCreateOptionsMenu(menu);
    }
    

    当用户点击App bar的items时,Activity会回调 onOptionsItemSelected()方法,并传递一个MenuItem对象,以指示单击了哪个item。
    我们实现onOptionsItemSelected()方法,调用MenuItem.getItemId()方法来确定按下了哪个项目。
    该ID匹配返回你在相应的元素的android公布值:id属性。

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.action_favorite:
                Toast.makeText(this, "点击了 favorite ", Toast.LENGTH_SHORT).show();
                return true;
    
            case R.id.action_settings:
                Toast.makeText(this, "点击了 设置 ", Toast.LENGTH_SHORT).show();
                return true;
    
            case R.id.action_about:
                Toast.makeText(this, "点击了 关于 ", Toast.LENGTH_SHORT).show();
                return true;
    
            default:    // 如果用户的行为没有被执行,则会调用父类的方法去处理,建议保留。
                return super.onOptionsItemSelected(item);
        }
    }
    

Toolbar使用SearchView(App bar中使用搜索)

效果图:
这里写图片描述
1. 在上一步中大家可能看到在应用栏(Material Design中称之为App bar)使用了搜索的这个功能,在menu中添加item如下:

    <item
        android:id="@+id/action_search"
        android:icon="@mipmap/ic_search"
        android:title="搜索"
        app:showAsAction="ifRoom|collapseActionView"
        app:actionViewClass="android.support.v7.widget.SearchView" />
  1. 在初始化Action Menu时,通过调用静态getActionView()方法,得到SearchView对象的引用。根据需求配置搜索信息,以及添加各种监听事件。

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.toolbar_menu, menu);
        MenuItem searchItem = menu.findItem(R.id.action_search);
        //得到SearchView对象
        SearchView searchView = (SearchView) MenuItemCompat.getActionView(searchItem);
        //设置SearchView视图打开关闭的监听事件
        MenuItemCompat.setOnActionExpandListener(searchItem, expandListener);
    
        return super.onCreateOptionsMenu(menu);
    }
    
    
    // SearchView的监听事件
    MenuItemCompat.OnActionExpandListener expandListener = new MenuItemCompat.OnActionExpandListener() {
        // 打开搜索视图
        @Override
        public boolean onMenuItemActionExpand(MenuItem item) {
            //do something when action item expand
    
            Toast.makeText(MainActivity.this, "onMenuItemActionExpand方法执行了...", Toast.LENGTH_SHORT).show();
            return true;    // 返回true 打开搜索扩展视图
        }
    
        // 关闭搜索视图
        @Override
        public boolean onMenuItemActionCollapse(MenuItem item) {
            //do something when action item collapse
    
            Toast.makeText(MainActivity.this, "onMenuItemActionCollapse方法执行了...", Toast.LENGTH_SHORT).show();
            return true;    // 返回true 关闭搜索扩展视图
        }
    };
    

由于篇幅的原因,暂时先写到这里,后续会继续添加上DrawerLayout等等的使用。

作者:listeners_Gao 发表于2016/10/4 23:09:55 原文链接
阅读:102 评论:0 查看评论

Java Annotation 简析(二)

$
0
0

上一篇文章解析了java Annotation的概念和基本用法,再简单回顾下注解的作用:
a.生成文档。这是最常见的,也是java 最早提供的注解。常用的有@see @param @return 等
b. 标记,用于告诉编译器一些信息,比如 @Override等,
c. 编译时动态处理,如动态生成代码
d. 运行时动态处理,如得到注解信息

上篇文章演示了处理运行时动态处理注解,这篇文件讲解注解的编译时动态处理机制,还是实现类似ButterKnife的功能,用注解代替findViewById()和setOnClickListener。

注解处理器

要使用编译时处理注解功能,首先的在android studio中添加注解处理器:android-apt
官方地址: https://bitbucket.org/hvisser/android-apt

Android Studio原本是不支持注解处理器的, 但是用这个插件后, 我们就可以使用注解处理器了, 这个插件可以自动的帮你为生成的代码创建目录, 让生成的代码编译到APK里面去, 而且它还可以让最终编译出来的APK里面不包含注解处理器本身的代码, 因为这部分代码只是编译的时候需要用来生成代码, 最终运行的时候是不需要的。

使用这个插件很简单, 首先在你项目顶层的build.gradle文件中添加依赖项, 如下:

java
dependencies {
classpath ‘com.android.tools.build:gradle:2.0.0’
classpath ‘com.neenbedankt.gradle.plugins:android-apt:1.8’
}

然后在app的build.gradle里面添加插件的引用以及需要依赖哪些库, 如下:

apply plugin: ‘com.android.application’
apply plugin: ‘com.neenbedankt.android-apt’
….
….
dependencies {

compile project(‘:bindannotation’)
apt project(‘:viewbindcompiler’)
compile project(‘:viewinject’)
}

注意上面同时添加了bindannotation,viewbindcompiler,viewinject 三个自定义模块:

  • bindannotation : 这是个Java Library,,主要来定义注解。
  • viewbindcompiler: 必须是java Library, 用于处理上面bindannotation定义的注解。
  • viewinject:这个是Android Library,被android其他模块调用实现View的Bind。

代码结构如下:
这里写图片描述

使用注解

下面就来介绍怎么使用注解生成代码

定义bindannotation注解

1.定义ViewBind注解,处理findViewById方法:

@Documented
@Target(ElementType.FIELD) //用于类属性成员上
@Retention(RetentionPolicy.CLASS) //编译时注解
@Inherited
public @interface ViewBind {
    int value() default 0;
}

2.定义BindClick注解,处理View onClickListener方法:

@Documented
@Target(ElementType.METHOD)//用于类方法上
@Retention(RetentionPolicy.CLASS)//编译时注解
@Inherited
public @interface BindClick {
    int id() default 0;
}

定义注解产生代码调用者

在viewinject模块里先定义一个接口:

public interface ViewBinder<T> {
    void viewBind(T target);
}

为什么要先定义这个接口呢,这个接口就是用于规范编译时自动产生的代码,后续我们编译时自动生成的代码都继承这个接口,并在viewBind方法里自动生成findViewById,setOnClickListener等代码,这样我们其他地方就可以方便统一调用这些自动生成的代码。

定义一个ViewBind类,调用自动生成的代码:

public class ViewBind {
    private static final String BINDING_CLASS_SUFFIX = "$$ViewBinder";//生成类的后缀 以后会用反射去取

    public static void init(Activity activity){
        String bindClassName = activity.getClass().getName() + BINDING_CLASS_SUFFIX;
        try {
            Class bindClass = Class.forName(bindClassName);
            ViewBinder viewBind = (ViewBinder) bindClass.newInstance();
            viewBind.viewBind(activity);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

有了ViewBind类后,我们就只需在activity中,调用ViewBind.init(activity)后,就可以完成view的bind工作了。注意,后面编译时生成的类的类名,都是按使用注解的类的类名+$$ViewBinder。

比如MainActivity.class,会生成MainActivity$$ViewBinder.class 文件。

定义注解的处理器

注解处理器最核心的就是要有一个Processor, 它继承自AbstractProcessor,编译器在编译时自动查找所有继承自 AbstractProcessor 的类,然后调用他们的 process 方法去处理。

先在viewbindcompiler模块里添加所需的依赖:

apply plugin: 'java'

sourceCompatibility = 1.7
targetCompatibility = 1.7

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.google.auto.service:auto-service:1.0-rc2'
    compile 'com.squareup:javapoet:1.7.0'
    compile project(':bindannotation')
}

auto-service:使用它就不需要把processor在META-INF配置了,编译时配置的Processor能够自动被发现并处理。
javapoet:辅助生成代码,能够更简单的生成.java源文件

定义一个ViewBindProcessor类:

@AutoService(Processor.class)
public class ViewBindProcessor extends AbstractProcessor {
    private static final String BINDING_CLASS_SUFFIX = "$$ViewBinder";//生成类的后缀 以后会用反射去取
    private static final ClassName VIEW_BINDER = ClassName.get("com.nick.study.viewinject", "ViewBinder");
    private Elements elementUtils;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        elementUtils = processingEnv.getElementUtils();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        types.add(ViewBind.class.getCanonicalName());
        types.add(BindClick.class.getCanonicalName());
        return types;
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Map<String, ViewBindInfo> targetClassMap = new LinkedHashMap<>();//定义一个按类名为key值,保存其类下所有使用了注解的元素
        Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(ViewBind.class); //取出所有使用ViewBind注解的元素
        // Element表示一个程序元素,比如包、类或者方法。
        for (Element element : set) {
            if (element.getKind() != ElementKind.FIELD) { //如果此元素的不是类成员,则抛出异常
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "only support class field");
                break;
            }
            TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
            String packageName = getPackageName(enclosingElement); //得到此元素所在的包名
            String className = getClassName(enclosingElement,packageName); //得到此元素所在类的类名
            String classFullPath = packageName+"."+className;  //完整类名

            ViewBindInfo viewBindInfo = targetClassMap.get(classFullPath);
            if(viewBindInfo == null){
                viewBindInfo = new ViewBindInfo();
                targetClassMap.put(classFullPath,viewBindInfo); //保存这个类的注解信息
            }
            viewBindInfo.packageName = packageName;
            viewBindInfo.className = className;
            viewBindInfo.viewBindElementList.add(element); //所有的viewBind元素保存在一个链表里
            viewBindInfo.typeClassName =  ClassName.bestGuess(getClassName(enclosingElement, packageName));//此ViewBind注解所在的类名类
        }
        Set<? extends Element> clickSet = roundEnv.getElementsAnnotatedWith(BindClick.class); //处理BindClick注解如上面一致
        for (Element element : clickSet) {
            if (element.getKind() != ElementKind.METHOD) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "only support class method");
                break;
            }
            TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
            String packageName = getPackageName(enclosingElement);
            String className = getClassName(enclosingElement,packageName);
            String classFullPath = packageName+"."+className;
            processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING,"process: "+classFullPath);
            ViewBindInfo viewBindInfo = targetClassMap.get(classFullPath);
            if(viewBindInfo == null){
                viewBindInfo = new ViewBindInfo();
                targetClassMap.put(classFullPath,viewBindInfo);
            }
            viewBindInfo.packageName = packageName;
            viewBindInfo.className = className;
            viewBindInfo.viewClickElementList.add(element);
            viewBindInfo.typeClassName =  ClassName.bestGuess(getClassName(enclosingElement, packageName));
        }
        buildViewBindClass(targetClassMap); //生成相应的java文件
        return false;
    }

    private void buildViewBindClass( Map<String, ViewBindInfo> targetClassMap ){
        if(targetClassMap.size() == 0){
            return;
        }
        for (Map.Entry<String, ViewBindInfo> item : targetClassMap.entrySet()) {
            String newClassName = item.getValue().className+BINDING_CLASS_SUFFIX; //java文件名
            String packageName = item.getValue().packageName;
            ClassName typeClassName = item.getValue().typeClassName;
            String methodName = "viewBind";  //方法名,跟自定义的ViewBinder接口中一致
            MethodSpec.Builder viewBindMethodBuilder = MethodSpec.methodBuilder(methodName)
                    .addModifiers(Modifier.PUBLIC)
                    .returns(TypeName.VOID)  //返回值void
                    .addAnnotation(Override.class)
                    .addParameter(typeClassName, "target", Modifier.FINAL); //添加一个参数,类型为typeClassName,形参名为target

            for(Element element : item.getValue().viewBindElementList){
                int id = element.getAnnotation(ViewBind.class).value();
                ClassName viewClass = ClassName.bestGuess(element.asType().toString());
                //生成findViewById方法
                viewBindMethodBuilder.addStatement("target.$L=($T)target.findViewById($L)",element.getSimpleName().toString(),viewClass, id);
            }

            if(item.getValue().viewClickElementList.size() > 0){
                viewBindMethodBuilder.addStatement("$T listener", ClassTypeUtils.ANDROID_ON_CLICK_LISTENER);
            }
            for(Element element : item.getValue().viewClickElementList){
                int id = element.getAnnotation(BindClick.class).id();
                // declare OnClickListener anonymous class
                TypeSpec listener = TypeSpec.anonymousClassBuilder("")
                        .addSuperinterface(ClassTypeUtils.ANDROID_ON_CLICK_LISTENER)
                        .addMethod(MethodSpec.methodBuilder("onClick")
                                .addAnnotation(Override.class)
                                .addModifiers(Modifier.PUBLIC)
                                .returns(TypeName.VOID)
                                .addParameter(ClassTypeUtils.ANDROID_VIEW, "view")
                                .addStatement("target.$N()",((ExecutableElement)element).getSimpleName())
                                .build())
                        .build();
                viewBindMethodBuilder.addStatement("listener = $L ", listener);
                // set listeners
                viewBindMethodBuilder.addStatement("target.findViewById($L).setOnClickListener(listener)", id);
            }

            MethodSpec viewBindMethod = viewBindMethodBuilder.build();
            TypeSpec viewBind = TypeSpec.classBuilder(newClassName) //生成viewBind方法
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)  //方法的修饰限定词
                    .addTypeVariable(TypeVariableName.get("T", typeClassName))
                    .addSuperinterface(ParameterizedTypeName.get(VIEW_BINDER, typeClassName))
                    .addMethod(viewBindMethod) //增加方法里面的语句
                    .build();
            //生成java文件
            JavaFile javaFile = JavaFile.builder(packageName, viewBind).build();
            try {
                javaFile.writeTo(processingEnv.getFiler());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private String getPackageName(TypeElement type) {
        return elementUtils.getPackageOf(type).getQualifiedName().toString();
    }

    private static String getClassName(TypeElement type, String packageName) {
        int packageLen = packageName.length() + 1;
        return type.getQualifiedName().toString().substring(packageLen).replace('.', '$');
    }

    //代表某个使用了ViewBind或BindClick注解类中的element注解信息
    private class ViewBindInfo{
        String packageName;
        String className;
        ClassName typeClassName; //JavaPoet类,代表某个类名
        List<Element> viewBindElementList = new ArrayList<>(); //当前类下的所有ViewBind注解元素
        List<Element> viewClickElementList = new ArrayList<>();//当前类下的所有ViewClick注解元素
    }
}

使用注解

下面我们就可以开始在activity中使用了:

public class MainActivity extends AppCompatActivity {

    @ViewBind(R.id.user_tv)
    TextView mTextView;
    @ViewBind(R.id.user_tv2)
    TextView mTextView2;

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

        ViewBind.init(this);

        mTextView.setText("hahaha!!!! annotation success!!!!");
        mTextView2.setText("hahaha!!!! annotation success!!!!");
    }

    @BindClick(id = R.id.user_tv)
    public void onClick(){
        Toast.makeText(this,"hahaha!!!! annotation click success!!!!",Toast.LENGTH_SHORT).show();
    }

}

此时编译下项目,就会发现在app的build\generated\source\apt\目录下生成一个MainActivity$$ViewBinder.java文件,这就是apt编译时自动帮我们生成的,其代码如下:

public final class MainActivity$$ViewBinder<T extends MainActivity> implements ViewBinder<MainActivity> {
  @Override
  public void viewBind(final MainActivity target) {
    target.mTextView=(TextView)target.findViewById(2131492944);
    target.mTextView2=(TextView)target.findViewById(2131492945);
    View.OnClickListener listener;
    listener = new View.OnClickListener() {
      @Override
      public void onClick(View view) {
        target.onClick();
      }
    } ;
    target.findViewById(2131492944).setOnClickListener(listener);
  }
}

这就跟我们手动代码一样,运行下,看看效果:
这里写图片描述

JavaPoet

上面用到的JavaPoet 是一个用来生成 Java源文件的Java API。

javapoet 常用的API

大多数JavaPoet的API使用的是简单的不可变的Java对象。通过建造者模式,链式方法,可变参数使得API比较友好。JavaPoet提供了(TypeSpec)用于创建类或者接口,(FieldSpec)用来创建字段,(MethodSpec)用来创建方法和构造函数,(ParameterSpec)用来创建参数,(AnnotationSpec)用于创建注解。

还可以通过字符串来构建代码块:

MethodSpec main = MethodSpec.methodBuilder("main")
    .addCode(""
        + "int total = 0;\n"
        + "for (int i = 0; i < 10; i++) {\n"
        + "  total += i;\n"
        + "}\n")
    .build();

生成的代码如下:

void main() {
  int total = 0;
  for (int i = 0; i < 10; i++) {
    total += i;
  }
}

addStatement(): 方法会给代码语句加上分号和换行
beginControlFlow() + endControlFlow() 需要一起使用,会提供换行符和缩进。
addCode() 以字符串的形式添加代码内
constructorBuilder() 生成构造器函数
addAnnotation 添加注解
addSuperinterface 给类添加实现的接口
superclass 给类添加继承的父类
ClassName.bestGuess(“类全名称”) 返回ClassName对象,这里的类全名称表示的类必须要存在,会自动导入相应的包
ClassName.get(“包名”,”类名”) 返回ClassName对象,不检查该类是否存在
TypeSpec.interfaceBuilder(“HelloWorld”)生成一个HelloWorld接口addTypeVariable(TypeVariableName.get(“T”, typeClassName))
会给生成的类加上泛型

javaPoet占位符

$L:代表的是字面量

$S:Strings

$N:Names(我们自己生成的方法名或者变量名等等)

$T: Types 这里的$T,在生成的源代码里面,也会自动导入你的类。

更多精彩Android技术可以关注我们的微信公众号,扫一扫下方的二维码或搜索关注公共号: Android老鸟


这里写图片描述

作者:Jo__yang 发表于2016/10/4 23:16:11 原文链接
阅读:95 评论:0 查看评论
Viewing all 5930 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>