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

Android doc|Getting Started| Training部分 --翻译 Working with System Permissions

$
0
0

Working with System Permissions

To protect the system’s integrity and the user’s privacy, Android runs each app in a limited access sandbox. If the app wants to use resources or information outside of its sandbox, the app has to explicitly request permission. Depending on the type of permission the app requests, the system may grant the permission automatically, or the system may ask the user to grant the permission.

This class shows you how to declare and request permissions for your app.
为了保护系统的完整性和用户的隐私,Android系统将每个app运行在一个限制访问的沙盒里。如果app想要访问他所在沙盒之外的信息或者资源,app需要明确申请权限。根据app申请的权限类型,系统也许会自动承认权限,或者系统会问用户是否给予app该权限。
这节课将向你展示如何为你的app声明和申请权限。

Declaring Permissions
Every Android app runs in a limited-access sandbox. If an app needs to use resources or information outside of its own sandbox, the app has to request the appropriate permission. You declare that your app needs a permission by listing the permission in the App Manifest.
Android系统将每个app运行在一个限制访问的沙盒里。如果app想要访问他所在沙盒之外的信息或者资源,app需要明确申请权限。你可以在App Manifest文件列出需要的权限以声明你的app所需要的权限。

Depending on how sensitive the permission is, the system might grant the permission automatically, or the device user might have to grant the request. For example, if your app requests permission to turn on the device’s flashlight, the system grants that permission automatically. But if your app needs to read the user’s contacts, the system asks the user to approve that permission. Depending on the platform version, the user grants the permission either when they install the app (on Android 5.1 and lower) or while running the app (on Android 6.0 and higher).
根据该权限的敏感度,系统也许会自动承认权限,或者系统会问用户是否给予app该权限。比如,如果你申请打开设备的闪光灯,系统会自动允许。但是如果你需要读取用户的联系人信息,系统会问用户是否允许该权限。根据运行的Android版本,用户也许会在他们安装app时(Android5.1或者更低)或者在运行app时(Android6.0或者更高)批准权限。

Determine What Permissions Your App Needs
As you develop your app, you should note when your app is using capabilities that require a permission. Typically, an app is going to need permissions whenever it uses information or resources that the app doesn’t create, or performs actions that affect the behavior of the device or other apps. For example, if an app needs to access the internet, use the device camera, or turn Wi-Fi on or off, the app needs the appropriate permission. For a list of system permissions, see Normal and Dangerous Permissions.
当你开发你的app时,当你使用到需要许可的功能的时候,你应该多加留意。通常,一个app,不管何时它使用 app没有创建的信息或者资源或者,使用后会影响设备或者其他app的动作此时它需要许可。例如,一个app需要访问网络,使用设备照相机或者打开或关闭WiFi,aoo需要恰当的许可。查看系统许可列表,详见通常和危险级别的许可。

Your app only needs permissions for actions that it performs directly. Your app does not need permission if it is requesting that another app perform the task or provide the information. For example, if your app needs to read the user’s address book, the app needs the READ_CONTACTS permission. But if your app uses an intent to request information from the user’s Contacts app, your app does not need any permissions, but the Contacts app does need to have that permission. For more information, see Consider Using an Intent.
你的app只需要它直接执行的动作的许可。app如果是请求其他app执行任务或者提供信息,就不需要许可。比如,你的app需要读取用户的通讯录,app需要READ_CONTACTS许可。但是如果你的app使用一个意图去向用户的联系人app请求信息,那就不需要READ_CONTACTS许可。更多信息参考Consider Using an Intent.

Add Permissions to the Manifest
To declare that your app needs a permission, put a <uses-permission> element in your app manifest, as a child of the top-level <manifest> element. For example, an app that needs to send SMS messages would have this line in the manifest:
声明你的app需要的权限,将<uses-permission> 元素放在你的app清单文件,作为顶级<manifest> 元素的子元素。比如,一个app需要发送信息的许可,清单文件应该有这几行:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.example.snazzyapp">

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


    <application ...>
        ...
    </application>

</manifest>

The system’s behavior after you declare a permission depends on how sensitive the permission is. If the permission does not affect user privacy, the system grants the permission automatically. If the permission might grant access to sensitive user information, the system asks the user to approve the request. For more information about the different kinds of permissions, see Normal and Dangerous Permissions.
根据该权限的敏感度,系统也许会自动承认权限,或者系统会问用户是否给予app该权限。

Requesting Permissions at Run Time

Beginning in Android 6.0 (API level 23), users grant permissions to apps while the app is running, not when they install the app. This approach streamlines the app install process, since the user does not need to grant permissions when they install or update the app. It also gives the user more control over the app’s functionality; for example, a user could choose to give a camera app access to the camera but not to the device location. The user can revoke the permissions at any time, by going to the app’s Settings screen.
从Android6.0开始(API等级23),用户会授予App许可在app运行的时候而不是安装的时候。这优化了Android app的安装流程,因为用户不再需要在安装或者升级app时授予app许可。这也给用户更多的机会控制app的功能;比如,一个用户可以给与一个app使用相机的许可而不给予定位的许可。进入app的设置界面。用户可以随时撤销app的许可。

System permissions are divided into two categories, normal and dangerous:
系统许可分为2中,正常和危险级别:

  • Normal permissions do not directly risk the user’s privacy. If your
    app lists a normal permission in its manifest, the system grants the
    permission automatically.
  • Dangerous permissions can give the app access to the user’s
    confidential data. If your app lists a normal permission in its
    manifest, the system grants the permission automatically. If you list
    a dangerous permission, the user has to explicitly give approval to
    your app.

  • 正常许可不会直接涉及用户隐私。如果你的app清单文件列出了一个正常级别的许可,系统自动许可。

  • 危险许可可能给予app访问用户机密数据的权限。如果你的app清单文件列出一个危险级别的许可,需要用户明确给予app许可。

For more information, see Normal and Dangerous Permissions.

On all versions of Android, your app needs to declare both the normal and the dangerous permissions it needs in its app manifest, as described in Declaring Permissions. However, the effect of that declaration is different depending on the system version and your app’s target SDK level:
在Android的所有版本,不管是正常还是危险级别的许可,你的app都需要在清单文件列出,就如在Declaring Permissions中所描述的一样。然而,你的定义根据不同的Android版本和SDK版本,效果可能不同。

  • If the device is running Android 5.1 or lower, or your app’s target
    SDK is 22 or lower: If you list a dangerous permission in your
    manifest, the user has to grant the permission when they install the
    app; if they do not grant the permission, the system does not install
    the app at all.
  • If the device is running Android 6.0 or higher, and your app’s target
    SDK is 23 or higher: The app has to list the permissions in the
    manifest, and it must request each dangerous permission it needs
    while the app is running. The user can grant or deny each permission,
    and the app can continue to run with limited capabilities even if the
    user denies a permission request.

  • 如果你的设备运行着Android5.1版本或者更低,或者你的app的目标SDK是22或者更低:如果你列出一个危险级别的许可,用户必须在安装app时授权;如果用户不允许,系统就不会安装app。

  • 如果你的设备运行在Android6.0或者更高版本并且你的目标SDK是23或者更高:app需要在请单文件列出他的app许可,而且他每次运行时都会向用户请求这个许可。用户可以许可或者拒绝每个许可,app可以继续以限制许可的方式运行即使用户拒绝了一个许可请求。

Note: Beginning with Android 6.0 (API level 23), users can revoke permissions from any app at any time, even if the app targets a lower API level. You should test your app to verify that it behaves properly when it’s missing a needed permission, regardless of what API level your app targets.
注意:在Android6.0开始(API23),用户可以随时撤销app的权限,即使app的目标版本是一个更低级别的API等级。不管你的目标API级别是多少,你都需要测试,保证你的app在缺失一些必须的许可也能正常运行。

This lesson describes how to use the Android Support Library to check for, and request, permissions. The Android framework provides similar methods as of Android 6.0 (API level 23). However, using the support library is simpler, since your app doesn’t need to check which version of Android it’s running on before calling the methods.
这节课描述如何使用Android支持类库检查和申请许可。Android框架在Android6.0(API23)提供相似的方法。然而,使用支持类库更简单,因为你的app在调用方法时不再需要检查Android的版本。
Check For Permissions
If your app needs a dangerous permission, you must check whether you have that permission every time you perform an operation that requires that permission. The user is always free to revoke the permission, so even if the app used the camera yesterday, it can’t assume it still has that permission today.
如果你的app需要一个危险级别的许可,你每次执行一个需要那个许可的操作时都必须检查你是否拥有那个许可。用户可以自由撤销app的许可,所以即使app昨天还可以使用摄像头,今天它就不一定还能使用。
To check if you have a permission, call the ContextCompat.checkSelfPermission() method. For example, this snippet shows how to check if the activity has permission to write to the calendar:
为了确认你是否拥有这个许可,调用ContextCompat.checkSelfPermission() 方法。比如下面这个代码片段展示了如何检查一个Activity是否拥有权限改写日历。

// Assume 假定 this Activity is the current activity
int permissionCheck = ContextCompat.checkSelfPermission(thisActivity,
        Manifest.permission.WRITE_CALENDAR);

If the app has the permission, the method returns PackageManager.PERMISSION_GRANTED, and the app can proceed with the operation. If the app does not have the permission, the method returns PERMISSION_DENIED, and the app has to explicitly ask the user for permission.
如果app拥有权限,方法返回PackageManager.PERMISSION_GRANTED,app可以继续执行操作。如果没有许可,方法返回PERMISSION_DENIED,app必须明确问用户申请这个许可。

Request Permissions
If your app needs a dangerous permission that was listed in the app manifest, it must ask the user to grant the permission. Android provides several methods you can use to request a permission. Calling these methods brings up a standard Android dialog, which you cannot customize.
如果你的app需要一个在app清单文件列出的危险级别的许可,它必须问用户来许可这个权限。Android提供了几种方法来申请权限。调用这些方法可以调出一个你无法定制的标准的Android对话框。

Explain why the app needs permissions
这里写图片描述

Figure 1. System dialog prompting the user to grant or deny a permission.
图1.系统对话框,提示用户许可或者拒绝一个权限。
In some circumstances, you might want to help the user understand why your app needs a permission. For example, if a user launches a photography app, the user probably won’t be surprised that the app asks for permission to use the camera, but the user might not understand why the app wants access to the user’s location or contacts. Before you request a permission, you should consider providing an explanation to the user. Keep in mind that you don’t want to overwhelm the user with explanations; if you provide too many explanations, the user might find the app frustrating and remove it.
在一些情况下,你可能想帮助用户理解为什么你的app需要一个许可。比如,如果一个用户启动一个摄影app,但是用户也许不理解为什么app需要访问自己的地址或者联系人。在你申请权限之前,你应该考虑提供一个解释给用户。记住不要用这些解释淹没用户。如果你提供了过多的解释,用户会觉得你的app很令人懊恼,然后就删了你的app。

One approach you might use is to provide an explanation only if the user has already turned down that permission request. If a user keeps trying to use functionality that requires a permission, but keeps turning down the permission request, that probably shows that the user doesn’t understand why the app needs the permission to provide that functionality. In a situation like that, it’s probably a good idea to show an explanation.
你可以使用的提供解释的一个方法是在用户已经拒绝了许可的时候。如果用户仍然想要那个需要权限的功能但是却拒绝授权,那也许是用户没有理解为什么app需要这个许可。在这种情况下,也许你给用户解释一下是一个好时机。

To help find situations where the user might need an explanation, Android provides a utiltity method, shouldShowRequestPermissionRationale(). This method returns true if the app has requested this permission previously and the user denied the request.
为了帮助用户在哪边需要一个解释,Android提供了一个实用方法,shouldShowRequestPermissionRationale()。如果app之前申请了这个权限并且用户拒绝了这个请求,这个方法返回true
Note: If the user turned down the permission request in the past and chose the Don’t ask again option in the permission request system dialog, this method returns false. The method also returns false if a device policy prohibits the app from having that permission.
注意:如果用户过去拒绝了许可并且在系统对话框选择了“Don’t ask again”选项,方法会返回false。如果设备本身禁止app拥有这个许可,方法也会返回false。

Request the permissions you need
If your app doesn’t already have the permission it needs, the app must call one of the requestPermissions() methods to request the appropriate permissions. Your app passes the permissions it wants, and also an integer request code that you specify to identify this permission request. This method functions asynchronously: it returns right away, and after the user responds to the dialog box, the system calls the app’s callback method with the results, passing the same request code that the app passed to requestPermissions().
如果你的app还没有它需要的许可,app必须调用一个requestPermissions()方法以申请适当的许可。你的app除了需要传递它所需要的申请的许可之外,还需要有一个标志申请的integer值。这个方法是异步的:他会立刻返回,之后用户响应那个请求许可的对话框时,系统会使用之前调用 requestPermissions()传的integer值来调用app的回调方法。
The following code checks if the app has permission to read the user’s contacts, and requests the permission if necessary:
下面的代码检查是否app拥有读取用户联系人的许可,如果需要会申请许可:

// Here, thisActivity is the current activity
if (ContextCompat.checkSelfPermission(thisActivity,
                Manifest.permission.READ_CONTACTS)
        != PackageManager.PERMISSION_GRANTED) {

    // Should we show an explanation?
    if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,
            Manifest.permission.READ_CONTACTS)) {

        // Show an expanation to the user *asynchronously* -- don't block
        // this thread waiting for the user's response! After the user
        // sees the explanation, try again to request the permission.

    } else {

        // No explanation needed, we can request the permission.

        ActivityCompat.requestPermissions(thisActivity,
                new String[]{Manifest.permission.READ_CONTACTS},
                MY_PERMISSIONS_REQUEST_READ_CONTACTS);

        // MY_PERMISSIONS_REQUEST_READ_CONTACTS is an
        // app-defined int constant. The callback method gets the
        // result of the request.
    }
}

Note: When your app calls requestPermissions(), the system shows a standard dialog box to the user. Your app cannot configure or alter that dialog box. If you need to provide any information or explanation to the user, you should do that before you call requestPermissions(), as described in Explain why the app needs permissions.
注意:当你调用requestPermissions时,系统会展现一个标准的Android对话框给用户。这个对话框,你无法做任何修改。如果你想给用户一些解释,需要在调用requestPermissions之前做,正如在前面的一节:Explain why the app needs permissions中解释的一样。

Handle the permissions request response
When your app requests permissions, the system presents a dialog box to the user. When the user responds, the system invokes your app’s onRequestPermissionsResult() method, passing it the user response. Your app has to override that method to find out whether the permission was granted. The callback is passed the same request code you passed to requestPermissions(). For example, if an app requests READ_CONTACTS access it might have the following callback method:
当你的app申请权限时,系统会向用户显示一个对话框。当用户响应完毕,系统调用你app的onRequestPermissionsResult回调方法,并将用户的回应传递进去。你的app必须重写这个方法以确定用户到底有没有准许这个许可。回调方法会使用你之前调用requestPermissions()申请许可时的那个integer值。比如,如果app申请READ_CONTACTS权限,他或许会有下面的回调方法。

@Override
public void onRequestPermissionsResult(int requestCode,
        String permissions[], int[] grantResults) {
    switch (requestCode) {
        case MY_PERMISSIONS_REQUEST_READ_CONTACTS: {
            // If request is cancelled, the result arrays are empty.
            if (grantResults.length > 0
                && grantResults[0] == PackageManager.PERMISSION_GRANTED) {

                // permission was granted, yay! Do the
                // contacts-related task you need to do.

            } else {

                // permission denied, boo! Disable the
                // functionality that depends on this permission.
            }
            return;
        }

        // other 'case' lines to check for other
        // permissions this app might request
    }
}

The dialog box shown by the system describes the permission group your app needs access to; it does not list the specific permission. For example, if you request the READ_CONTACTS permission, the system dialog box just says your app needs access to the device’s contacts. The user only needs to grant permission once for each permission group. If your app requests any other permissions in that group (that are listed in your app manifest), the system automatically grants them. When you request the permission, the system calls your onRequestPermissionsResult() callback method and passes PERMISSION_GRANTED, the same way it would if the user had explicitly granted your request through the system dialog box.
系统显示的对话框描述了你的app需要的权限组;他不会列出什么特别的权限。比如,如果你申请READ_CONTACTS权限,系统对话框仅仅说你的app需要访问用户的联系人。对于同一个权限组用户只需要批准一次。如果你的app申请了这个许可组中其他的许可(比如在清单文件中列举的),系统会自动批准。当你申请这个权限时,系统会调用你的onRequestPermissionsResult并传入PERMISSION_GRANTED,方式跟你通过系统对话框来申请许可是一样的。

Note: Your app still needs to explicitly request every permission it needs, even if the user has already granted another permission in the same group. In addition, the grouping of permissions into groups may change in future Android releases. Your code should not rely on the assumption that particular permissions are or are not in the same group.
注意:你的app仍然需要明确的申请他需要的每一个许可,即使用户可能已经在app申请同一个权限组时批准了这个许可。另外,许可的分组可能会在将来的Android版本中改变。你的代码不应该去猜测某些权限应该在或不在同一个分组中。

For example, suppose you list both READ_CONTACTS and WRITE_CONTACTS in your app manifest. If you request READ_CONTACTS and the user grants the permission, and you then request WRITE_CONTACTS, the system immediately grants you that permission without interacting with the user.
比如,假设你申请了 READ_CONTACTS 和WRITE_CONTACTS的许可。如果申请READ_CONTACTS已经被批准了,那么系统会自动许可WRITE_CONTACTS权限,而不会打扰用户。

If the user denies a permission request, your app should take appropriate action. For example, your app might show a dialog explaining why it could not perform the user’s requested action that needs that permission.
如果用户拒绝了许可,你的app应该做出恰当的行为。比如,你的app也许会向用户解释的需要权限的理由。

When the system asks the user to grant a permission, the user has the option of telling the system not to ask for that permission again. In that case, any time an app uses requestPermissions() to ask for that permission again, the system immediately denies the request. The system calls your onRequestPermissionsResult() callback method and passes PERMISSION_DENIED, the same way it would if the user had explicitly rejected your request again. This means that when you call requestPermissions(), you cannot assume that any direct interaction with the user has taken place.
当系统问用户去确认是否批准权限时,用户可以选择是否下次不再询问是否批准此权限。在那种情况下,任何时候app调用requestPermissions来申请许可时,系统直接拒绝。系统调用onRequestPermissionsResult并传入PERMISSION_DENIED,就跟用户明确拒绝许可一样。这意味着当你调用requestPermissions时,你并不能和用户直接交互。

Permissions Best Practices

It’s easy for an app to overwhelm a user with permission requests. If a user finds the app frustrating to use, or the user is worried about what the app might be doing with the user’s information, they may avoid using the app or uninstall it entirely. The following best practices can help you avoid such bad user experiences.
对于一个app来说,权限申请很容易惹恼用户。如果一个用户发现一个app用起来很烦人,或者用户担心app会对自己的信息做什么,他们也许会避免使用app或者直接整个删除app。下面的这些练习可以帮你避免这些差劲的用户体验。

Consider Using an Intent
In many cases, you can choose between two ways for your app to perform a task. You can have your app ask for permission to perform the operation itself. Alternatively, you can have the app use an intent to have another app perform the task.
在很多情况下,你可以有两种方式来执行一个任务。你可以让你自己的app来执行这个操作,或者,你可以让app使用一个Intent去打开另一个app,让另外的app执行这个任务。

For example, suppose your app needs to be able to take pictures with the device camera. Your app can request the CAMERA permission, which allows your app to access the camera directly. Your app would then use the camera APIs to control the camera and take a picture. This approach gives your app full control over the photography process, and lets you incorporate the camera UI into your app.
比如,假设你的app需要能够使用设备的摄像头拍照。你的app可以申请CAMERA 权限,这样直接可以让你的app有权限拍照。你的app之后可以用camera的API来控制摄像头拍照。这个方法让你的app完全控制拍照过程,并且让你把拍照的UI界面包含进你的app。

However, if you don’t need such complete control, you can use an ACTION_IMAGE_CAPTURE intent to request an image. When you send the intent, the system prompts the user to choose a camera app (if there isn’t already a default camera app). The user takes a picture with the selected camera app, and that app returns the picture to your app’s onActivityResult() method.
然而,如果你不需要完全的控制,你可以使用一个 ACTION_IMAGE_CAPTURE的意图去申请一个图片。当你发送这个意图,系统会提示用户选择一个照相的app(如果还没有一个默认的照相app)。用户会用选择的app拍张照片,那个拍照app通过onActivityResult()方法返回照片。

Similarly, if you need to make a phone call, access the user’s contacts, and so on, you can do that by creating an appropriate intent, or you can request the permission and access the appropriate objects directly. There are advantages and disadvantages to each approach.
相似的,如果你需要打电话,访问用户的通讯录或者其他什么权限,你可以通过创建一个恰当的意图来实现,或者你可以直接请求许可,并直接得到相应的对象。这两种方法都有利有弊。

If you use permissions:
如果你使用权限:
Your app has full control over the user experience when you perform the operation. However, such broad control adds to the complexity of your task, since you need to design an appropriate UI.
The user is prompted to give permission once, either at run time or at install time (depending on the user’s Android version). After that, your app can perform the operation without requiring additional interaction from the user. However, if the user doesn’t grant the permission (or revokes it later on), your app becomes unable to perform the operation at all.
当你执行操作时你的app可以完全控制用户行为。然而,这个完全的控制是你的任务复杂化,因为你需要设计合适的UI。用户可能在运行app时或者安装时(依赖用户的Android版本) 被提示来给予权限 。在那之后,如果用户不授予权限(或者之后收回权限),你的app就会完全无法执行那个操作。
If you use an intent:
如果你使用意图:
You do not have to design the UI for the operation. The app that handles the intent provides the UI. However, this means you have no control over the user experience. The user could be interacting with an app you’ve never seen.
你不需要为那个动作设计UI。处理你发送来的意图的app会提供UI。然而,这意味着你不能控制用户的操作。用户可能与一个你从来不知道的app交互。
If the user does not have a default app for the operation, the system prompts the user to choose an app. If the user does not designate a default handler, they may have to go through an extra dialog every time they perform the operation.
如果用户没有默认的app去执行那个操作,系统会提示用户去选择一个app。如果用户没有指定一个默认的handler,这就需要每天都通过一个对话框来执行那个操作。
Only Ask for Permissions You Need
Every time you ask for a permission, you force the user to make a decision. You should minimize the number of times you make these requests. If the user is running Android 6.0 (API level 23) or later, every time the user tries some new app feature that requires a permission, the app has to interrupt the user’s work with a permission request. If the user is running an earlier version of Android, the user has to grant every one of the app’s permissions when installing the app; if the list is too long or seems inappropriate, the user may decide not to install your app at all. For these reasons, you should minimize the number of permissions your app needs.
每次你申请一个权限,你强制用户去做一个决定。你应该让这种情况发生次数最小化。如果用户使用的Android版本是6.0(API23)或者更新的版本,每次用户尝试一下新的需要权限的app功能,app需要打断用户的操作来进行权限许可请求。如果用户用的是低于6.0的版本,用户安装时需要许可这些权限;如果用户发现这个许可的list很长或者看起来不合适,用户可能觉得不会安装app。因为这些原因,你应该将你的权限申请数最小化。

Quite often your app can avoid requesting a permission by using an intent instead. If a feature is not a core part of your app’s functionality, you should consider handing the work over to another app, as described in Consider Using An Intent.
你可以避免直接请求权限,而通过发送一个intent的情况是相当常见的。如果一个功能并非你的app功能的核心部分,你应该考虑在其他app处理这个工作,正如在Consider Using An Intent一节所述。
Don’t Overwhelm the User
If the user is running Android 6.0 (API level 23) or later, the user has to grant your app its permissions while they are running the app. If you confront the user with a lot of requests for permissions at once, you may overwhelm the user and cause them to quit your app. Instead, you should ask for permissions as you need them.
如果用户的版本是Android6.0或者之后的,用户需要在app运行时批准权限。如果你让用户一下子面对许多的请求,你可能惹怒用户,导致他们卸载你的app。取而代之,你应该在你需要权限的时候在请求权限。

In some cases, one or more permissions might be absolutely essential to your app. It might make sense to ask for all of those permissions as soon as the app launches. For example, if you make a photography app, the app would need access to the device camera. When the user launches the app for the first time, they won’t be surprised to be asked for permission to use the camera. But if the same app also had a feature to share photos with the user’s contacts, you probably should not ask for the READ_CONTACTS permission at first launch. Instead, wait until the user tries to use the “sharing” feature and ask for the permission then.
在一些情况下,一个或多个权限可能对你的app相当重要。此时在app一运行起来就问用户批准这些权限可能说得通。比如,如果你在做一个摄影app,app可能需要使用摄像头的权限。当用户第一次运行app时,当被要求来许可照相机权限时他们不会感到意外。但是如果同一个app,有一个分享照片给用户联系人的功能,你也许不应该在第一次运行时就问用户申请READ_CONTACTS 权限。相反的,直到用户尝试使用“分享”功能是再问用户来批准这个权限。

If your app provides a tutorial, it may make sense to request the app’s essential permissions at the end of the tutorial sequence.
如果你的app提供一个小提示之类的东西,在最后申请app的必要权限,也许讲得通。
Explain Why You Need Permissions
The permissions dialog shown by the system when you call requestPermissions() says what permission your app wants, but doesn’t say why. In some cases, the user may find that puzzling. It’s a good idea to explain to the user why your app wants the permissions before calling requestPermissions().
当你调用requestPermissions方法来表名你的app需要的权限时,系统会显示权限对话框 ,但是并不会说为什么需要这些权限。在一些情况下,用户就会感到莫名其妙。在你调用requestPermissions方法前解释一下你为什么需要这些权限也许是一个好主意。

For example, a photography app might want to use location services so it can geotag the photos. A typical user might not understand that a photo can contain location information, and would be puzzled why their photography app wants to know the location. So in this case, it’s a good idea for the app to tell the user about this feature before calling requestPermissions().
比如,一个摄影app也许会需要定位服务,以便标记照片的位置。一个普通的用户或许不会明白为什么拍个照片需要包含位置信息,并且会困惑为什么摄影app需要知道地址信息。
One way to inform the user is to incorporate these requests into an app tutorial. The tutorial can show each of the app’s features in turn, and as it does this, it can explain what permissions are needed. For example, the photography app’s tutorial could demonstrate its “share photos with your contacts” feature, then tell the user that they need to give permission for the app to see the user’s contacts. The app could then call requestPermissions() to ask the user for that access. Of course, not every user is going to follow the tutorial, so you still need to check for and request permissions during the app’s normal operation.
提示用户为什么app需要包含其他的许可的一个方式。辅助性提示文字可以依次向用户展示app的功能,如果这么做了,就可以解释为什么需要这个权限。比如摄影app的辅助文字可以写成“分享你的照片给你的联系人”功能,然后高速用户他们需要访问联系人。当然,不是每一个用户都会跟着导读文字,所以在app通常操作时你仍然需要检查并且申请权限。
Test for Both Permissions Models
Beginning with Android 6.0 (API level 23), users grant and revoke app permissions at run time, instead of doing so when they install the app. As a result, you’ll have to test your app under a wider range of conditions. Prior to Android 6.0, you could reasonably assume that if your app is running at all, it has all the permissions it declares in the app manifest. Under the new permissions model, you can no longer make that assumption.
从Android6.0开始,用户在app运行时可以收回权限,而不是在安装app的时候了。因此,你将必须在更多的条件下测试你的app。Android6.0之前,你可以确信,如果你的app在运行,那么app久拥有它在清单文件所列举的权限。在新的权限模式下,你不能再做这种断言了。

The following tips will help you identify permissions-related code problems on devices running API level 23 or higher:
下面的贴士将帮助你解决在Android6.0版本,区分依赖许可的代码问题。
- Identify your app’s current permissions and the related code paths. 区分你的app现在的权限和相关的代码部分
- Test user flows across permission-protected services and data.测试权限被限制时的服务和数据
- Test with various combinations of granted or revoked permissions. For
example, a camera app might list CAMERA, READ_CONTACTS, and
ACCESS_FINE_LOCATION in its manifest. You should test the app with
each of these permissions turned on and off, to make sure the app can
handle all permission configurations gracefully. Remember, beginning
with Android 6.0 the user can turn permissions on or off for any app,
even an app that targets API level 22 or lower.测试许可和拒绝权限的多种组合。比如,一个摄影app也许在清单文件列举了CAMERA,READ_CONTACTS,ACCESS_FINE_LOCATION 。你应该测试你的app,在它有和没有的情况下,以确保app可以完美地处理所有权限配置。记住,在Android6.0,用户随时可以同意或者拒绝app的许可。
- Use the adb tool to manage permissions from the command line:使用adb工具来管理权限的命令行命令:
- List permissions and status by group:分组列举许可及其状态:

$ adb shell pm list permissions -d -g
    - Grant or revoke one or more permissions:许可或者拒绝一个或者多个权限:
$ adb shell pm [grant|revoke] <permission-name> ...
  • Analyze your app for services that use permissions.分析你的app的使用到权限的服务。
作者:u011109881 发表于2016/9/24 15:09:40 原文链接
阅读:175 评论:0 查看评论

IOS 之 Quartz 2D 绘图(下)

$
0
0

由于最近一段时间比较忙,一直没有时间更新博客,今天总算是抽空为上次的 IOS 之 Quartz 2D 绘图(上)写一个“续集”。上次主要介绍了有关 Quartz2D 的图形上下文、drawRect方法、图形上下文栈等概念,并对画线段作了相关说明。本次将在上次博客的基础上,用 Quartz2D 画出更多种类的图形,并作相应分析。

1.画矩形
这里写图片描述
注意点:通过系统的AddRect方法所绘制出来的矩形的长和宽分别是和X轴和Y轴平行的,也就是说不能是斜着的的样式,而要使矩形斜着显示,要么旋转View,要么用上篇博客中用连接线段的方式)

2.画三角形
这里写图片描述
解析:画三角形的前两条边用连接线段的方式即可,最后一条边通过关闭路径的方法连接第一个和最后一个点。

3.画椭圆形
这里写图片描述
解析:当椭圆的长轴和短轴相等时即是圆形。

4.画弧形
这里写图片描述
解析

void CGContextAddArc (
   CGContextRef c,       //表示图形上下文
   CGFloat x,            //圆心X轴坐标
   CGFloat y,            //圆心Y轴坐标
   CGFloat radius,       //半径 
   CGFloat startAngle,   //开始角度(弧度)
   CGFloat endAngle,     //结束角度(弧度)
   int clockwise         //圆弧的伸展方向(0:顺时针, 1:逆时针)
);

5.画贝塞尔曲线
这里写图片描述
解析:贝塞尔曲线是一种应用广泛的曲线,一阶贝塞尔曲线就是线段,二阶贝塞尔曲线是三点确定的抛物线。
起始点、终止点(也称锚点)、控制点。通过调整控制点,贝塞尔曲线的形状会发生变化。

在IOS中,1个控制点的描述如下(前两个坐标值为控制点坐标):

CGContextAddQuadCurveToPoint(context, 150, 200, 200, 100);

2个控制点的描述如下(前四个坐标值为控制点坐标):

CGContextAddCurveToPoint(context, 120, 100, 180, 50, 190, 190);

到此,介绍了一些常见图形的画法,通过对这些常见图形的组合,可以达到绘制更复杂图形的目的,后续会不断更新,不断丰富内容!

作者:huangfei711 发表于2016/9/24 15:23:07 原文链接
阅读:181 评论:0 查看评论

《React-Native系列》34、 ReactNative的那些坑

$
0
0

梳理了下ReactNative中的一些坑,你踩没踩过,它就在那里。

  • 1、fetch

fetch没有提供超时时间,设置timeout貌似没有作用。


标红的地方不能调用response.json() 或 .text()方法,哪怕是通过console.log()输出也不行,如果想查看数据,只能alert。


  • 2、Image标签

使用本地资源图片的时候需要制定宽度和高度,使用网络资源没有限制。

<Image source={{uri:loadgif}} style={{width:20,height:20}}/>


  • 3、Text标签

iOS下的padding和lineHeight属性都正常,在Android下无效。

解决方案:将padding换成margin,lineHeight改用marginTop为负值


  • 4、TextInput标签

textAlign属性:

iOS下为:auto left right center justify
Android下为:start center end

TextInput标签在Android下默认大概有10dp的paddingLeft和paddingRight
TextInput在Android下有黑色边框和选中黄色,设置underlineColorAndroid='rgba(0,0,0,0)' ,文本框的下划线颜色(译注:如果要去掉文本框的边框,请将此属性设为透明transparent)。


  • 5、ScrollView

Android下 AcrollView嵌套继承了ScrollView的组件内部滚动失效

解决方案:内部ListView、ScrollView、WebView 固定高度


  • 6、lineHeight 属性

iOS会显示在lineHeight的最低端
Android会显示在lieHeight的最顶端
两端都不支持在web里的 height=lineHeight   居中


  • 7、Gif图片格式的支持

iOS下可以直接使用Gif格式的图片

Android下需要特殊处理,桥接的方式或者图片切割轮播,可以参考:http://blog.csdn.net/codetomylaw/article/details/52280828


当然,坑远远不止这些。

作者:hsbirenjie 发表于2016/9/24 18:21:51 原文链接
阅读:199 评论:0 查看评论

Android简易实战教程--第三十一话《自定义土司》

$
0
0

最近有点忙,好几天不更新博客了。今天就简单点,完成自定义土司。

主布局文件代码:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="76dp"
        android:text="显示普通的通知" />

    <Button
        android:id="@+id/button2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/button1"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="62dp"
        android:text="自定义通知" />

</RelativeLayout>

一个用于显示原生土司,一个用于显示自定义土司。

自定义土司的布局:一张图片,一段文本。都是自定义上去

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/toast_layout_root"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="#44ff4400"
    android:orientation="horizontal"
    android:padding="10dp" >

    <ImageView
        android:id="@+id/image"
        android:layout_width="wrap_content"
        android:layout_height="fill_parent"
        android:layout_marginRight="10dp" />

    <TextView
        android:id="@+id/text"
        android:layout_width="wrap_content"
        android:layout_height="fill_parent"
        android:textColor="#FFF" />

</LinearLayout>

主活动代码:

package com.example.android_toast2;

import android.os.Bundle;
import android.app.Activity;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;

public class MainActivity extends Activity {

	private Button button;
	private Button button2;

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		button = (Button) this.findViewById(R.id.button1);
		button.setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View v) {
				// TODO Auto-generated method stub
				Toast toast = Toast.makeText(MainActivity.this, "提示信息",
						Toast.LENGTH_LONG);
				toast.setGravity(Gravity.CENTER, 0, 0);
				toast.show();
			}
		});

		button2 = (Button) this.findViewById(R.id.button2);
		button2.setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View v) {
				// TODO Auto-generated method stub
				Toast toast = new Toast(MainActivity.this);
				// 加载自定义布局
				View view = LayoutInflater.from(MainActivity.this).inflate(
						R.layout.dialog, null);
				ImageView imageView = (ImageView) view.findViewById(R.id.image);
				imageView.setImageResource(R.drawable.a);
				TextView textView = (TextView) view.findViewById(R.id.text);
				textView.setText("自定义的吐司通知");
				toast.setDuration(Toast.LENGTH_LONG);//显示时长
				toast.setGravity(Gravity.CENTER, 0, 0);//土司位于父布局的位置
				// 加载自定义的布局
				toast.setView(view);
				toast.show();
				//统一UI风格,需要自定义的通知,声明在一个类中,可以反复调用

			}
		});
	}
}

运行看看效果:


作者:qq_32059827 发表于2016/9/24 22:13:53 原文链接
阅读:188 评论:0 查看评论

Android Multimedia框架总结(十三)CodeC部分之OpenMAX框架初识及接口与适配层实现

$
0
0

转载请把头部出处链接和尾部二维码一起转载,本文出自逆流的鱼yuiop:http://blog.csdn.net/hejjunlin/article/details/52629598

前言:上篇中介绍OMX事件回调,从今天开始,走入Codec部分之OpenMAX框架里。看下今天的Agenda如下:

  • 一张图回顾音视频同步
  • 一张图看清OpenMAX在Android系统中位置
  • OpenMAX是什么
  • OpenMax IL简介
  • OpenMax IL结构
  • Android中OpenMax的使用情况
  • OpenMax的接口与实现
  • Android中OpenMax的适配层
  • mp3,aac解码时序图

一张图回顾音视频同步


这里写图片描述

一张图看清OpenMAX在Android系统中位置:


这里写图片描述

OpenMAX是什么?

以下是官网翻译:

  • OpenMAX™ 是无授权费的,跨平台的应用程序接口API,通过使媒体加速组件能够在开发、集成和编程环节中实现跨多操作系统和处理器硬件平台,提供全面的流媒体编解码器和应用程序便携化。
  • OpenMAX API将会与处理器一同提供,以使库和编解码器开发者能够高速有效地利用新器件的完整加速潜能 - 无需担心其底层的硬件结构。
    OpenMAX分为3层:
    • 第一层:OpenMax DL(Development Layer,开发层)
      • OpenMax DL定义了一个API,它是音频、视频和图像功能的集合。供应商能够在一个新的处理器上实现并优化,然后编解码供应商使用它来编写更广泛的编解码器功能。它包括音频信号的处理功能,如FFT和filter,图像原始处理,如颜色空间转换、视频原始处理,以实现例如MPEG-4、H.264、MP3、AAC和JPEG等编解码器的优化。
    • 第二层:OpenMax IL(Integration Layer,集成层)
      • OpenMax IL作为音频、视频和图像编解码器能与多媒体编解码器交互,并以统一的行为支持组件(例如,资源和皮肤)。这些编解码器或许是软硬件的混合体,对用户是透明的底层接口应用于嵌入式、移动设备。它提供了应用程序和媒体框架,透明的。S编解码器供应商必须写私有的或者封闭的接口,集成进移动设备。IL的主要目的是使用特征集合为编解码器提供一个系统抽象,为解决多个不同媒体系统之间轻便性的问题。
    • 第三层:OpenMax AL(Appliction Layer,应用层)
      • OpenMax AL API在应用程序和多媒体中间件之间提供了一个标准化接口,多媒体中间件提供服务以实现被期待的API功能。

本文出自逆流的鱼yuiop:http://blog.csdn.net/hejjunlin/article/details/52629598

OpenMax的三个层次如图所示(来自OpenMax官网):


这里写图片描述

提示:在实际的应用中,OpenMax的三个层次中使用较多的是OpenMax IL集成层,由于操作系统到硬件的差异和多媒体应用的差异,OpenMax的DL和AL层使用相对较少。

OpenMax IL简介

  • OpenMax IL 处在中间层的位置,OpenMAX IL 作为音频,视频和图像编解码器 能与多媒体编解码器交互,并以统一的行为支持组件(例如资源和皮肤)。这些编解码器或许是软硬件的混合体,对用户是的底层接口应用于嵌入式或 / 和移动设备。它提供了应用程序和媒体框架, 透明的。本质上不存在这种标准化的接口,编解码器供 应商必须写私有的或者封闭的接口,集成进移动设备。 IL 的主要目的 是使用特征集合为编解码器提供一个系统抽象,为解决多个不同媒体系统之间轻便性的问题。

  • OpenMax IL 的目的就是为硬件平台的图形及音视频提供一个抽象层,可以为上层的应用提供一个可跨平台的支撑。这一点对于跨平台的媒体应用来说十分重要。不同厂商的芯片底层的音视频接口虽然功能上大致相同,但是接口设计及用法上各有不同,而且相差很多。你要想让自己开发的媒体 应用完美的运行在不同的硬件厂商平台上,就得适应不同芯片的底层解码接口。这个对于应用开发来说十分繁琐。所以就需要类似于OpenMax IL 这种接口规范。应用假如涉及到音视频相关功能时,只需调用这些标准的接口,而不需要关心接口下方硬件相关的实现。假如换了硬件平台时,只需要把接口层与硬件适配好了就行了。上层应用不需要频繁改动。你可以把OpenMax IL看作是中间件中的porting层接口,但是现在中间件大部分都是自家定义自己的。

本文出自逆流的鱼yuiop:http://blog.csdn.net/hejjunlin/article/details/52629598

OpenMax IL结构

OpenMax IL的层次结构如图:


这里写图片描述

  • 虚线中的内容是OpenMax IL层的内容,其主要实现了OpenMax IL中的各个组件(Component)。对下层,OpenMax IL可以调用OpenMax DL层的接口,也可以直接调用各种Codec实现。对上层,OpenMax IL可以给OpenMax AL 层等框架层(Middleware)调用,也可以给应用程序直接调用。

OpenMax IL主要内容如下所示。

  • 客户端(Client):OpenMax IL的调用者
  • 组件(Component):OpenMax IL的单元,每一个组件实现一种功能
  • 端口(Port):组件的输入输出接口
  • 隧道化(Tunneled):让两个组件直接连接的方式

组件、端口、隧道化思想和GStreamer (一种多媒体框架)中的 pipeline 十分类似。
Component实现单一功能、或是Source、Host、Accelerator和Sink。
Port 是 Component对外的输入输出口。
通过Tunneled 将单一Component串联起来形成一个完整功能。
OpenMax Core是辅助各个组件运行的部分

OpenMax IL 的基本运作过程如图

这里写图片描述

如图所示,openMAX IL的客户端,通过调用四个OpenMAX IL组件,实现了一个功能。四个组件分别是Source组件、Host组件、Accelerator组件和Sink组件。Source组件只有一个输出端口;而Host组件有一个输入端口和一个输出端口;Accelerator组件具有一个输入端口,调用了硬件的编解码器,加速主要体现在这个环节上。Accelerator组件和Sink组件通过私有通讯方式在内部进行连接,没有经过明确的组件端口。
OpenMAL IL在使用的时候,其数据流也有不同的处理方式:

  • 既可以经由客户端,也可以不经由客户端。
  • 图中,Source组件到Host组件的数据流就是经过客户端的;
  • 而Host组件到Accelerator组件的数据流就没有经过客户端,使用了隧道化的方式;
  • Accelerator组件和Sink组件甚至可以使用私有的通讯方式。

OpenMax Core是辅助各个组件运行的部分,它通常需要完成各个组件的初始化等工作,在真正运行过程中,重点是各个OpenMax IL的组件,OpenMax Core不是重点,也不是标准。

  • OpenMAL IL的组件是OpenMax IL实现的核心内容,一个组件以输入、输出端口为接口,端口可以被连接到另一个组件上。外部对组件可以发送命令,还进行设置/获取参数、配置等内容。组件的端口可以包含缓冲区(Buffer)的队列。
  • 组件的处理的核心内容是:通过输入端口消耗Buffer,通过输出端口填充Buffer,由此多组件相联接可以构成流式的处理。

本文出自逆流的鱼yuiop:http://blog.csdn.net/hejjunlin/article/details/52629598

OpenMAL IL中一个组件的结构如图:


这里写图片描述

组件的功能和其定义的端口类型密切相关,通常情况下:

  • 只有一个输出端口的,为Source组件;
  • 只有一个输入端口的,为Sink组件;
  • 有多个输入端口,一个输出端口的为Mux组件;
  • 有一个输入端口,多个输出端口的为DeMux组件;
  • 输入输出端口各一个组件的为中间处理环节,这是最常见的组件。
  • 端口具体支持的数据也有不同的类型。例如,对于一个输入、输出端口各一个组件,其输入端口使用MP3格式的数据,输出端口使用PCM格式的数据,那么这个组件就是一个MP3解码组件。
  • 隧道化(Tunneled)是一个关于组件连接方式的概念。通过隧道化可以将不同的组件的一个输入端口和一个输出端口连接到一起,在这种情况下,两个组件的处理过程合并,共同处理。尤其对于单输入和单输出的组件,两个组件将作为类似一个使用。

Android中OpenMax的使用情况

  • Android系统的一些部分对OpenMax IL层进行使用,基本使用的是标准OpenMax IL层的接口,只是进行了简单的封装。标准的OpenMax IL实现很容易以插件的形式加入到Android系统中。
  • Android的多媒体引擎OpenCore和StageFright都可以使用OpenMax作为多媒体编解码的插件,只是没有直接使用OpenMax IL层提供的纯C接口,而是对其进行了一定的封装(C++封装)。
  • 在Android2.x版本之后,Android的框架层也对OpenMax IL层的接口进行了封装定义,甚至使用Android中的Binder IPC机制。Stagefright使用了这个层次的接口,OpenCore没有使用。
  • OpenCore使用OpenMax IL层作为编解码插件在前,Android框架层封装OpenMax接口在后面的版本中才引入。

Android OpenMax实现的内容

  • Android中使用的主要是OpenMax的编解码功能。虽然OpenMax也可以生成输入、输出、文件解析-构建等组件,但是在各个系统(不仅是Android)中使用的最多的还是编解码组件。媒体的输入、输出环节和系统的关系很大,引入OpenMax标准比较麻烦;文件解析-构建环节一般不需要使用硬件加速。编解码组件也是最能体现硬件加速的环节,因此最常使用。
  • 在Android中实现OpenMax IL层和标准的OpenMax IL层的方式基本,一般需要实现以下两个环节。
    • 编解码驱动程序:位于Linux内核空间,需要通过Linux内核调用驱动程序,通常使用非标准的驱动程序。
    • OpenMax IL层:根据OpenMax IL层的标准头文件实现不同功能的组件。
      Android中还提供了OpenMax的适配层接口(对OpenMax IL的标准组件进行封装适配),它作为Android本地层的接口,可以被Android的多媒体引擎调用。

OpenMax的接口与实现

OpenMax IL层的接口(1)

OpenMax IL层的接口定义由若干个头文件组成,这也是实现它需要实现的内容,它们的基本描述如下所示。

这里写图片描述

提示:OpenMax标准只有头文件,没有标准的库,设置没有定义函数接口。对于实现者,需要实现的主要是包含函数指针的结构体。
其中,OMX_Component.h中定义的OMX_COMPONENTTYPE结构体是OpenMax IL层的核心内容,表示一个组件,其内容如下所示:

这里写图片描述
这里写图片描述
这里写图片描述

OpenMax IL层的接口(2)

  • OMX_COMPONENTTYPE结构体实现后,其中的各个函数指针就是调用者可以使用的内容。各个函数指针和OMX_core.h中定义的内容相对应。
    EmptyThisBuffer和FillThisBuffer是驱动组件运行的基本的机制,前者表示让组件消耗缓冲区,表示对应组件输入的内容;后者表示让组件填充缓冲区,表示对应组件输出的内容。
  • UseBuffer,AllocateBuffer,FreeBuffer为和端口相关的缓冲区管理函数,对于组件的端口有些可以自己分配缓冲区,有些可以使用外部的缓冲区,因此有不同的接口对其进行操作。
  • SendCommand表示向组件发送控制类的命令。GetParameter,SetParameter,GetConfig,SetConfig几个接口用于辅助的参数和配置的设置和获取。
    ComponentTunnelRequest用于组件之间的隧道化连接,其中需要制定两个组件及其相连的端口。
  • ComponentDeInit用于组件的反初始化。
    提示:OpenMax函数的参数中,经常包含OMX_IN和OMX_OUT等宏,它们的实际内容为空,只是为了标记参数的方向是输入还是输出。
    OMX_Component.h中端口类型的定义为OMX_PORTDOMAINTYPE枚举类型,内容如下所示:

这里写图片描述

音频类型,视频类型,图像类型,其他类型是OpenMax IL层此所定义的四种端口的类型。
端口具体内容的定义使用OMX_PARAM_PORTDEFINITIONTYPE类(也在OMX_Component.h中定义)来表示,其内容如下所示:

这里写图片描述

对于一个端口,其重点的内容如下:

  • 端口的方向(OMX_DIRTYPE):包含OMX_DirInput(输入)和- -OMX_DirOutput(输出)两种
  • 端口分配的缓冲区数目和最小缓冲区数目
  • 端口的类型(OMX_PORTDOMAINTYPE):可以是四种类型
  • 端口格式的数据结构:使用format联合体来表示,具体由四种不同类型来表示,与端口的类型相对应
    OMX_AUDIO_PORTDEFINITIONTYPE,OMX_VIDEO_PORTDEFINITIONTYPE,OMX_IMAGE_PORTDEFINITIONTYPE和OMX_OTHER_PORTDEFINITIONTYPE等几个具体的格式类型,分别在OMX_Audio.h,OMX_Video.h,OMX_Image.h和OMX_Other.h这四个头文件中定义。
    OMX_BUFFERHEADERTYPE是在OMX_Core.h中定义的,表示一个缓冲区的头部结构。

OMX_Core.h中定义的枚举类型OMX_STATETYPE命令表示OpenMax的状态机,内容如下所示:

这里写图片描述

OpenMax组件的状态机可以由外部的命令改变,也可以由内部发生的情况改变。OpenMax IL组件的状态机的迁移关系如图所示:

这里写图片描述

OMX_Core.h中定义的枚举类型OMX_COMMANDTYPE表示对组件的命令类型,内容如下所示:

这里写图片描述

OMX_COMMANDTYPE类型在SendCommand调用中作为参数被使用,其中OMX_CommandStateSet就是改变状态机的命令。

OpenMax IL实现的内容

对于OpenMax IL层的实现,一般的方式并不调用OpenMax DL层。具体实现的内容就是各个不同的组件。OpenMax IL组件的实现包含以下两个步骤。

  • 组件的初始化函数:硬件和OpenMax数据结构的初始化,一般分成函数指针初始化、私有数据结构的初始化、端口的初始化等几个步骤,使用其中的pComponentPrivate成员保留本组件的私有数据为上下文,最后获得填充完成OMX_COMPONENTTYPE类型的结构体。
  • OMX_COMPONENTTYPE类型结构体的各个指针实现:实现其中的各个函数指针,需要使用私有数据的时候,从其中的pComponentPrivate得到指针,转化成实际的数据结构使用。端口的定义是OpenMax IL组件对外部的接口。OpenMax IL常用的组件大都是输入和输出端口各一个。对于最常用的编解码(Codec)组件,通常需要在每个组件的实现过程中,调用硬件的编解码接口来实现。在组件的内部处理中,可以建立线程来处理。OpenMax的组件的端口有默认参数,但也可以在运行时设置,因此一个端口也可以支持不同的编码格式。音频编码组件的输出和音频编码组件的输入通常是原始数据格式(PCM格式),视频编码组件的输出和视频编码组件的输入通常是原始数据格式(YUV格式)。
    提示:在一种特定的硬件实现中,编解码部分具有相似性,因此通常可以构建一个OpenMax组件的”基类”或者公共函数,来完成公共性的操作。

Android中OpenMax的适配层

Android中的OpenMax适配层的接口在frameworks/base/include/media/目录中的IOMX.h文件定义,其内容如下所示:

这里写图片描述
这里写图片描述

  • IOMX表示的是OpenMax的一个组件,根据Android的Binder IPC机制,BnOMX继承IOMX,实现者需要继承实现BnOMX。IOMX类中,除了和标准的OpenMax的GetParameter,SetParameter,GetConfig,SetConfig,SendCommand,UseBuffer,AllocateBuffer,FreeBuffer,FillThisBuffer和EmptyThisBuffer等接口之外,还包含了创造渲染器的接口createRenderer(),创建的接口为IOMXRenderer类型。
  • IOMX中只有第一个createRenderer()函数是纯虚函数,第二个的createRenderer()函数和createRendererFromJavaSurface()通过调用第一个createRenderer()函数实现。

IOMXRenderer类表示一个OpenMax的渲染器,其定义如下所示:

这里写图片描述

  • IOMXRenderer只包含了一个render接口,其参数类型IOMX::buffer_id实际上是void*,根据不同渲染器使用不同的类型。
  • 在IOMX.h文件中,另有表示观察器类的IOMXObserver,这个类表示OpenMax的观察者,其中只包含一个onMessage()函数,其参数为omx_message接口体,其中包含Event事件类型、FillThisBuffer完成和EmptyThisBuffer完成几种类型。

提示:Android中OpenMax的适配层是OpenMAX IL层至上的封装层,在Android系统中被StageFright调用,也可以被其他部分调用。

mp3 aac格式解码时序图

这里写图片描述

第一时间获得博客更新提醒,以及更多android干货,源码分析,欢迎关注我的微信公众号,扫一扫下方二维码或者长按识别二维码,即可关注。

这里写图片描述

如果你觉得好,随手点赞,也是对笔者的肯定,也可以分享此公众号给你更多的人,原创不易

作者:hejjunlin 发表于2016/9/24 22:19:31 原文链接
阅读:196 评论:0 查看评论

View绘制详解,从LayoutInflater谈起

$
0
0

自定义View算是Android开发中的重中之重了,很多小伙伴可能或多或少都玩过自定义View,对View的绘制流程也有一定的理解。那么现在我想通过几篇博客来详细介绍View的绘制流程,以便使我们更加深刻的理解自定义View。

如果小伙伴们还没用过自定义View或者用的不多的话,那么建议通过以下几篇文章先来热个身:

1.Android自定义View之ProgressBar出场记

2.android自定义View之NotePad出鞘记

3.android自定义View之仿通讯录侧边栏滑动,实现A-Z字母检索

4.android自定义View之钟表诞生记

5.自己动手,丰衣足食!一大波各式各样的ImageView来袭!

6.Android开发之Path类使用详解,自绘各种各样的图形!

7.自定义View

OK,View的世界浩如烟海,不过凡事只要抓住纲就好解决,所谓提纲挈领嘛。那我这里就打算从LayoutInflater这个布局管理器开始我们的View绘制详解之旅。so,开始吧!

使用LayoutInflater加载一个布局文件,一般情况下,我们通过下面的方式来获取一个LayoutInflater实例:

LayoutInflater inflater = LayoutInflater.from(this);


点到这个方法里边,我们发现这里实际上是调用了系统服务,如下:

    public static LayoutInflater from(Context context) {
        LayoutInflater LayoutInflater =
                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        if (LayoutInflater == null) {
            throw new AssertionError("LayoutInflater not found.");
        }
        return LayoutInflater;
    }

看到这里小伙伴们应该明白了,获取LayoutInflater我们还有另外一种方式:

LayoutInflater layoutInflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);

两种方式都可以获取一个LayoutInflater实例,拿到实例后,接下来就可以加载布局了,加载布局时我们使用inflate方法,该方法有两个重载的方法,一个是两个参数,一个是三个参数,关于这两个方法的差异小伙伴们如果还不懂可以移步这里三个案例带你看懂LayoutInflater中inflate方法两个参数和三个参数的区别,不论是两个参数还是三个参数,inflate方法最后都会到达这里:

    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

            final Context inflaterContext = mContext;
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            Context lastContext = (Context) mConstructorArgs[0];
            mConstructorArgs[0] = inflaterContext;
            View result = root;

            try {
                // Look for the root node.
                int type;
                while ((type = parser.next()) != XmlPullParser.START_TAG &&
                        type != XmlPullParser.END_DOCUMENT) {
                    // Empty
                }

                if (type != XmlPullParser.START_TAG) {
                    throw new InflateException(parser.getPositionDescription()
                            + ": No start tag found!");
                }

                final String name = parser.getName();
                
                if (DEBUG) {
                    System.out.println("**************************");
                    System.out.println("Creating root view: "
                            + name);
                    System.out.println("**************************");
                }

                if (TAG_MERGE.equals(name)) {
                    if (root == null || !attachToRoot) {
                        throw new InflateException("<merge /> can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }

                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {
                    // Temp is the root view that was found in the xml
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                    ViewGroup.LayoutParams params = null;

                    if (root != null) {
                        if (DEBUG) {
                            System.out.println("Creating params from root: " +
                                    root);
                        }
                        // Create layout params that match root, if supplied
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            temp.setLayoutParams(params);
                        }
                    }

                    if (DEBUG) {
                        System.out.println("-----> start inflating children");
                    }

                    // Inflate all children under temp against its context.
                    rInflateChildren(parser, temp, attrs, true);

                    if (DEBUG) {
                        System.out.println("-----> done inflating children");
                    }

                    // We are supposed to attach all the views we found (int temp)
                    // to root. Do that now.
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }

                    // Decide whether to return the root that was passed in or the
                    // top view found in xml.
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }

            } catch (XmlPullParserException e) {
                final InflateException ie = new InflateException(e.getMessage(), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            } catch (Exception e) {
                final InflateException ie = new InflateException(parser.getPositionDescription()
                        + ": " + e.getMessage(), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            } finally {
                // Don't retain static reference on context.
                mConstructorArgs[0] = lastContext;
                mConstructorArgs[1] = null;

                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }

            return result;
        }
    }

小伙伴们请看,上面这个方法虽然很长,但是逻辑却很简单,前面几行代码都很简单,就是XML的解析,我们看到Google中布局XML的解析使用了PULL解析的方式,在第24行,首先获取了XML文件根节点的名称,第33行,如果根节点是merge的话,那么要求根节点必须添加到某一个容器中,否则会抛异常(如果小伙伴们对merge这个节点尚不了解,请移步这里android开发之merge结合include优化布局)。如果根节点不是merge,则在第42行通过createViewFromTag方法创建一个View(该方法稍后详述),这个View实际上就是我们要加载布局的根节点,加载到根View之后,加载到根View之后,如果root不为null,则在第52行根据root生成一个LayoutParams,第53行,如果我们设置了不将加载进来的布局加载到root中,即我们传入的attachToRoot为false的话,则将刚刚加载进来的params设置给temp(temp是我们要加载布局的根节点),然后在65行通过rInflateChildren方法开始去读取这个根节点下的所有子控件(该方法稍后详述),第73行,如果我们设置了root不为null,并且要将添加进来的布局加入到root中,则会执行root.addView方法。第79行,如果root为null,则attachToRoot不管是什么都会执行第80行,将temp赋值给result。小伙伴们请注意这个result本身的值就是root。最后将result返回。OK如此看来整个inflate方法思路还是非常清晰的,并没有什么难以理解的地方。接下来我们再来看一看系统是怎么样创建根布局的,以及是怎样创建根布局中的子元素的。首先我们来看看createViewFromTag这个方法。

createViewFromTag这个方法最终会到达这里:

    View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
        if (name.equals("view")) {
            name = attrs.getAttributeValue(null, "class");
        }

        // Apply a theme wrapper, if allowed and one is specified.
        if (!ignoreThemeAttr) {
            final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
            final int themeResId = ta.getResourceId(0, 0);
            if (themeResId != 0) {
                context = new ContextThemeWrapper(context, themeResId);
            }
            ta.recycle();
        }

        if (name.equals(TAG_1995)) {
            // Let's party like it's 1995!
            return new BlinkLayout(context, attrs);
        }

        try {
            View view;
            if (mFactory2 != null) {
                view = mFactory2.onCreateView(parent, name, context, attrs);
            } else if (mFactory != null) {
                view = mFactory.onCreateView(name, context, attrs);
            } else {
                view = null;
            }

            if (view == null && mPrivateFactory != null) {
                view = mPrivateFactory.onCreateView(parent, name, context, attrs);
            }

            if (view == null) {
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
                    if (-1 == name.indexOf('.')) {
                        view = onCreateView(parent, name, attrs);
                    } else {
                        view = createView(name, null, attrs);
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            }

            return view;
        } catch (InflateException e) {
            throw e;

        } catch (ClassNotFoundException e) {
            final InflateException ie = new InflateException(attrs.getPositionDescription()
                    + ": Error inflating class " + name, e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;

        } catch (Exception e) {
            final InflateException ie = new InflateException(attrs.getPositionDescription()
                    + ": Error inflating class " + name, e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;
        }
    }

小伙伴们注意,在这个方法的第40行,首先判断了要实例化的节点的名字中是否包含一个点,包含的话说明该View不是由android.jar提供的,这种情况对应一种解析方式,不包含说明该View是由系统提供的,对应一种解析方式,如果name中不包含点,则最终会调用下面的方法:

protected View onCreateView(String name, AttributeSet attrs)
            throws ClassNotFoundException {
        return createView(name, "android.view.", attrs);
    }

小伙伴们请看,这里系统会自动为View添加上包名,我们再来看看这个createView方法(小伙伴们注意,如果我们的View是自定义View的话,最终也会来到这个方法中):

    public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
        Constructor<? extends View> constructor = sConstructorMap.get(name);
        if (constructor != null && !verifyClassLoader(constructor)) {
            constructor = null;
            sConstructorMap.remove(name);
        }
        Class<? extends View> clazz = null;

        try {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);

            if (constructor == null) {
                // Class not found in the cache, see if it's real, and try to add it
                clazz = mContext.getClassLoader().loadClass(
                        prefix != null ? (prefix + name) : name).asSubclass(View.class);
                
                if (mFilter != null && clazz != null) {
                    boolean allowed = mFilter.onLoadClass(clazz);
                    if (!allowed) {
                        failNotAllowed(name, prefix, attrs);
                    }
                }
                constructor = clazz.getConstructor(mConstructorSignature);
                constructor.setAccessible(true);
                sConstructorMap.put(name, constructor);
            } else {
                // If we have a filter, apply it to cached constructor
                if (mFilter != null) {
                    // Have we seen this name before?
                    Boolean allowedState = mFilterMap.get(name);
                    if (allowedState == null) {
                        // New class -- remember whether it is allowed
                        clazz = mContext.getClassLoader().loadClass(
                                prefix != null ? (prefix + name) : name).asSubclass(View.class);
                        
                        boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
                        mFilterMap.put(name, allowed);
                        if (!allowed) {
                            failNotAllowed(name, prefix, attrs);
                        }
                    } else if (allowedState.equals(Boolean.FALSE)) {
                        failNotAllowed(name, prefix, attrs);
                    }
                }
            }

            Object[] args = mConstructorArgs;
            args[1] = attrs;

            final View view = constructor.newInstance(args);
            if (view instanceof ViewStub) {
                // Use the same context when inflating ViewStub later.
                final ViewStub viewStub = (ViewStub) view;
                viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
            }
            return view;

        } catch (NoSuchMethodException e) {
            final InflateException ie = new InflateException(attrs.getPositionDescription()
                    + ": Error inflating class " + (prefix != null ? (prefix + name) : name), e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;

        } catch (ClassCastException e) {
            // If loaded class is not a View subclass
            final InflateException ie = new InflateException(attrs.getPositionDescription()
                    + ": Class is not a View " + (prefix != null ? (prefix + name) : name), e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;
        } catch (ClassNotFoundException e) {
            // If loadClass fails, we should propagate the exception.
            throw e;
        } catch (Exception e) {
            final InflateException ie = new InflateException(
                    attrs.getPositionDescription() + ": Error inflating class "
                            + (clazz == null ? "<unknown>" : clazz.getName()), e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

这个方法虽然有点长,但是核心目的却很清晰,就是根据View的名称,通过Java中的反射机制来获取一个View实例并返回。OK,以上就是对创建根布局方法createViewFromTag的讲解,其实严格来说,这个方法是创建一个View,我们一会在创建容器中的子View的时候,还会再用到这个方法。OK,现在我们再回到inflate方法的第65行,这里又一个方法叫做rInflateChildren,我们来看看该方法:

void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {

        final int depth = parser.getDepth();
        int type;

        while (((type = parser.next()) != XmlPullParser.END_TAG ||
                parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {

            if (type != XmlPullParser.START_TAG) {
                continue;
            }

            final String name = parser.getName();
            
            if (TAG_REQUEST_FOCUS.equals(name)) {
                parseRequestFocus(parser, parent);
            } else if (TAG_TAG.equals(name)) {
                parseViewTag(parser, parent, attrs);
            } else if (TAG_INCLUDE.equals(name)) {
                if (parser.getDepth() == 0) {
                    throw new InflateException("<include /> cannot be the root element");
                }
                parseInclude(parser, context, parent, attrs);
            } else if (TAG_MERGE.equals(name)) {
                throw new InflateException("<merge /> must be the root element");
            } else {
                final View view = createViewFromTag(parent, name, context, attrs);
                final ViewGroup viewGroup = (ViewGroup) parent;
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                rInflateChildren(parser, view, attrs, true);
                viewGroup.addView(view, params);
            }
        }

        if (finishInflate) {
            parent.onFinishInflate();
        }
    }

这个方法倒是很简单,并不长,核心代码就是28行到32行,这里还是调用了上文所说的createViewFromTag方法来获取一个View的实例,然后第31行通过一个递归方法来遍历容器中的所有控件,并将获取到的控件添加到对应的viewGroup中。


OK,以上就是对inflate方法的一个简单解读,整体来说貌似没有什么难点,有问题欢迎留言讨论。


以上。


作者:u012702547 发表于2016/9/24 23:56:25 原文链接
阅读:185 评论:0 查看评论

数据结构之广义表

$
0
0

 广义表>

     广义表是非线性的结构,是线性表的一种扩展,是有n个元素组成的有限序列.

     广义表的定义是递归的,因为在表的描述中又得到了表,允许表中有表.

     eg:

      

(1).空表套空表((()))--深度为3
(2).未出现子表(a,b)--深度为1
(3).表中有表(a,b,(c,d))--深度为2
(4).多层子表的嵌套(a,b,(c,d),(e,(f),g))--深度3
(5).空表()--深度为1



       

      在求一个广义表的深度的时候我们要求的深度是所有子表中的最大值而不是所有的以'('开头的表的总和.例如上述的实例(4),它的深度不是4而是3.

     

     

      通过观察我们发现有三种类型-头结点类型,值类型和子表类型,在广义表的结点中下一个结点可能是值也可能是子表,并且只能是其中的一个所以用共用体来封装数据项和子表项,而且该广义表的结点成员中还必须存在指向下一个结点的指针域.

 

     

enum Type
{
	HEAD,   //头类型
	VALUE,  //值类型
	SUB,    //子表类型
};

struct GeneralNode
{
	GeneralNode(Type type)
		:_type(type)
		,_next(NULL)
	{}
	GeneralNode(Type type,const char& value)
		:_type(type)
		,_next(NULL)
		,_value(value)
	{}
	Type _type;             //类型
	GeneralNode *_next;     //指向同层的下一个结点
	union 
	{
		//下一个结点可能是值也可能是子表
		char _value;
		GeneralNode *_sublink; //指向子表的指针
	};
};


 

      

      程序源代码>

      GeneralList.h

 

         

#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include <iostream>
#include <cassert>
using namespace std;

enum Type
{
	HEAD,   //头类型
	VALUE,  //值类型
	SUB,    //子表类型
};

struct GeneralNode
{
	GeneralNode(Type type)
		:_type(type)
		,_next(NULL)
	{}
	GeneralNode(Type type,const char& value)
		:_type(type)
		,_next(NULL)
		,_value(value)
	{}
	Type _type;             //类型
	GeneralNode *_next;     //指向同层的下一个结点
	union 
	{
		//下一个结点可能是值也可能是子表
		char _value;
		GeneralNode *_sublink; //指向子表的指针
	};
};

class GeneralList
{
public:
	GeneralList()
		:_head(NULL)
	{}
	GeneralList(char *str)
	{
		_head=_CreatList(str);
	}
	GeneralList(const GeneralList& g)
	{
		_head=_Copy(g._head);
	}
	////传统的写法
	//GeneralList& operator=(const GeneralList& g)
	//{
	//	if (this != &g)    //防止自赋值
	//	{
	//		GeneralNode *head=_Copy(g._head);
	//		_Destroy(_head);
	//		_head=head;
	//	}
	//	return *this;
	//}

	//改进的现代写法
	GeneralList& operator=(const GeneralList& g)
	{
		if (this != &g)
		{
			GeneralList tmp(g);
			std::swap(_head,tmp._head);
		}
		return *this;
	}
	~GeneralList()
	{
		_Destroy(_head);
	}
public:
	size_t Size()
	{
		size_t size=_Size(_head);
		return size;
	}
	size_t Depth()
	{
		size_t depth=_Depth(_head);
		return depth;
	}
	void Display()
	{
		_Display(_head);
		cout<<endl;
	}
protected:
	bool IsValue(char str)
	{
		if (str >= 'A' && str <= 'Z'
			||str >= 'a' && str <= 'z'
			||str >= '0' && str <= '9')
		{
			return true;
		}
		else
		{
			return false;
		}
	}
	GeneralNode *_CreatList(char *&str)
	{
		assert(*str=='(');
		++str;
		GeneralNode *head=new GeneralNode(HEAD);
		GeneralNode *cur=head;
		while(*str)
		{
			if (IsValue(*str))
			{
				//是有效数据
				cur->_next=new GeneralNode(VALUE,*str);
				cur=cur->_next;
				cur->_value=*str;
				str++;
				cur->_type=VALUE;
			}
			else if (*str == ')')
			{
				//子表的结束标志
				str++;
				return head;
			}
			else if (*str == '(')
			{
				//表中有表
				cur->_next=new GeneralNode(SUB);
				cur=cur->_next;
				cur->_sublink=_CreatList(str);
				++str;
				cur->_type=SUB;
			}
			else
			{
				//逗号或者其他的分隔符
				str++;
			}
		}
		return head;
	}
	GeneralNode* _Copy(GeneralNode *head)
	{
		//phead,pcur为要拷贝的对象结点
		//head,cur为未拷贝的对象结点
		GeneralNode *phead=new GeneralNode(HEAD);
		GeneralNode *pcur=phead;
		GeneralNode *cur=head;
		while (cur)
		{
			if (cur->_type == VALUE)
			{
				pcur->_next=new GeneralNode(VALUE,cur->_value);
				pcur=pcur->_next;
				pcur->_value=cur->_value;
				cur=cur->_next;
			}
			else if (cur->_type == SUB)
			{
				pcur->_next=new GeneralNode(SUB);
				pcur=pcur->_next;
				pcur->_type=cur->_type;
				pcur->_sublink=_Copy(cur->_sublink);
				cur=cur->_next;
			}
			else
			{
				cur=cur->_next;
			}
		}
		return phead;
	}
	void _Destroy(GeneralNode *head)
	{
		GeneralNode *cur=head;
		while (cur)
		{
			GeneralNode *del=cur;
			cur=cur->_next;
			if (del->_type == SUB)
			{
				_Destroy(del->_sublink);
			}
			else
			{
				delete del;
				del=NULL;
			}
		}
	}
protected:
	void _Display(GeneralNode *head)
	{
		GeneralNode *cur=head;
		while (cur)
		{
			if (cur->_type == HEAD)
			{
				cout<<"(";
			}
			else if (cur->_type == VALUE)
			{
				cout<<cur->_value;
				if (cur->_next != NULL)
				{
					cout<<","; 
				}
			}
			else
			{
				_Display(cur->_sublink);
				if (cur->_next != NULL)
				{
					cout<<",";
				}
			}
			cur=cur->_next;
		}
		cout<<")";
	}
	size_t _Size(GeneralNode *head)
	{
		size_t count=0;
		GeneralNode *cur=head;
		while (cur)
		{
			if (cur->_type == VALUE)
			{
				count++;
			}
			else if (cur->_type == SUB)
			{
				count += _Size(cur->_sublink);
			}
			cur=cur->_next;
		}
		return count;
	}
	size_t _Depth(GeneralNode *head)
	{
		size_t maxDepth=1;
		GeneralNode *cur=head;
		while (cur)
		{
			if (cur->_type == SUB)
			{
				size_t depth=_Depth(cur->_sublink)+1;
				if (depth > maxDepth)
				{
					//更新maxDepth
					maxDepth=depth;
				}
			}
			cur=cur->_next;
		}
		return maxDepth;
	}
protected:
	GeneralNode *_head;
};


 

      test.cpp

 

       

#define _CRT_SECURE_NO_WARNINGS 1
#include "GeneralList.h"

void testGeneralList()
{
	GeneralList g1("((()))");
	GeneralList g2("(a,b)");
	GeneralList g3("(a,b,(c,d))");
	GeneralList g4("(a,b,(c,d),(e,(f),g))");
	g1.Display();
	g2.Display();
	g3.Display();
	g4.Display();
	cout<<g1.Size()<<endl;  //0
	cout<<g2.Size()<<endl;  //2
	cout<<g3.Size()<<endl;  //4
	cout<<g4.Size()<<endl;  //7
	cout<<g1.Depth()<<endl; //3
	cout<<g2.Depth()<<endl; //1
	cout<<g3.Depth()<<endl; //2
	cout<<g4.Depth()<<endl; //3

	GeneralList g5(g4);
	g5.Display();

	GeneralList g6;
	g6=g5;
	g6.Display();
}
int main()
{
	testGeneralList();
	system("pause");
	return 0;
}


 

    

     运行结果展示>

    

     

作者:qq_34328833 发表于2016/9/25 8:16:30 原文链接
阅读:41 评论:0 查看评论

通过源码,手把手带你学属性动画(四) - 理解插值器(附神器)

$
0
0

转载请注明出处:http://blog.csdn.net/my_truelove/article/details/52619091

访问 ruicb.com,一键抵达我的博客!扫描左侧或右下方二维码,关注我的公众号,及时获取最新文章推送!


这已经是系列文章第四篇了,算是基础知识的最后一篇了,讲完这篇就开始分析源码、写动效了,我感觉我能写到十,哈哈!感兴趣的可以看一下前几篇文章:

通过源码,手把手带你学属性动画(一) - 相关类总览
通过源码,手把手带你学属性动画(二) - ValueAnimator基础
通过源码,手把手带你学属性动画(三) - ValueAnimator进阶

说起插值器你可能会陌生,因为在之前的几篇文章中我们都没有提及过,不过没关系,本文将一步一步带你全面了解插值器。

细心地读者会发现,在系列文章第一篇的“监听动画每一帧的值”部分,我们打印了动画的值,发现其变化速率并不是匀速变化的,而是呈现“先加速、后减速”的状态。这就是插值器的作用!

下面,跟着源码及官方文档,我们一起探究插值器。

1. 插值器介绍

插值器只是一个概念,系统中与之相关的类叫做 TimeInterpolator ,其只是一个接口,准确来说叫做“时间插值器”。该接口的注释为:

A time interpolator defines the rate of change of an animation. This allows animations to have non-linear motion, such as acceleration and deceleration.

意思是:该时间插值器定义了动画的变化率,允许动画做非线性的运动,比如加速、减速。

这样,插值器的主要作用我们就明白了。接下来,看看这个接口的代码,该接口只有一个接口方法:

/**
 * @param input A value between 0 and 1.0 indicating our current point
 *        in the animation where 0 represents the start and 1.0 represents
 *        the end
 * @return The interpolation value. This value can be more than 1.0 for
 *         interpolators which overshoot their targets, or less than 0 for
 *         interpolators that undershoot their targets.
 */
float getInterpolation(float input);

不管是系统内置的插值器,还是我们自定义插值器,只需要实现接口并重写该方法,就可以起到插值器的作用。

该方法的作用是什么呢?上图保留了源码中对方法的注释,我就不直译了,说下大概的意思:

  • 方法参数 input 接收 0 和 1.0 之间的值表示动画的当前进度,是线性变化的,其中0表示开始,1.0表示结束; 返回值表示对
  • input 进行插值之后的值,我们就是在这儿做“手脚”,让返回值不再是线性的,就完成自己定义动画的变化率了。

2. 插值器原理分析

系统会根据动画当前时间和动画总时长,计算时间进度fraction,fraction为float类型,值范围为0f~1f,该值是随着时间流逝匀速变化的。我们将 fraction 作为参数input传入getInterpolation(float input)方法:

  • 直接 return fraction,则实际的值依然呈现匀速变化。
  • 这次 return (float)(Math.cos((input 1) * Math.PI) / 2.0f) 0.5f,则实际的值将呈现“先加速、后减速”的变化过程。(具体如何实现先加速后减速,后面再介绍。)

上述总结可能不太好理解,下面举个例子。在40ms内,距离 x 从0过渡到40,结合上述总结,则匀速变化和变速变化的过程分别如下图,很直观:

属性动画4-官方线性运动

属性动画4-官方加速运动

图片来自官方网站

下面的效果(先加速后减速),就是插值器的作用,是系统内置的AccelerateDecelerateInterpolator插值器。下面,带你着重分析一下系统内置插值器:AccelerateDecelerateInterpolator,看其是如何实现动画“先加速、后减速”变化的。

3.AccelerateDecelerateInterpolator

系统内置了许多实现了TimeInterpolator接口的插值器,让我们看官方文档:
属性动画4-TimeInterpolator文档

这么多插值器,数都数不过来,就不一一介绍了,具体效果大家可以自行看一下,就只看关键的getInterpolation()方法就好了。

下面着重分析插值器 AccelerateDecelerateInterpolator,其是属性动画默认采用的插值器,我们看一下其内部 getInterpolation() 方法的实现:

public float getInterpolation(float input) {
    return (float)(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f;
}

就一行代码,这个表达式有没有觉得似曾相识呢?对,就是在上面举例子用的表达式,那么其如何实现先加速后减速的效果呢?我们分析一下:

  • 输入:input,值即为fraction,从0~1
  • 计算:cos((input 1) * Math.PI) 即表示cos(π)到cos(2π),该区间函数值范围为-1~1,再除以2,则为-0.5~0.5,最后再加上0.5
  • 输出:范围0~1。

我们对比一下直接输出input和对input进行变换后输出的效果图:

属性动画4-先加速后减速

很明显,绿色线条表示直接输出,其表示随时间推移的匀速变化,而红色的曲线,是对input经过三角变换后的输出,分析其切线,能明显跟决出其先加速,在 input 为 0.5 处速度达到最大,而后减速的过程。

其他插值器的原理类似,大家可以根据上述分析思路,查看其他插值器的getInterpolation()方法。下面,我们来看看如何自定义插值器!

4. 自定义插值器

只知道用是不够的,还要会自定义!

系统内置插值器已经能满足我们的大多数需求,但有时候难免会需要根据特殊业务自定义。出于演示,来一个系统没有的效果 – “先减速后加速”。

自定义一个类叫 MyDecelerateAccelerateInterpolator,然后重写getInterpolation()方法即可,我直接上代码:

public class MyDecelerateAccelerateInterpolator implements TimeInterpolator {
    @Override
    public float getInterpolation(float input) {
        if (input <= 0.5) {
            return (float) (Math.sin(Math.PI * input)) / 2;
        } else {
            return (float) (2 - Math.sin(Math.PI * input)) / 2;
        }
    }
}

根据上面分析AccelerateDecelerateInterpolator的逻辑,分析MyDecelerateAccelerateInterpolator 对你来说不是问题,我就不再分析了,下面直接看变换前后的对比效果:

属性动画4-先减速后加速

上图的效果不难理解吧。

经过前面的讲解,相信你已经掌握了有关插值器的原理,以及如何自定义插值器。自定义这块,一般都会用到函数的知识,像上面的三角函数就是。如果不会,你应该找高中体育老师去嘿嘿!

5. 使用插值器

说了这么多,对插值器应该理解了吧,下面说说使用。我们可以通过setInterpolator()方法使用插值器,这个真没啥要强调的,你可以把其他插值器都试试:

valueAnimator.setInterpolator(new MyDecelerateAccelerateInterpolator());

具体的效果可自行打印查看,在后面介绍 ObjectAnimator 并对 view 进行动画时,再演示效果。

6. 补充

还记得在上节介绍 TypeEvaluator 时,阐明其作用是:告诉属性动画系统如何从开始值过渡到结束值。并且,本质可以用如下表达式概括:

返回值 = 开始值 + (终点值-开始值) * 进度

如果你认为此处的“进度”就是动画运行的时间进度,那就错了,这样的话要插值器还怎么派上用场?

实际上,该“进度”指的是时间进度经过插值器计算后得到的动画进度。用 fraction 表示动画运行时间占总的动画时间比例,即时间进度,那么上述表达式应该扩展为:

返回值 = 开始值 + (终点值-开始值) * getInterpolation(fraction)

记住这一点就好了,在下节分析源码时我们会证明这一点。

7. 福利

文章的最后,送给大家一个理解插值器的神器,效果如下,自己使用感受一下!

GIF神器

本文就到这儿了,文中若有任何不妥之处,还望留言指正;若对你有帮助,还望点个赞

下篇分析源码,看 ValueAnimator 动画实现的机制及流程,敬请期待!不想错过了可以关注我的公众号哈!

作者:My_TrueLove 发表于2016/9/24 15:11:36 原文链接
阅读:164 评论:0 查看评论

Android:视图绘制(四) ------Path进阶

$
0
0

这里主要讲Path的填充方式 FillType 和 他的一个辅助工具类 PathMeasure

前文我们已经讲过如何用Path画出各种图形,《Android:视图绘制(三) ——Path介绍》,不了解的朋友可以移步。

FillType 填充方式

前文讲Paint的时候,我们就讲到过填充方式,不记得的朋友请移步《Android:视图绘制(一) ——基本的绘图操作Paint和Canvas》,Path的填充方式和Paint的不同,他提供了四种可供选择的值。

· FillType.WINDING: 默认值,取Path的所有区域
· FillType.EVEN_ODD: 取path所在并不相交的区域
· FillType.INVERSE_WINDING: 取path的外部区域
· FillType.INVERSE_EVEN_ODD: 取path外部和相交区域

INVERSE 相反取逆的意思,所以下面的两种填充方式是上面两种的相反形式。

Path提供了 setFillType(FillType ft) 方法,来设置填充方式。下面看图:

这里写图片描述

这里写图片描述

这里写图片描述

值得注意的是,当我们用了INVERSE 属性,取相交外部区域,会填充整个Canvas。

FillType系列还有一些函数。

boolean isInverseFillType() 是否是INVERSE 系列函数。
void toggleInverseFillType() 切换到相反的函数。即 WINDING 切换到 INVERSE_WINDING,反之亦然。

PathMeasure Path的辅助工具类,用于Path的计算的。其可以获得Path的长度和其中任意点的坐标,基于此,我们多是实现一些沿特定图形运动的动画。

其提供了两个构造方法。

PathMeasure()
PathMeasure(Path path, boolean forceClosed)

一种是无参的,另一种提供了两个参数。

参数一 path:因为PathMeasure是用于Path的计算的,所以PathMeasure一定要和我们要操作的Path绑定到一起,其内部会有一个全局的变量用于保存我们的Path,这个构造函数就是一个赋值的过程。

参数二 forceClosed:强制关闭,是一个Boolean的变量。官方解释:If true, then the path will be considered as “closed” even if its contour was not explicitly closed. 就是说,如果我们设置为true的话,就相当于强行的把Path闭合了,也就是相当于调用了Path的close。

PathMeasure 还提供了一个方法setPath(Path path, boolean forceClosed) 可以看到其参数和构造方法中的一样,其实就是对应上面那个无参的构造方法,用来设置值的。

下面来看一下 PathMeasure 中的主要方法。

float getLength() 返回Path的总长度。

boolean getPosTan(float distance, float pos[], float tan[]) 获得距Path起点distance长度的点的坐标,并赋值给pos[]

boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo) 获得距Path起点startD到stopD长度的path,并赋值给dst

还记得前面讲的贝塞尔曲线吗,今天用这个贝塞尔曲线加PathMeasure ,给大家模拟一个小球落地的例子:

这里写图片描述

Gif效果不是很好,有兴趣的可以拷到自己程序中看效果。直接上代码,注释很详细,就不在赘述了。

/**
 * 模拟小球抛物线落地 Demo
 *
 * @author adong
 * @date 2016-9-24 17:11:39
 */
public class CustomPaintView extends View {

    private Paint mPaint;
    private Path mPath;
    private PathMeasure pathMeasure;

    // path长度
    private int mLenght;
    // 当前距离
    private int mCurrentPath;
    // 当前点坐标
    private float[] currentPosition;
    // 用于存放小球的Bitmap
    private Bitmap bitmap;

    public CustomPaintView(Context context, AttributeSet attrs) {
        super(context, attrs);

        setLayerType(View.LAYER_TYPE_SOFTWARE, null);

        mPaint = new Paint();
        mPath = new Path();
        currentPosition = new float[2];
        bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.leida_point_small);
        mPaint.setColor(Color.parseColor("#ff0000"));
        mPaint.setStyle(Paint.Style.STROKE);

        mPath.moveTo(0, 100);
        // 贝塞尔曲线,用于模拟抛物线
        mPath.cubicTo(300, 50, 600, 150, 800, 500);
        mPath.quadTo(950, 400, 1000, 500);

        pathMeasure = new PathMeasure();
        pathMeasure.setPath(mPath, false);
        // 获得长度
        mLenght = (int) pathMeasure.getLength();
    }

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

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

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawPath(mPath, mPaint);
        if (mCurrentPath == 0) {
            // 第一次启动
            startAnimator();
        } else {
            // 在每次ValueAnimator的回调中更新小球的位置
            canvas.drawBitmap(bitmap, currentPosition[0] - bitmap.getWidth() / 2,
                    currentPosition[1] - bitmap.getHeight() / 2, mPaint);
        }
    }

    /**
     * 计算每次的位置
     */
    private void startAnimator() {
        ValueAnimator valueAnimator = ValueAnimator.ofInt(0, mLenght);
        // 持续时间
        valueAnimator.setDuration(5000);
        // 加速插值器
        valueAnimator.setInterpolator(new AccelerateInterpolator());
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                // 获得当前的长度
                mCurrentPath = (int) animation.getAnimatedValue();
                // 获得对应点坐标
                pathMeasure.getPosTan(mCurrentPath, currentPosition, null);

                // 重绘
                invalidate();
            }
        });
        valueAnimator.start();
    }
}
作者:u010635353 发表于2016/9/24 17:19:14 原文链接
阅读:7 评论:0 查看评论

自定义控件三部曲之绘图篇(二十)——RadialGradient与水波纹按钮效果

$
0
0

前言:每当感叹自己的失败时,那我就问你,如果让你重新来一次,你会不会成功?如果会,那说明并没有拼尽全力。

最近博主实在是太忙了,博客更新实在是太慢了,真是有愧大家。

这篇将是Shader的最后一篇,下部分,我们将讲述Canvas变换的知识。在讲完Canvas变换以后,就正式进入第三部曲啦,是不是有点小激动呢……

今天给大家讲的效果是使用RadialGradient来实现水波纹按钮效果,水波纹效果是Android L平台上自带的效果,这里我们就看看它是如何实现的,本篇的最终效果图如下
这里写图片描述

一、RadialGradient详解

RadialGradient的意思是放射渐变,即它会向一个放射源一样,从一个点开始向外从一个颜色渐变成另一种颜色;

一、构造函数

RadialGradient有两个构造函数

//两色渐变
RadialGradient(float centerX, float centerY, float radius, int centerColor, int edgeColor, Shader.TileMode tileMode)
//多色渐变
RadialGradient(float centerX, float centerY, float radius, int[] colors, float[] stops, Shader.TileMode tileMode)

(1)、两色渐变构造函数使用实例
下面我们来看一下两色渐变构造函数的使用方法。

RadialGradient(float centerX, float centerY, float radius, int centerColor, int edgeColor, Shader.TileMode tileMode)

这个两色渐变的构造函数的各项参数意义如下:

  • centerX:渐变中心点X坐标
  • centerY:渐变中心点Y坐标
  • radius:渐变半径
  • centerColor:渐变的起始颜色,即渐变中心点的颜色,取值类型必须是八位的0xAARRGGBB色值!透明底Alpha值不能省略,不然不会显示出颜色。
  • edgeColor:渐变结束时的颜色,即渐变圆边缘的颜色,同样,取值类型必须是八位的0xAARRGGBB色值!
  • TileMode:与我们前面讲的各个Shader一样,用于指定当控件区域大于指定的渐变区域时,空白区域的颜色填充方式。

下面我们举个例子来看下用法:

public class DoubleColorRadialGradientView extends View {
    private Paint mPaint;
    private RadialGradient mRadialGradient;
    private int mRadius = 100;
    public DoubleColorRadialGradientView(Context context) {
        super(context);
        init();
    }

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

    public DoubleColorRadialGradientView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init(){
        setLayerType(LAYER_TYPE_SOFTWARE,null);
        mPaint = new Paint();
    }

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

        mRadialGradient = new RadialGradient(w/2,h/2,mRadius,0xffff0000,0xff00ff00, Shader.TileMode.REPEAT);
        mPaint.setShader(mRadialGradient);
    }

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

        canvas.drawCircle(getWidth()/2,getHeight()/2,mRadius,mPaint);
    }
}

代码量不大,这里首先在onSizeChange中,创建RadialGradient实例。onSizeChange会在布局发生改变时调用,onSizeChanged(int w, int h, int oldw, int oldh)传过来四个参数,前两个参数就代表当前控件所应显示的宽和高。有关onSizeChange的具体意义,我们会在第三部曲讲解回调函数流程中具体讲到,这里大家就先理解到这吧。
在onSizeChange中,我们创建了一个RadialGradient,以控件的中心点为圆点,创建一个半径为mRadius的,从0xffff0000到0xff00ff00的放射渐变。我们这里指定的空白填充方式为Shader.TileMode.REPEAT。
然后在绘图的时候:

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawCircle(getWidth()/2,getHeight()/2,mRadius,mPaint);
}

在绘图时,依然是以控件中心点为圆心,画一个半径为mRadius的圆;注意我们画的圆的大小与所构造的放射渐变的大小是一样的,所以不存在空白区域的填充问题。
效果图如下:
这里写图片描述

(2)、多色渐变构造函数使用实例
多色渐变的构造函数如下:

RadialGradient(float centerX, float centerY, float radius, int[] colors, float[] stops, Shader.TileMode tileMode)

这里与两色渐变不同的是两个函数:

  • int[] colors:表示所需要的渐变颜色数组
  • float[] stops:表示每个渐变颜色所在的位置百分点,取值0-1,数量必须与colors数组保持一致,不然直接crash,一般第一个数值取0,最后一个数值取1;如果第一个数值和最后一个数值并没有取0和1,比如我们这里取一个位置数组:{0.2,0.5,0.8},起始点是0.2百分比位置,结束点是0.8百分比位置,而0-0.2百分比位置和0.8-1.0百分比的位置都是没有指定颜色的。而这些位置的颜色就是根据我们指定的TileMode空白区域填充模式来自行填充!!!有时效果我们是不可控的。所以为了方便起见,建议大家stop数组的起始和终止数值设为0和1.

下面我们举个例子来看下用法:

public class MultiColorRadialGradientView extends View {
    private Paint mPaint;
    private RadialGradient mRadialGradient;
    private int mRadius = 100;
    public MultiColorRadialGradientView(Context context) {
        super(context);
        init();
    }

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

    public MultiColorRadialGradientView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init(){
        setLayerType(LAYER_TYPE_SOFTWARE,null);
        mPaint = new Paint();
    }

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

        int[]   colors = new int[]{0xffff0000,0xff00ff00,0xff0000ff,0xffffff00};
        float[] stops  = new float[]{0f,0.2f,0.5f,1f};
        mRadialGradient = new RadialGradient(w/2,h/2,mRadius,colors,stops, Shader.TileMode.REPEAT);
        mPaint.setShader(mRadialGradient);
    }

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

        canvas.drawCircle(getWidth()/2,getHeight()/2,mRadius,mPaint);
    }
}

这里主要看下多色渐变的构造方法:

int[]   colors = new int[]{0xffff0000,0xff00ff00,0xff0000ff,0xffffff00};
float[] stops  = new float[]{0f,0.2f,0.5f,1f};
mRadialGradient = new RadialGradient(w/2,h/2,mRadius,colors,stops, Shader.TileMode.REPEAT);

这里构造了一个四色颜色数组,渐变位置对应{0f,0.2f,0.5f,1f},然后创建RadialGradient实例。没什么难度。
然后在绘画的时候,同样以控件中心为半径,以放射渐变的半径为半径画圆。由于画的圆半径与放射渐变的大小相同,所以不存在空白位置填充的问题,所以TileMode.REPEAT并没有用到。
效果图如下:
这里写图片描述

二、TileMode重复方式

TileMode的问题,已经重复讲了几篇文章了,其实虽然每种Shader所表现出来的效果不一样,但是形成原理都是相同的。下面我们再来看一下RadialGradient在不同的TileMode下的具体表现。

(1)、X、Y轴共用填充参数

与LinearGradient一样,从构造函数中,可以明显看出RadialGradient只有一个填充模式:

//两色渐变
RadialGradient(float centerX, float centerY, float radius, int centerColor, int edgeColor, Shader.TileMode tileMode)
//多色渐变
RadialGradient(float centerX, float centerY, float radius, int[] colors, float[] stops, Shader.TileMode tileMode)

这就说明了,当填充空白区域时,X轴和Y轴使用同一种填充模式。而不能像BitmapShader那样分别指定X轴与Y轴的填充参数。

(2)、TileMode.CLAMP——边缘填充

我们依然使用双色渐变的示例来看下效果,为了显示填充效果,我们这次画一个屏幕大小的矩形:

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

    mRadialGradient = new RadialGradient(w/2,h/2,mRadius,0xffff0000,0xff00ff00, Shader.TileMode.CLAMP);
    mPaint.setShader(mRadialGradient);
}

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

    canvas.drawRect(0,0,getWidth(),getHeight(),mPaint);
}

效果图如下:
这里写图片描述

从效果图中可以明显看出,除了放渐渐变以外的空白区域都被边缘填充成为了绿色;

(3)、TileMode.REPEAT——重复填充

我们仍使用上面的代码,只是将填充模式改为重复填充:

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

    mRadialGradient = new RadialGradient(w/2,h/2,mRadius,0xffff0000,0xff00ff00, Shader.TileMode.REPEAT);
    mPaint.setShader(mRadialGradient);
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawRect(0,0,getWidth(),getHeight(),mPaint);
}

效果图如下:

这里写图片描述

这个图像乍看去有点辣眼睛,花花绿绿的……从效果图中可以看出,最内部的圆形是红到绿的原始放射渐变。其外面的圆就是空白区域填充模式了,在它的外围,从红到绿渐变。

(4)、TileMode.MIRROR—镜像填充

同样是使用上面的代码,只是将填充模式改为镜像填充:

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

    mRadialGradient = new RadialGradient(w/2,h/2,mRadius,0xffff0000,0xff00ff00, Shader.TileMode.MIRROR);
    mPaint.setShader(mRadialGradient);
}

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

    canvas.drawRect(0,0,getWidth(),getHeight(),mPaint);
}

效果图如下:

Alt text

有些同学第一下看到这个图可能有点懵,所谓镜像,就是把原来的颜色的倒过来填充。即原始是红到绿渐变,第二圈就变成了绿到红渐变,第三圈就又是红到绿渐变,如此往复。
如果我把每一圈渐变的界限标出来,大家可能就容易看懂了:

Alt text

图中白色线就是每一圈渐变的边界线,一次完整的填充就是两个白色圈中的部分。

(5)、填充方式:从控件左上角开始填充

在讲BitmapShader和LinearShader时,我们就一再强调一个点:无论哪种Shader,都是从控件的左上角开始填充的,利用canvas.drawXXX系列函数只是用来指定显示哪一块;
我们在RadialGradient中也做下测试:

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

    mRadialGradient = new RadialGradient(w/2,h/2,mRadius,0xffff0000,0xff00ff00, Shader.TileMode.REPEAT);
    mPaint.setShader(mRadialGradient);
}

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

    canvas.drawRect(0,0,200,200,mPaint);
}

我们这里使用TileMode.REPEAT来填充空白区域,在绘图时,我们只画出左上角的一部分;
效果图如下:

Alt text

从效果图中明显可以看出结论了:
无论哪种Shader,都是从控件的左上角开始填充的,利用canvas.drawXXX系列函数只是用来指定显示哪一块

二、水波纹按钮效果

这部分就要利用RadialGradient来实现水波纹效果了,我们这里直接继承自Button类,做一个按钮的水波纹效果,其实这里继承自任何一个类都是可以在这个类原本的显示内容上显示水波纹效果的,比如,大家可以试验下在源码的基础上,将其改为派生自ImageView,当然要记得给它添加上src属性,是一样会有水波纹效果的。

1、概述

根据上面的的对RadialGradient的讲解,大家第一反应应该是,水波纹很好实现啊:不就是,画一个带有渐变效果的逐渐放大的圆不就得了。不错,思想确实就是这么简单。

(1)、不过,第一个问题来了,从哪个颜色,渐变到哪个颜色呢?

最理想的状态是,从按钮的背景色渐变到天蓝色(开篇效果图中颜色)。但是,怎么拿到按钮的背景色呢?因为按钮的android:background属性填充不一定是颜色,有可能是一个drawable,而这个drawable可以是图片,也可能是selector文件等,所以这条路根本走不通。
而我们讲过,RadialGradient中填充的渐变色彩必须是AARRGGBB形式的,所以我们只需要讲初始颜色的透明度设为0,不就露出了按钮的背景色了么。即类似下面的代码:

 mRadialGradient = new RadialGradient(x, y,20 , 0x00FFFFFF, 0xFF58FAAC, Shader.TileMode.CLAMP);

在这里我们将初始的渐变色改为0x00FFFFFF,由于透明度部分全部设置为0,所以整个颜色就是透明的。所以整个渐变过程就变为从零透明度逐渐变为纯天蓝色(0xFF58FAAC)。

(2)、第二个问题,我们应该怎么安排RadialGradient的填充模式

从效果图中是可以明显看出我们会逐渐放大绘制RadialGradient的圆的,那么,我们是让RadialGradient的渐变变径随着绘制的圆增大而增大,还是不改变RadialGradient的初始半径,空余部分使用Shader.TileMode.CLAMP填充来实现水波纹呢。

答案是让RadialGradient的渐变变径随着绘制的圆增大而增大;下面我们分别举个例子来看下效果就知道区别了:

我们将RadialGradient的初始半径设置为20,而假设当前绘制圆的半径是150,分别用模拟代码来展示在不同代码处理下的效果,以最终决定选用哪种方式来绘制RadialGradient渐变。

如果使用空余部分使用Shader.TileMode.CLAMP填充:

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

    if (mRadialGradient == null) {
        int x = getWidth()/2;
        int y = getHeight()/2;
        mRadialGradient = new RadialGradient(x, y,20 , 0x00FFFFFF, 0xFF58FAAC, Shader.TileMode.CLAMP);
        mPaint.setShader(mRadialGradient);
        canvas.drawCircle(x, y, 150, mPaint);
    }
}

这里以控件中心为圆心,构造一个RadialGradient,这个RadialGradient的半径是20,从透明色,渐变到天蓝色

mRadialGradient = new RadialGradient(x, y,20 , 0x00FFFFFF, 0xFF58FAAC, Shader.TileMode.CLAMP);

而在canvas画圆时,仍然以控件中心为圆心,但圆的半径却是150,明显要超出RadialGradient的半径,空白部分使用Shader.TileMode.CLAMP边缘模式填充

canvas.drawCircle(x, y, 150, mPaint);

效果图如下:

Alt text

从效果图中可以看出,在0-20的部分是从透明色到天蓝色的渐变,但是超出半径20的部分,都以边缘模式填充为完全不透明的天蓝色,感觉跟按钮完全没有融合在一起有没有

如果让RadialGradient的渐变变径随着绘制的圆增大而增大

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

    if (mRadialGradient == null) {
        int x = getWidth()/2;
        int y = getHeight()/2;
        mRadialGradient = new RadialGradient(x, y,150 , 0x00FFFFFF, 0xFF58FAAC, Shader.TileMode.CLAMP);
        mPaint.setShader(mRadialGradient);
        canvas.drawCircle(x, y, 150, mPaint);
    }
}

这里的代码跟上面的一样,唯一不同的是,构造的RadialGradient的渐变半径与canvas.drawCircle所画的圆的半径是一样的,都是150;这就演示了让RadialGradient的渐变变径随着绘制的圆增大而增大的效果

效果图如下:

Alt text

很明显,这是我们想要的结果,渐变色与按钮的背景完全融合。

2、代码实现

上面在讲解了解决了核心问题,以后,下面我们就开始正式实战了

我们再来看下效果图:

Alt text

从效果图中,可以看到我们所需要完成的功能:

  • 在手指按下时,绘制一个默认大小的圆
  • 在手指移动时,所绘制的默认圆的位置需要跟随手指移动
  • 在手指放开时,圆逐渐变大
  • 在动画结束时,波纹效果消失

按下和移动

首先,我们来完成前两个功能:当首先按下时,绘制一个默认大小的圆,而且当手指移动时,可以跟随移动:

private int mX, mY;
private int DEFAULT_RADIUS = 50;
public boolean onTouchEvent(MotionEvent event) {

    if (mX != event.getX() || mY != mY) {
        mX = (int) event.getX();
        mY = (int) event.getY();
        setRadius(DEFAULT_RADIUS);
    }

    if (event.getAction() == MotionEvent.ACTION_DOWN) {
        return true;
    } 
    return super.onTouchEvent(event);
}   

首先,我们这里并没区分MotionEvent.ACTION_DOWNMotionEvent.ACTION_UP的绘图操作,只是统一在当前手指位置与上次的不一样时,就调用setRadius(DEFAULT_RADIUS);重绘RadialGradient;很明显,mX、mY变量表示当前手指的位置,而DEFAULT_RADIUS变量表示默认的RadialGradient的渐变尺寸。但是必须在 MotionEvent.ACTION_DOWN时return true,因为如果不return true,就表示当前控件并不需要下按之后的消息,所以ACTION_MOVE、ACTION_UP消息都不会再传到这个控件里来了,有关这个问题,在前面的文章中已经不只一次提到,这里就不再缀述了。

其中,setRadius(DEFAULT_RADIUS)函数的定义如下:

//表示当前渐变半径
private int mCurRadius = 0;
public void setRadius(final int radius) {
    mCurRadius = radius;
    if (mCurRadius > 0) {
        mRadialGradient = new RadialGradient(mX, mY, mCurRadius, 0x00FFFFFF, 0xFF58FAAC, Shader.TileMode.CLAMP);
        mPaint.setShader(mRadialGradient);
    }
    postInvalidate();
}

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

    canvas.drawCircle(mX, mY, mCurRadius, mPaint);
}

在setRadius中主要负责在手指位置和渐变半径改变时,重新创建RadialGradient,然后重绘。很明显mCurRadius变量表示当前的渐变半径。最后在OnDraw函数中重绘时,画一个跟渐变半径同样大小的圆即可。

手指放开

在手指放开时,主要是开始逐渐放大放射半径的动画,然后在动画结束的时候,清除RadialGradient。代码如下:

private ObjectAnimator mAnimator;
@Override
public boolean onTouchEvent(MotionEvent event) {
    …………
    f (event.getAction() == MotionEvent.ACTION_UP) {
        if (mAnimator != null && mAnimator.isRunning()) {
            mAnimator.cancel();
        }
        if (mAnimator == null) {
            mAnimator = ObjectAnimator.ofInt(this,"radius",DEFAULT_RADIUS, getWidth());
        }
        mAnimator.setInterpolator(new AccelerateInterpolator());
        mAnimator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {

            }

            @Override
            public void onAnimationEnd(Animator animation) {
                setRadius(0);
            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });
        mAnimator.start();
    }

    return super.onTouchEvent(event);
}

在这段代码中,首先是在开始下一个动画前,先判断当前mAnimator是不是还在动画中,如果是正在动画就先取消:

if (mAnimator != null && mAnimator.isRunning()) {
    mAnimator.cancel();
}

这是为了避免当用户连续点击多次的时候,下一次开始动画时,上一次动画还没结束,这样两次动画就会造成冲突,应该先把上次的动画取消掉,然后再重新开始这次的动画:

mAnimator = ObjectAnimator.ofInt(this,"radius",DEFAULT_RADIUS, getWidth());
}
mAnimator.setInterpolator(new AccelerateInterpolator());

然后创建一个ObjectAnimator对象,这里动画操作的函数是setRadius(final int radius)函数,动画的区间是从默认半径到整个控件的宽度,之所以用当前控件的宽度来做为最大动画值,是因为,我们必须指定一个足够大的值,足以让波纹能够覆盖整个控件以后再结束。从效果图中可以看出,在这里控件的宽度是整个控件长度的最大值,所以,我们就以用户点击控件最边缘来算,当用户点击最左或最右边缘时,整个RadialGradient的半径是最大的,此时的最大值是控件宽度,所以我们就用控件宽度来做为动画的最大值即可。

其实这里还是不够严谨,因为在实际应用中,控件的宽度并不是整个控件的最大值,也有可能是控件的高度是最大的,所以最严谨的做法就是先判断控件的高度和宽度哪个最大,然后将最大值做为动画的半径。这里为了简化代码可读性,就不再对比了。

有关ObjectAnimation的知识可以参考:《自定义控件三部曲之动画篇(七)——ObjectAnimator基本使用》

然后给mAnimator设置AccelerateInterpolator()插值器,因为我们需要让波纹的速度逐渐加快,如果不设置插值器的话,默认是使用LinearInterpolator插值器的,这样出来的效果是波纹的变大速度将是匀速的。

mAnimator.setInterpolator(new AccelerateInterpolator());

最后我们需要监听mAnimator结束的动作,当动画结束时,我们需要让RadialGradient消失,最简单的消失办法就是将所画圆的半径设置为0。

mAnimator.addListener(new Animator.AnimatorListener() {
    …………
    @Override
    public void onAnimationEnd(Animator animation) {
        setRadius(0);
    }
    …………
});

到这里所有的代码就讲完了,完整的代码如下:

public class RippleView extends Button {
    private int mX, mY;
    private ObjectAnimator mAnimator;
    private int DEFAULT_RADIUS = 50;
    private int mCurRadius = 0;
    private RadialGradient mRadialGradient;
    private Paint mPaint;

    public RippleView(Context context) {
        super(context);
        init();
    }

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

    public RippleView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init() {
        setLayerType(LAYER_TYPE_SOFTWARE,null);
        mPaint = new Paint();
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {

        if (mX != event.getX() || mY != mY) {
            mX = (int) event.getX();
            mY = (int) event.getY();

            setRadius(DEFAULT_RADIUS);
        }

        if (event.getAction() == MotionEvent.ACTION_DOWN) {

            return true;
        } else if (event.getAction() == MotionEvent.ACTION_UP) {

            if (mAnimator != null && mAnimator.isRunning()) {
                mAnimator.cancel();
            }

            if (mAnimator == null) {
                mAnimator = ObjectAnimator.ofInt(this,"radius",DEFAULT_RADIUS, getWidth());
            }

            mAnimator.setInterpolator(new AccelerateInterpolator());
            mAnimator.addListener(new Animator.AnimatorListener() {
                @Override
                public void onAnimationStart(Animator animation) {

                }

                @Override
                public void onAnimationEnd(Animator animation) {
                    setRadius(0);
                }

                @Override
                public void onAnimationCancel(Animator animation) {

                }

                @Override
                public void onAnimationRepeat(Animator animation) {

                }
            });
            mAnimator.start();
        }

        return super.onTouchEvent(event);
    }

    public void setRadius(final int radius) {
        mCurRadius = radius;
        if (mCurRadius > 0) {
            mRadialGradient = new RadialGradient(mX, mY, mCurRadius, 0x00FFFFFF, 0xFF58FAAC, Shader.TileMode.CLAMP);
            mPaint.setShader(mRadialGradient);
        }
        postInvalidate();
    }


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

        canvas.drawCircle(mX, mY, mCurRadius, mPaint);
    }
}

如果本文有帮到你,记得加关注哦
源码下载地址:http://download.csdn.net/detail/harvic880925/9639134
转载请标明出处,http://blog.csdn.net/harvic880925/article/details/52653811谢谢

如果你喜欢我的文章,那么你将会更喜欢我的微信公众号,将定期推送博主最新文章与收集干货分享给大家(一周一次)
这里写图片描述

作者:harvic880925 发表于2016/9/24 21:58:28 原文链接
阅读:207 评论:4 查看评论

安卓开发中遇到的奇奇怪怪的问题

$
0
0

写这篇文章属于脑中一闪的一个念头,是想着把自己做项目中遇到的一些奇怪问题和解决办法分享出来。因为是现想,所以想到一个在更新一个吧。可能有理解错误的地方,望指出。

1.App的首次安装。

问题描述:在我们安装完成一个app时,在安装界面直接点击打开。我们进入了app的首页,这时我们按home键返回桌面,再点击应用图标,会发现没有直接进入首页,而是先进入了app的闪屏页,在进入首页。重复这一步一直如此。这时我们按back键返回,发现没有直接退回桌面,而是返回到之前打开的多个首页。但是如果一开始安装完我们不是直接打开,而是在桌面点击应用进入就不会这样了。

奇奇怪怪~~

记得当时我在应用市场下载了部分应用,也有一些有同样的问题。但是有些虽然重复打开,但双击退出程序将整个重复打开的关闭了。这确实也是一种方法,但是觉得不是最合理。

解决方法:

  1. https://code.google.com/p/android/issues/detail?id=2373#c40
  2. http://stackoverflow.com/questions/3042420/home-key-press-behaviour/4782423#4782423

我贴一下代码

if (!isTaskRoot()) {
            Intent intent = getIntent();
            String action = intent.getAction();
            if (intent.hasCategory(Intent.CATEGORY_LAUNCHER) && action != null && action.equals(Intent.ACTION_MAIN)) {
                finish();
                return;
            }
        }

2.Android自定义CheckBox

问题描述:曾经写过一个自定义CheckBox,结果运行在4.1的手机上消失不见了。。。你说郁闷不。写法如下:

<CheckBox
      android:id="@+id/check_box"
      style="@style/MyCheckBox"
      android:layout_width="wrap_content"
      android:layout_height="match_parent"/>
<style name="MyCheckBox" parent="Widget.AppCompat.CompoundButton.CheckBox">
        <item name="android:button">@drawable/checkbox_selector</item>
</style>
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/icon_on" android:state_checked="true"/>
 <!-- 设置选中图片 -->
    <item android:drawable="@drawable/icon_off" android:state_checked="false"/>
 <!-- 设置未选中图片 -->
</selector>

写法没有什么问题吧,但是就是不显示,凭空消失。当然如果我加上文字,就出来了,但是文字图片重叠。。。

最后谷歌出了原因:大致是说4.1.2版本中CompoundButton没有getCompoundPaddingXX。两个版本的计算方式不一样,4.1.2以上版本绘制文字时,会把图片的宽度和paddingXX的宽度加上,而4.1.2版本只计算设置的paddingLeft。

知道了原因,解决方法:

<style name="MyCheckBox" parent="Widget.AppCompat.CompoundButton.CheckBox">
        <item name="android:button">@null</item>
        <item name="android:paddingLeft">0dp</item>
        <item name="android:drawableLeft">@drawable/checkbox_selector</item>
</style>

3. RecyclerView Bug

问题描述:这个是在友盟的错误分析中报的,错误信息如下:

java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter positionViewHolder{42fb7f40 position=11 id=-1, oldPos=-1, pLpos:-1 no parent}
    at android.support.v7.widget.RecyclerView$Recycler.validateViewHolderForOffsetPosition(RecyclerView.java:4801)
    at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:4932)
    at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:4913)
    at android.support.v7.widget.LinearLayoutManager$LayoutState.next(LinearLayoutManager.java:2029)
    at android.support.v7.widget.LinearLayoutManager.layoutChunk(LinearLayoutManager.java:1414)
    at android.support.v7.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1377)
    at android.support.v7.widget.LinearLayoutManager.scrollBy(LinearLayoutManager.java:1193)
    at android.support.v7.widget.LinearLayoutManager.scrollVerticallyBy(LinearLayoutManager.java:1043)
    at android.support.v7.widget.RecyclerView.scrollByInternal(RecyclerView.java:1552)
    at android.support.v7.widget.RecyclerView.onTouchEvent(RecyclerView.java:2649)
    at android.view.View.dispatchTouchEvent(View.java:7706)
    at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2224)
    at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1954)
    at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2230)
    at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1968)
    at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2230)
    at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1968)
    at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2230)
    at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1968)
    at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2230)
    at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1968)
    at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2230)
    at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1968)
    at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2230)
    at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1968)
    at com.android.internal.policy.impl.PhoneWindow$DecorView.superDispatchTouchEvent(PhoneWindow.java:2074)
    at com.android.internal.policy.impl.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1521)
    at android.app.Activity.dispatchTouchEvent(Activity.java:2569)
    at com.android.internal.policy.impl.PhoneWindow$DecorView.dispatchTouchEvent(PhoneWindow.java:2022)
    at android.view.View.dispatchPointerEvent(View.java:7886)
    at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:3967)
    at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:3846)
    at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3412)
    at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:3462)
    at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:3431)
    at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:3538)
    at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:3439)
    at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:3595)
    at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3412)
    at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:3462)
    at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:3431)
    at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:3439)
    at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3412)
    at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:5552)
    at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:5532)
    at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:5503)
    at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:5632)
    at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:185)
    at android.view.InputEventReceiver.nativeConsumeBatchedInputEvents(Native Method)
    at android.view.InputEventReceiver.consumeBatchedInputEvents(InputEventReceiver.java:176)
    at android.view.ViewRootImpl.doConsumeBatchedInput(ViewRootImpl.java:5605)
    at android.view.ViewRootImpl$ConsumeBatchedInputRunnable.run(ViewRootImpl.java:5651)
    at android.view.Choreographer$CallbackRecord.run(Choreographer.java:761)
    at android.view.Choreographer.doCallbacks(Choreographer.java:574)
    at android.view.Choreographer.doFrame(Choreographer.java:542)
    at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:747)
    at android.os.Handler.handleCallback(Handler.java:733)
    at android.os.Handler.dispatchMessage(Handler.java:95)
    at android.os.Looper.loop(Looper.java:136)
    at android.app.ActivityThread.main(ActivityThread.java:5162)
    at java.lang.reflect.Method.invokeNative(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:515)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:789)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:602)

从上面可以看到并没有报到我们自己的代码里面来,这就很尴尬了。老办法谷歌,找到了Drakeet大神的一篇博客,说到了我的心坎里。

重现的方法是:使用 RecyclerView 加官方下拉刷新的时候,如果绑定的 List 对象在更新数据之前进行了 clear,而这时用户紧接着迅速上滑 RV,就会造成崩溃,而且异常不会报到你的代码上,属于RV内部错误。初次猜测是,当你 clear 了 list 之后,这时迅速上滑,而新数据还没到来,导致 RV 要更新加载下面的 Item 时候,找不到数据源了,造成 crash.

真是一样一样的,那么解决方法大神也提供了,就是在刷新,也就是 clear 的同时,让 RecyclerView 暂时不能够滑动,之后再允许滑动即可。代码就是在 RecyclerView 初始化的时候加上是否在刷新进而拦截手势:

mRecyclerView.setOnTouchListener(
        new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                if (mIsRefreshing) {
                    return true;
                } else {
                    return false;
                }
            }
        }
);

当然如果觉得刷新时不能滑动可以用这种方案

public class WrapContentLinearLayoutManager extends LinearLayoutManager {
    //... constructor
    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        try {
            super.onLayoutChildren(recycler, state);
        } catch (IndexOutOfBoundsException e) {
            Log.e("probe", "meet a IOOBE in RecyclerView");
        }
    }
}

使用:

RecyclerView recyclerView = (RecyclerView)findViewById(R.id.recycler_view);

recyclerView.setLayoutManager(new WrapContentLinearLayoutManager(activity, LinearLayoutManager.HORIZONTAL, false));

4.一些注意点

(1)平时为了给apk瘦身,我们会对图片进行有损或无损的压缩。那我平时有用到的一个有损压缩网站tinypng,但是切记我们的9图不要压缩,不然会有问题。(这里顺便分享一个做9图的在线工具http://inloop.github.io/shadow4android/

(2)Android的透明主题需谨慎使用。

(3)for循环不要把获得数量的代码写在循环当中:for(int i = 0; i <= list.size(); i ++)

(4)ListView的如果其宽度或高度被设置为wrap_content,会有性能等问题。参见:http://stackoverflow.com/questions/4270278/layout-width-of-a-listview

暂时就想起这么多了,码字不易,多多点赞。

作者:qq_17766199 发表于2016/9/25 12:49:21 原文链接
阅读:260 评论:0 查看评论

Android Studio快捷键

$
0
0

前言

Android Studio对于快捷键的设置比较的灵活,开发者在从不同的平台转移到Android Studio进行Android开发的时候,应该都能找到合适的KeyMap和快捷键使用方式,因为AS直接其他平台的快捷键映射或者是自定义快捷键,比较的方便,此文只为记录。

KeyMap

这里写图片描述
针对不同的平台有不同的映射
这里写图片描述

还可以自定义快捷键,右键点击一项,展开菜单,可以设置键盘快捷键和鼠标快捷键
这里写图片描述

针对不同的模块,都可以设置快捷键,比如说编辑过程中的一些操作以及IDE上方的主菜单,版本控制等等,尽管快捷键很方便,但是记忆也是比较费劲的,所以记住一些使用概率很高的快捷键基本上就可以很大程度的提高开发效率
这里写图片描述

常用快捷键

File
打开Settings Ctrl + ALt + S
打开Project Struct Ctrl + ALt + Shift + S
保存 Ctrl + S
同步 Ctrl + Y
文件比较,选中一个文件后按快捷键 Ctrl + D
快捷创建 Ctrl + Alt + Insert
这里写图片描述

Edit
Undor Ctrl + Z
Redo Ctrl + Shift + Z
Cut Ctrl + X
Copy Ctrl + C
复制路径名 Ctrl + Shift + C
复制引用 Ctrl + Shift +Alt + C
Paste Ctrl + V
从复制历史中选择后粘贴 Ctrl + Shift + V
删除 Delete
查找 Ctrl + F
替换 Ctrl + R
浏览下一个查询结果 F3
浏览上一个查询结果 Shift + F3
向下查找当前选中内容 Ctrl + F3
选中所有和当前选中内容的查找结果 Ctrl + Alt + Shift + J
添加当前选中对象为下个查找对象 Alt + J
高亮选中对象的引用 Ctrl + Shift + F7
列选中模式开关 Alt + Shift + Insert
全选 Ctrl + A
扩大选中范围 Ctrl + W
缩小选中范围 Ctrl + Shift + W
自动补全 Ctrl + Shift + Enter
显示提示信息 Ctrl + Shift + Space
合并行 Ctrl + Shift + J
复制一行 Ctrl + D
删除一行 Ctrl + Y
选中内容切换大小写 Ctrl + Shift + U
增加缩进 Tab
减少缩进 Shift + Tab

备注
列选中
这里写图片描述
行选中, 在Column Selection Mode关闭的情况下,按住 Alt 键执行选中,依然可以出现列选中的效果
这里写图片描述


View
JSON View Meta + Ctrl + Shift + N
这里写图片描述

Navigation
跳转到行 Ctrl + G
查找类 Ctrl + N
查找文件 Ctrl + Shift + N
查找Symbols Ctrl + Shift + Alt + N
Back Ctrl + Alt + 向左箭头
Forward Ctrl + Alt + 向右箭头
上一个编辑过的地方 Ctrl + Alt + BackSpace
代码中下一个报错的地方 F2
代码中上一个报错的地方 Shift + F2
下一个方法 Alt + 向下箭头
上一个方法 Alt + 向上箭头

Code
可以覆写的方法 Ctrl + O
可以覆写的方法 Ctrl + I
生成各种东西 Alt + Insert
这里写图片描述 一些插件提示的内容也可以用过Generate产生,比如如中的GsonFormat
Surround With Ctrl + Alt + T
这里写图片描述
行注释 Alt + /
取消行注释,在执行一次Alt + / Alt + /
插入模板内容 Ctrl + J
这里写图片描述 这个可以用来快速打log,注释或者通用的方法,比较方便
用模板包裹 Ctrl + Alt + J
块注释 Alt + Shift + /
取消行注释,在执行一次Alt + Shift+ / Alt + Shift + /
格式化代码 Alt + Shift + L
解决缩进格式错误 Alt + Shift + I
快速导入包和删除不要的包 Alt + Shift + O
Statements上移 Ctrl + Shift + 向上箭头
Statements下移 Ctrl + Shift + 向下箭头
行上移 Alt+ Shift + 向上箭头
行下移 Alt+ Shift + 向下箭头

Build
构建项目 Ctrl + F9

Run
Run Alt + Shift + F10
Debug Alt + Shift + F9

备注

大致整理一些常用的快捷键,整理的过程中发现了一些自己之前不知道的功能,有收获,对今后的开发工作肯定有正向的帮助,如有错误,请指出。

作者:poorkick 发表于2016/9/25 12:51:49 原文链接
阅读:232 评论:0 查看评论

OpenGL ES —— Perspective Projection的推导

$
0
0

引言

透视投影(Perspective Projection)是3D固定流水线的重要组成部分,是将相机空间中的点从视锥体(frustum)变换到规则观察体(Canonical View Volume)中,待裁剪完毕后进行透视除法的行为。

这里写图片描述
简单的来讲就是把图中的cube投影到屏幕上(二维图形)的过程,我们丢弃了Z坐标,然后将它投影到屏幕上。(显然这种简单的投影会是的画面不是很真实,因为在现实世界里,越远的物体会变得越小,想象你站在铁路上,如果视线足够远的话,你脚底下的铁轨是会在远处相交的)

理解Perspective Projection还是很困难的,还好OpenGL为我们做好了这些底层工作,我们可以在不理解Perspective Projection的情况下也能够控制程序按照我们的意愿运行。不过我觉得程序员不应该只满足于调用API,而应该去尝试了解How does it work。

本文就是从数学角度一步步推导OpenGL里Perspective Projection的原理。不过我们首先要学习齐次坐标的相关知识,因为2D世界中通过它才能表示3D世界中的点。

let’s examine things at a visual level

在欧几里得空间(Euclidean space)里,两个平行的直线是永远不会相交的,但这在projective space里并不总是对的,考虑一下的例子:
这里写图片描述
这是一条铁轨,其中他的两条轨道是平行的,然而随着距离的拉长,我们可以看到最极远处,他们已经相交于一个点。显然在Euclidean Space里,2d/3d的几何图形被很好的描述,但是却不能够准确描述射影空间(projective space)里的图形:比如极远的点。两条平行线相交于无穷远处的一点,我们不妨设它为(∞,∞),然而这个点却在Euclidean Space无意义。

解决方案:齐次坐标(Homogeneous Coordinates)

Homogeneous Coordinates是使用N+1个坐标来表示N维坐标的一种表示方式,考虑2D点的例子,我们会使用一个额外的变量来表示一个点,比如(x, y)在Homogeneous Coordinates中就是(x, y, w),其中w就是那个额外的变量,之后这三个变量可以表示一个笛卡尔坐标(Cartesian coordinates)中新的点(X, Y):
X = x / w;
Y = y / w;

所以,对于刚刚上文提到的无穷远处的点(∞,∞)即可用(x, y, 0)表示。

那么为什么称它是Homogeneous的呢?
我们知道当Homogeneous Coordinates坐标转为Cartesian coordinates只是简单的让x, y除以w:

这里写图片描述

那么考虑下面的例子:

这里写图片描述

我们发现(1, 2, 3), (2 , 4, 6)都指向了同一个点,因此他们是Homogeneous的(都指向了Cartesian space中的同一个点)

证明两条平行线相交

在Cartesian space中考虑如下的式子:

这里写图片描述

当 C 等于 D的时候无解,当C 不等于 D的时候这两条线是重合的,那么下面让我们在projective space重写这个式子

这里写图片描述

现在我们有一个解(x, y, 0),因此这两平行线条线将会在(x, y, 0)处相交,这也是上文我们所说的无穷远处的一点。

透视投影变换

在OpenGL中,我们不会明确指出齐次坐标中w的值,而是通过OpenGL提供的接口生成投影矩阵,之后我们的坐标(2D, 3D坐标)便会通过这个矩阵,变换到一个新的空间体中

我们先看下OpenGL提供给我们的接口
这里写图片描述

  • fovy 是视线在y方向的角度
  • aspect 是宽高比
  • zNear 是近平面z坐标
  • zFar 是远平面z坐标

什么是近平面,远平面z坐标呢?

原来在透视投影中,视域体是一个金字塔
这里写图片描述
小端的面称为近平面,大端成为远平面,相同的物体,如果离近平面越远,那么它就会被压缩的更厉害(因为这个形状变换到规范视域体盒子,更大的体积应该缩放一下放入规范视域体),因此便会有一种3D的感觉,毕竟越远的物体越小嘛。

我们看下刚刚接口生成的矩阵:

这里写图片描述

  • a是y方向角度一般的cot值
  • aspect 宽高比
  • f 远平面z坐标
  • n 近平面z坐标

证明

这里写图片描述

我们可以看到在View Frustum中有一点B,我们所要做的工作就是求出它在近平面中对应的点(A点),并且把A点的x, y 坐标最后规格化到[-1 , 1], z规格化到[0, 1]

根据三角形相似,我们可以得出

OD / OC = L2 / L

其中OD = n, OC = z

所以

L2 / L = n / z

对于L 它的长度是
这里写图片描述

所以L2的长度是
这里写图片描述

所以A的坐标就是

( xn / z, yn / z, n)

下面就是把A的 x, y 坐标 映射到[-1 , 1]的范围中

我们假设在近平面中 x 的范围是[l, r], y的范围是[t, b]
所以
l <= x <= r (1)

0 <= x - l <= r - l (2)

0 <= (x - l) / (r - l) <= 1 (3)

不等式两边乘以2
0 <=  2 * (x - l) / (r - l )<= 2 (4) 

0 - 1 <= (2 * (x - l) / (r - l)) - 1 <= 2 - 1 (5)

(5)式经过化简可得:
这里写图片描述

同理y坐标:
这里写图片描述

代入得:
这里写图片描述

两边同乘以z得
这里写图片描述

显然这里的已经不能化成:
这里写图片描述

除非我们能够得到z’z = …的形式,这样只要对等式除以z就可以得到(x’, y’, z’)的形式

我们定义:

z'z = pz + q 

因为z坐标比较特殊,他的范围是[0, 1],所以当z = n时z’ = 0, z = f时 z’ = 1
所以得到等式

0 = np + q (6)

f = p + q (7)

解得

p = f / (f - n) (8)

q = - fn / (f - n) (9)

所以

z'z = f / (f - n) * p - fn / (f - n) (10)

还剩下其次坐标系中的w值,不过这里不用担心,OpenGL默认会设置它为1,所以

w'z = z (11)

现在我们得到的等式:

这里写图片描述

我们写成矩阵的形式
这里写图片描述

现在有点像一开始的矩阵了,不过我们还可以再化简一下

r - l = w (12)

t - b = h (13)

这里写图片描述

我们看到,之前的api,还提供一个Y方向的角度:
这里写图片描述

可得
这里写图片描述

我们定义aspect为 w / h,但是为了方便计算,我们定义它为r
所以
这里写图片描述

所以现在得到的矩阵为:
这里写图片描述

cot (α / 2) = 1 / tan ( α  / 2) = a

aspect = r

所以得到的公式是:
这里写图片描述

作者:u013022222 发表于2016/9/25 14:03:36 原文链接
阅读:267 评论:0 查看评论

微信底部菜单栏实现的几种方法 -- Android学习之路

$
0
0

sky-mxc 总结 转载注明:http://blog.csdn.net/mxiaochao?viewmode=contents

最近总结几种类似于微信的底部菜单实现的几种方式 这里做个总结 。

实现方式

实现的方式有很多种 这里总结最常见的几种方式,以后再添加其他的。

  • viewPager + RadioGroup
  • viewPager + FragmentTabHost
  • viewpager +TabLayout

viewPager+RadioGroup

感觉这是最简单的一个了,我也就不贴代码 说说我理解的思路吧
通过给pager 和RadioGroup 添加监听,监听两个控件的变化 实现联动
当viewPager的显示pager改变就会触发监听 ,在监听中选中对应的RadioButton即可
当RadioGroup发生 选中改变时也会触发监听 ,在选中改变后 设置显示对应的pager即可

FragmentTabHost +viewpager

这个方式 跟上面那个方式差不多 都是通过 监听 实现联动
如果只使用FragmentTabHost 只能实现 点击tab切换 页面的效果 不能实现左右滑动 而 结合viewPager 刚好实现这一效果
先来看看FragmentTabHost经常用的方法
- setup() 在使用addTab之前调用 设置必要的数据 如 FragmentManager,Fragment的容器id
- addTab() 添加标签
- newTabSpec() 新建 tab
- setCurrentTab() 设置当前显示的标签
- setOnChangeTabListtener 设置tab选中改变监听
- tabHost.getTabWidget().setDividerDrawable(null); //去除间隔线
- Tab的常用方法:
- setIndicator() 可以设置view 和 字符串


main布局

<?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_tab_pager"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.skymxc.demo.fragment.TabPagerActivity">

    <android.support.v4.view.ViewPager
        android:id="@+id/pager"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"></android.support.v4.view.ViewPager>

    <android.support.v4.app.FragmentTabHost
        android:id="@+id/tab_host"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"></android.support.v4.app.FragmentTabHost>

</LinearLayout>

Framgent 简单起见 就不写布局文件了 其他的Fragment 跟这个类似

public class DiscoverFragment extends Fragment {

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        TextView textView = new TextView(getActivity());
        textView.setText("发现");
        textView.setGravity(Gravity.CENTER);
        return textView;
    }
}

tab 的布局 图片在上 文本在下 比较简单

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

    <ImageView
        android:id="@+id/icon"
        android:layout_width="30dp"
        android:layout_height="30dp"
        android:layout_gravity="center"/>
    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@color/selector_font"
        android:text="发现"
        android:layout_gravity="center"/>

</LinearLayout>

selector 基本类似 这里贴一个

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

    <item android:state_selected="true" android:drawable="@mipmap/cb_icon_discover_selected"/>
    <item android:drawable="@mipmap/cb_icon_discover_normal"/>

</selector>

java 代码
初始化 TabHost

    private void initTabHost() {
        tabHost.setup(this,getSupportFragmentManager(), R.id.pager);
        tabHost.getTabWidget().setDividerDrawable(null);
        tabHost.addTab(tabHost.newTabSpec("discover").setIndicator(createView(R.drawable.selector_bg,"发现")), DiscoverFragment.class,null);
        tabHost.addTab(tabHost.newTabSpec("attach").setIndicator(createView(R.drawable.selector_bg_attach,"关注")), AttachFragment.class,null);
        tabHost.addTab(tabHost.newTabSpec("message").setIndicator(createView(R.drawable.selector_bg_message,"消息")), MsgFragment.class,null);
        tabHost.addTab(tabHost.newTabSpec("info").setIndicator(createView(R.drawable.selector_bg_info,"我的")), ContactFragment.class,null);
    }

初始化 pager 并绑定适配器

    /**
     * 初始化 pager 绑定适配器
     */
    private void initPager() {
        fragments = new ArrayList<>();
        fragments.add(new DiscoverFragment());
        fragments.add(new AttachFragment());
        fragments.add(new MsgFragment());
        fragments.add(new ContactFragment());
        FragmentAdapter adapter = new FragmentAdapter(getSupportFragmentManager(),fragments);
        pager.setAdapter(adapter);
    }

分别给 TabHost 和pager 添加监听 实现联动

 /**
  * 为TabHost和viewPager 添加监听 让其联动
  */
 private void bindTabAndPager() {
     tabHost.setOnTabChangedListener(new TabHost.OnTabChangeListener() {
         /**
          *  tab改变后
          * @param tabId 当前tab的tag
          */
         @Override
         public void onTabChanged(String tabId) {
             log("vonTabChanged:"+tabId);
            int position = tabHost.getCurrentTab();
             pager.setCurrentItem(position,true);
         }
     });

     pager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
         /**
          * 页面滑动 触发
          * @param position 当前显得第一个页面的索引,当滑动出现时屏幕就会显示两个pager, 向右滑 position为当前-1(左边的pager就显示出来了),向左滑position为当前(右面就显出来了),
          * @param positionOffset 0-1之间 position的偏移量 从原始位置的偏移量
          * @param positionOffsetPixels 从position偏移的像素值 从原始位置的便宜像素
          */
         @Override
         public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
         log("onPageScrolled=============position:"+position+"====positionOffset:"+positionOffset+"====positionOffsetPixels:"+positionOffsetPixels);
         }

         /**
          * 页面选中后
          * @param position 当前页面的index
          */
         @Override
         public void onPageSelected(int position) {
             tabHost.setCurrentTab(position);
             log("onPageSelected==========:position:"+position);
         }

         /**
          * 页面滑动状态改变时触发
          * @param state 当前滑动状态 共三个状态值
          */
         @Override
         public void onPageScrollStateChanged(int state) {

             String stateStr="";
             switch (state){
                 case ViewPager.SCROLL_STATE_DRAGGING:
                     stateStr="正在拖动";
                     break;
                 case ViewPager.SCROLL_STATE_SETTLING:
                     stateStr="正在去往最终位置 即将到达最终位置";
                     break;
                 case ViewPager.SCROLL_STATE_IDLE:
                     stateStr="滑动停止,当前页面充满屏幕";
                     break;
             }
             log("onPageScrollStateChanged========stateCode:"+state+"====state:"+stateStr);
         }

     });
 }

完整代码

 public class TabPagerActivity extends AppCompatActivity {

    private static final String TAG ="TabPagerActivity";

    private FragmentTabHost  tabHost;
    private ViewPager pager;
    private List<Fragment> fragments;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_tab_pager);
        tabHost = (FragmentTabHost) findViewById(R.id.tab_host);
        pager = (ViewPager) findViewById(R.id.pager);
        //初始化TabHost
        initTabHost();

        //初始化pager
        initPager();

        //添加监听关联TabHost和viewPager
        bindTabAndPager();
    }

    /**
     * 为TabHost和viewPager 添加监听 让其联动
     */
    private void bindTabAndPager() {
        tabHost.setOnTabChangedListener(new TabHost.OnTabChangeListener() {
            /**
             *  tab改变后
             * @param tabId 当前tab的tag
             */
            @Override
            public void onTabChanged(String tabId) {
                log("vonTabChanged:"+tabId);
               int position = tabHost.getCurrentTab();
                pager.setCurrentItem(position,true);
            }
        });

        pager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            /**
             * 页面滑动 触发
             * @param position 当前显得第一个页面的索引,当滑动出现时屏幕就会显示两个pager, 向右滑 position为当前-1(左边的pager就显示出来了),向左滑position为当前(右面就显出来了),
             * @param positionOffset 0-1之间 position的偏移量 从原始位置的偏移量
             * @param positionOffsetPixels 从position偏移的像素值 从原始位置的便宜像素
             */
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
            log("onPageScrolled=============position:"+position+"====positionOffset:"+positionOffset+"====positionOffsetPixels:"+positionOffsetPixels);
            }

            /**
             * 页面选中后
             * @param position 当前页面的index
             */
            @Override
            public void onPageSelected(int position) {
                tabHost.setCurrentTab(position);
                log("onPageSelected==========:position:"+position);
            }

            /**
             * 页面滑动状态改变时触发
             * @param state 当前滑动状态 共三个状态值
             */
            @Override
            public void onPageScrollStateChanged(int state) {

                String stateStr="";
                switch (state){
                    case ViewPager.SCROLL_STATE_DRAGGING:
                        stateStr="正在拖动";
                        break;
                    case ViewPager.SCROLL_STATE_SETTLING:
                        stateStr="正在去往最终位置 即将到达最终位置";
                        break;
                    case ViewPager.SCROLL_STATE_IDLE:
                        stateStr="滑动停止,当前页面充满屏幕";
                        break;
                }
                log("onPageScrollStateChanged========stateCode:"+state+"====state:"+stateStr);
            }

        });
    }

    /**
     * 初始化 pager 绑定适配器
     */
    private void initPager() {
        fragments = new ArrayList<>();
        fragments.add(new DiscoverFragment());
        fragments.add(new AttachFragment());
        fragments.add(new MsgFragment());
        fragments.add(new ContactFragment());
        FragmentAdapter adapter = new FragmentAdapter(getSupportFragmentManager(),fragments);
        pager.setAdapter(adapter);
    }

    /**
     * 初始化 TabHost
     */
    private void initTabHost() {
        tabHost.setup(this,getSupportFragmentManager(), R.id.pager);
        tabHost.getTabWidget().setDividerDrawable(null);
        tabHost.addTab(tabHost.newTabSpec("discover").setIndicator(createView(R.drawable.selector_bg,"发现")), DiscoverFragment.class,null);
        tabHost.addTab(tabHost.newTabSpec("attach").setIndicator(createView(R.drawable.selector_bg_attach,"关注")), AttachFragment.class,null);
        tabHost.addTab(tabHost.newTabSpec("message").setIndicator(createView(R.drawable.selector_bg_message,"消息")), MsgFragment.class,null);
        tabHost.addTab(tabHost.newTabSpec("info").setIndicator(createView(R.drawable.selector_bg_info,"我的")), ContactFragment.class,null);
    }

    /**
     * 返回view
     * @param icon
     * @param tab
     * @return
     */
    private View createView(int icon,String tab){
        View view = getLayoutInflater().inflate(R.layout.fragment_tab_discover,null);
        ImageView imageView = (ImageView) view.findViewById(R.id.icon);
        TextView  title = (TextView) view.findViewById(R.id.title);
        imageView.setImageResource(icon);
        title.setText(tab);
        return  view;
    }

    private void log(String log){
        Log.e(TAG,"="+log+"=");
    }

}

效果如下:
这里写图片描述



viewpager +TabLayout

TabLayout 输入 design的扩展包 使用之前必须得先导入扩展包
tabLayout 可以自动去关联 viewPager 只需为tabLayout 指定关联的viewPager就可以了
这样是方便了很多,但是也有缺点,在自动关联之后 tabLayout会自动去读取 viewPager的title,想使用自定的view当做tab就不可能了

导入 design 扩展包 并排在v7上面
这里写图片描述

布局 这里使用了两个TabLayout 分别实现 自动关联 和 手动关联

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/activity_tab_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.skymxc.demo.fragment.TabLayoutActivity">

    <!-- 使用自动关联-->
    <android.support.design.widget.TabLayout
        android:id="@+id/tab_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:tabMode="fixed"
        app:tabSelectedTextColor="#f0f"
        app:tabIndicatorColor="#f0f"></android.support.design.widget.TabLayout>


    <android.support.v4.view.ViewPager
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"></android.support.v4.view.ViewPager>

    <!--通过监听去关联-->
    <android.support.design.widget.TabLayout
        android:id="@+id/tab_layout_menu"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:tabMode="fixed"
        app:tabSelectedTextColor="#ff0"
        app:tabIndicatorColor="#ff0"></android.support.design.widget.TabLayout>
</LinearLayout>

初始化 自动关联的tab 并和viewPager绑定


    private void initTabLayoutAndPager() {
        //关联 viewPager 使用关联后 tab就会自动去获取pager的title,使用addTab就会无效
        tabLayout.setupWithViewPager(pager);

        fragments = new ArrayList<>();
        fragments.add(new DiscoverFragment());
        fragments.add(new AttachFragment());
        fragments.add(new MsgFragment());
        fragments.add(new ContactFragment());
        adapter = new FragmentAdapter(getSupportFragmentManager(),fragments);
        pager.setAdapter(adapter);
    }

tab的布局和上面是一样的。



为TabLayout 添加view 自动关联添加也没用

        tabLayoutMenu.addTab(tabLayoutMenu.newTab().setCustomView(createView(R.drawable.selector_bg,"发现")));
        tabLayoutMenu.addTab(tabLayoutMenu.newTab().setCustomView(createView(R.drawable.selector_bg_attach,"关注")));
        tabLayoutMenu.addTab(tabLayoutMenu.newTab().setCustomView(createView(R.drawable.selector_bg_message,"消息")));
        tabLayoutMenu.addTab(tabLayoutMenu.newTab().setCustomView(createView(R.drawable.selector_bg_info,"我的")));
        tabLayoutMenu.setSelectedTabIndicatorHeight(0);//去除指示器

设置 viewPager的监听和 TabLayout的监听 实现联动

        tabLayoutMenu.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {

            /**
             * 选中tab后触发
             * @param tab 选中的tab
             */
            @Override
            public void onTabSelected(TabLayout.Tab tab) {
                //与pager 关联
                pager.setCurrentItem(tab.getPosition(),true);
            }

            /**
             * 退出选中状态时触发
             * @param tab 退出选中的tab
             */
            @Override
            public void onTabUnselected(TabLayout.Tab tab) {

            }

            /**
             * 重复选择时触发
             * @param tab 被 选择的tab
             */
            @Override
            public void onTabReselected(TabLayout.Tab tab) {

            }
        });

        pager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

            }

            @Override
            public void onPageSelected(int position) {
                //关联 TabLayout
                tabLayoutMenu.getTabAt(position).select();
            }

            @Override
            public void onPageScrollStateChanged(int state) {

            }
        });

完整代码

package com.skymxc.demo.fragment;

import android.support.design.widget.TabLayout;
import android.support.v4.app.Fragment;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;

import com.skymxc.demo.fragment.adapter.FragmentAdapter;
import com.skymxc.demo.fragment.fragment.AttachFragment;
import com.skymxc.demo.fragment.fragment.ContactFragment;
import com.skymxc.demo.fragment.fragment.DiscoverFragment;
import com.skymxc.demo.fragment.fragment.MsgFragment;

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

public class TabLayoutActivity extends AppCompatActivity {

    private TabLayout tabLayout;
    private ViewPager pager;
    private TabLayout tabLayoutMenu;

    private FragmentAdapter adapter ;
    private List<Fragment> fragments;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_tab_layout);
        tabLayout = (TabLayout) findViewById(R.id.tab_layout);
        pager = (ViewPager) findViewById(R.id.container);
        tabLayoutMenu = (TabLayout) findViewById(R.id.tab_layout_menu);
        initTabLayoutAndPager();


        //想使用自己的布局就得 通过 监听进行关联
        bindPagerAndTab();
    }

    private void bindPagerAndTab() {
        tabLayoutMenu.addTab(tabLayoutMenu.newTab().setCustomView(createView(R.drawable.selector_bg,"发现")));
        tabLayoutMenu.addTab(tabLayoutMenu.newTab().setCustomView(createView(R.drawable.selector_bg_attach,"关注")));
        tabLayoutMenu.addTab(tabLayoutMenu.newTab().setCustomView(createView(R.drawable.selector_bg_message,"消息")));
        tabLayoutMenu.addTab(tabLayoutMenu.newTab().setCustomView(createView(R.drawable.selector_bg_info,"我的")));
        tabLayoutMenu.setSelectedTabIndicatorHeight(0);//去除指示器

        tabLayoutMenu.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {

            /**
             * 选中tab后触发
             * @param tab 选中的tab
             */
            @Override
            public void onTabSelected(TabLayout.Tab tab) {
                //与pager 关联
                pager.setCurrentItem(tab.getPosition(),true);
            }

            /**
             * 退出选中状态时触发
             * @param tab 退出选中的tab
             */
            @Override
            public void onTabUnselected(TabLayout.Tab tab) {

            }

            /**
             * 重复选择时触发
             * @param tab 被 选择的tab
             */
            @Override
            public void onTabReselected(TabLayout.Tab tab) {

            }
        });

        pager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

            }

            @Override
            public void onPageSelected(int position) {
                //关联 TabLayout
                tabLayoutMenu.getTabAt(position).select();
            }

            @Override
            public void onPageScrollStateChanged(int state) {

            }
        });
    }

    private void initTabLayoutAndPager() {
        //关联 viewPager 使用关联后 tab就会自动去获取pager的title,使用addTab就会无效
        tabLayout.setupWithViewPager(pager);

        fragments = new ArrayList<>();
        fragments.add(new DiscoverFragment());
        fragments.add(new AttachFragment());
        fragments.add(new MsgFragment());
        fragments.add(new ContactFragment());
        adapter = new FragmentAdapter(getSupportFragmentManager(),fragments);
        pager.setAdapter(adapter);
    }


    private View createView(int icon, String tab){
        View view = getLayoutInflater().inflate(R.layout.fragment_tab_discover,null);
        ImageView imageView = (ImageView) view.findViewById(R.id.icon);
        TextView title = (TextView) view.findViewById(R.id.title);
        imageView.setImageResource(icon);
        title.setText(tab);
        return  view;
    }
}

效果图
这里写图片描述



这里贴一下 viewPager的适配器

public class FragmentAdapter extends FragmentPagerAdapter {
    List<Fragment> fragments ;
    private String[] titles = new String[]{"发现","关注","消息","我的"};
    public FragmentAdapter(FragmentManager fm,List<Fragment> fragments) {
        super(fm);
        this.fragments =fragments;
    }

    @Override
    public Fragment getItem(int position) {
        return fragments.get(position);
    }

    @Override
    public int getCount() {
        return fragments ==null ?0:fragments.size();
    }

    @Override
    public CharSequence getPageTitle(int position) {
        return titles[position];
    }
}

目前就总结了这几种方式 想到别的方式 再总结
关于这个 Demo的github :https://github.com/sky-mxc/AndroidDemo/tree/master/fragment

作者:MXiaoChao 发表于2016/9/25 16:26:10 原文链接
阅读:195 评论:0 查看评论

Android开发之手把手教你写ButterKnife框架(二)

$
0
0

欢迎转载,转载请标明出处:
http://blog.csdn.net/johnny901114/article/details/52664112
本文出自:【余志强的博客】

上一篇博客Android开发之手把手教你写ButterKnife框架(一)我们讲了ButterKnife是什么、ButterKnife的作用和功能介绍以及ButterKnife的实现原理。

本篇博客主要讲在android studio中如何使用apt。

一、新建个项目, 然后创建一个module名叫processor

新建module的时候一定要选择 Java Library 否则在后面会找不到AbstractProcessor。

分别在app和processor 的文件夹下的build.gralde添加如下配置:

compileOptions {
   sourceCompatibility JavaVersion.VERSION_1_7
   targetCompatibility JavaVersion.VERSION_1_7
}

二、然后在建一个module名叫annotation,主要用来保存项目用到的annotation.

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.CLASS;

@Retention(CLASS) @Target(FIELD)
public @interface BindView {
    int value();
}

三、新建MainActivity 字段上加上注解,如下:

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.text_view)
    TextView textView;

    @BindView(R.id.view)
    TextView view;

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

三、在processor module下新建一个ButterKnifeProcessor 继承AbstractProcessor.

@SupportedAnnotationTypes("com.chiclaim.processor.annotation.BindView")
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class ButterKnifeProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        StringBuilder builder = new StringBuilder()
                .append("package com.chiclaim.processor.generated;\n\n")
                .append("public class GeneratedClass {\n\n") // open class
                .append("\tpublic String getMessage() {\n") // open method
                .append("\t\treturn \"");


        // for each javax.lang.model.element.Element annotated with the CustomAnnotation
        for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) {
            String objectType = element.getSimpleName().toString();
            // this is appending to the return statement
            builder.append(objectType).append(" says hello!\\n");
        }

        builder.append("\";\n") // end return
                .append("\t}\n") // close method
                .append("}\n"); // close class

        try { // write the file
            JavaFileObject source = processingEnv.getFiler().createSourceFile("com.chiclaim.processor.generated.GeneratedClass");
            Writer writer = source.openWriter();
            writer.write(builder.toString());
            writer.flush();
            writer.close();
        } catch (IOException e) {
            // Note: calling e.printStackTrace() will print IO errors
            // that occur from the file already existing after its first run, this is normal
        }
        return true;
    }
}

@SupportedAnnotationTypes(…) 里面的参数是我们需要处理的注解

四、在processor module主目录下resources目录

然后新建META-INF目录,然后在META-INF下新建services 然后新建一个文件名为 javax.annotation.processing.Processor, 里面的内容就是刚刚新建的ButterKnifeProcessor的qualified name :

com.chiclaim.butterknife.processor.ButterKnifeProcessor

当然也可以用新建这么多文件夹,只需要加入google AutoService,这样就会自动完成上面的操作。

@SupportedSourceVersion(SourceVersion.RELEASE_7)
@AutoService(Processor.class)
//AutoService自动生成文件(in processor.jar): META-INF/services/javax.annotation.processing.Processor
public class ButterKnifeProcessor extends AbstractProcessor

五、添加android-apt支持

在全局的build.gradle添加添加apt支持,com.neenbedankt.gradle.plugins:android-apt:1.8,如下所示:

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.2.0'
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

ext {
    sourceCompatibilityVersion = JavaVersion.VERSION_1_7
    targetCompatibilityVersion = JavaVersion.VERSION_1_7
}

allprojects {
    repositories {
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

分别在app module的build.gradle添加 apply plugin 如下所示:

apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'

六、添加module之间的依赖

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:24.2.1'
    testCompile 'junit:junit:4.12'
    compile project(':annotation')
    compile project(':processor')
    compile project(':butterknife')
}
//把processor module生成的jar拷贝到app libs目录
task processorTask(type: Exec) {
    commandLine 'cp', '../processor/build/libs/processor.jar', 'libs/'
}
//build processor 生成processor.jar
processorTask.dependsOn(':processor:build')
preBuild.dependsOn(processorTask)

如下图所示:
这里写图片描述

会在app module的build的目录下生成代码,如:

这里写图片描述

public class GeneratedClass {
    public String getMessage() {
        return "button says hello!\nimageView says hello!\ntextView says hello!\nview says hello!\n";
    }
}

据此,在android studio 使用apt就介绍完毕了。

具体的可以查看github代码:https://github.com/chiclaim/study-butterknife

下一篇将介绍如何实现ButterKnife注入初始化View功能。

作者:johnny901114 发表于2016/9/25 20:33:57 原文链接
阅读:334 评论:0 查看评论

Android系统内置下载器服务DownloadManager的使用

$
0
0

本文链接: http://blog.csdn.net/xietansheng/article/details/52513624

在 Android 程序开发中如果需要下载文件,除了自己程序内部实现下载外,还可以直接使用 Android 系统自带的下载器进行下载,使用系统下载器通常有两种方式:

1. 浏览器下载

将下载链接使用浏览器打开,把下载任务交给浏览器,让浏览器调用系统下载器去下载,下载过程在通知栏有下载进度,下载完后文件通常存放在 “外部存储器” 根目录下的 download 文件夹, 也就是: /mnt/sdcard/download

打开下载链接的 Intent:

Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.addCategory(Intent.CATEGORY_BROWSABLE);
intent.setData(Uri.parse("下载链接"));
startActivity(intent);

使用这种方法下载完全把工作交给了系统应用,自己的应用中不需要申请任何权限,方便简单快捷。但如此我们也不能知道下载文件的大小,不能监听下载进度和下载结果。

2. DownloadManager 系统服务

Android 2.3 (API 10) 以后,系统开放了内置下载器服务,也就是 DownloadManager,是专用于处理耗时长的 HTTP 文件下载的系统服务,在后台进行下载,并自动处理网络连接变化,失败重试。

通过 DownloadManager 我们可以在自己的程序中提交下载请求,可以指定下载文件的保存位置,并实时获取下载进度,监听下载结果。

DownloadManager 的实例通过 context.getSystemService(Context.DOWNLOAD_SERVICE) 获取,使用 DownloadManager 还必须要声明网络权限: android.permission.INTERNET;如果下载文件保存到外部存储器,还需要声明外部存储器的读写权限。

DownloadManager 中有两个重要的内部类:

  • 1) DownloadManager.Request :封装一个下载请求添加到系统下载器队列。
  • 2) DownloadManager.Query :查询下载任务,可实时获取下载进度,下载结果。

使用步骤:

1、配置权限

在 AndroidManifest.xml 配置权限:

<!-- 必须配置网络权限 -->
<uses-permission android:name="android.permission.INTERNET"/>
<!-- 如果将下载的文件保存到外部存储器,还需要配置外部存储器的读写权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

2、封装下载请求(Request),加入下载队列

/*
 * 1. 封装下载请求
 */

// http 下载链接(该链接为 CSDN APP 的下载链接,仅做参考)
String downloadUrl = "http://apk.hiapk.com/appdown/net.csdn.csdnplus";

// 创建下载请求
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(downloadUrl));

/*
 * 设置在通知栏是否显示下载通知(下载进度), 有 3 个值可选:
 *    VISIBILITY_VISIBLE:                   下载过程中可见, 下载完后自动消失 (默认)
 *    VISIBILITY_VISIBLE_NOTIFY_COMPLETED:  下载过程中和下载完成后均可见
 *    VISIBILITY_HIDDEN:                    始终不显示通知
 */
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);

// 设置通知的标题和描述
request.setTitle("通知标题XXX");
request.setDescription("对于该请求文件的描述");

/*
 * 设置允许使用的网络类型, 可选值:
 *     NETWORK_MOBILE:      移动网络
 *     NETWORK_WIFI:        WIFI网络
 *     NETWORK_BLUETOOTH:   蓝牙网络
 * 默认为所有网络都允许
 */
// request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI);

// 添加请求头
// request.addRequestHeader("User-Agent", "Chrome Mozilla/5.0");

// 设置下载文件的保存位置
File saveFile = new File(Environment.getExternalStorageDirectory(), "demo.apk");
request.setDestinationUri(Uri.fromFile(saveFile));

/*
 * 2. 获取下载管理器服务的实例, 添加下载任务
 */
DownloadManager manager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);

// 将下载请求加入下载队列, 返回一个下载ID
long downloadId = manager.enqueue(request);

// 如果中途想取消下载, 可以调用remove方法, 根据返回的下载ID取消下载, 取消下载后下载保存的文件将被删除
// manager.remove(downloadId);

3、查询下载状态(Query)

添加一个下载请求(Request)到下载管理器的队列中,将返回一个下载ID,通过该ID可以实时查询到下载进度,成功与失败等状态。

// 获取下载管理器服务的实例
DownloadManager manager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);

// 创建一个查询对象
DownloadManager.Query query = new DownloadManager.Query();

// 根据 下载ID 过滤结果
query.setFilterById(downloadId);

// 还可以根据状态过滤结果
// query.setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL);

// 执行查询, 返回一个 Cursor (相当于查询数据库)
Cursor cursor = manager.query(query);

if (!cursor.moveToFirst()) {
    cursor.close();
    return;
}

// 下载ID
long id = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_ID));
// 下载请求的状态
int status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS));
// 下载文件在本地保存的路径
String localFilename = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME));
// 已下载的字节大小
long downloadedSoFar = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
// 下载文件的总字节大小
long totalSize = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));

cursor.close();

System.out.println("下载进度: " + downloadedSoFar  + "/" + totalSize);

/*
 * 其中状态 status 的值有 5 种:
 *     DownloadManager.STATUS_SUCCESSFUL:   下载成功
 *     DownloadManager.STATUS_FAILED:       下载失败
 *     DownloadManager.STATUS_PENDING:      等待下载
 *     DownloadManager.STATUS_RUNNING:      正在下载
 *     DownloadManager.STATUS_PAUSED:       下载暂停
 */
if (status == DownloadManager.STATUS_SUCCESSFUL) {
    System.out.println("下载成功, 打开文件, 文件路径: " + localFilename);
}

通常如果在自己的应用中需要显示下载进度,可以使用一个定时器,每隔1秒获取一次下载进度,然后根据自己的需求显示在界面上。

4、监听 点击通知 与 下载完成 的广播

上面查询下载状态的方式是自己主动轮询,监听下载完成更好的方式是监听系统下载服务发出的广播,DownloadManager 在用户点击了下载进度的通知栏 和 下载完成后 都会发出相应的广播。

广播实现:

package com.xiets.demo;

import android.app.DownloadManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;

import java.util.Arrays;

public class DownloadManagerReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();

        if (DownloadManager.ACTION_NOTIFICATION_CLICKED.equals(action)) {
            System.out.println("用户点击了通知");

            // 点击下载进度通知时, 对应的下载ID以数组的方式传递
            long[] ids = intent.getLongArrayExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS);
            System.out.println("ids: " + Arrays.toString(ids));

        } else if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action)) {
            System.out.println("下载完成");

            /*
             * 获取下载完成对应的下载ID, 这里下载完成指的不是下载成功, 下载失败也算是下载完成,
             * 所以接收到下载完成广播后, 还需要根据 id 手动查询对应下载请求的成功与失败.
             */
            long id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1L);
            System.out.println("id: " + id);

            // 根据获取到的ID,使用上面第3步的方法查询是否下载成功
        }
    }

}

在 AndroidManifest.xml 配置广播:

<receiver android:name="com.unnoo.demo.DownloadManagerReceiver">
    <intent-filter>
        <!-- 配置 点击通知 和 下载完成 两个 action -->
        <action android:name="android.intent.action.DOWNLOAD_NOTIFICATION_CLICKED"/>
        <action android:name="android.intent.action.DOWNLOAD_COMPLETE"/>
    </intent-filter>
</receiver>

作者:xietansheng 发表于2016/9/25 21:39:26 原文链接
阅读:183 评论:0 查看评论

Masonry 布局 cell高度适应的一种方案(实现类似朋友圈简单布局)

$
0
0

—更好的阅读体验请点击原文链接

前言: 我模仿的是微博的布局所以也就没有 评论动态刷新cell.

  1. 什么人群适合看?

    好奇Masonry使用的, 听过没用过, 没有深入的接触过的 可以看.

  2. 为什么要写?

    很多文章都是这个原因 1 备忘 2 给需要的人 -.-

  3. 这篇可以了解哪些?

    Masonry + HYBMasonryAutoCellHeight + TTTAttributedLabel + 话题正则 + 表情正则 + 链接 同时感谢作者开源
    这里图片浏览器使用的 SDPhotoBrowser

  4. 使用方便吗?

    使用很方便, 不需要通过文本的宽度计算高度来自适应label了, 使用Masonry 在iOS10 貌似没有出现因为像素点原因, 而显示不完全的问题~

我用类似于微博界面的样式进行测试的 so最帅的头像就是我的微博啦

我用类似于微博界面的样式进行测试的 so最帅的头像就是我的微博啦

我用类似于微博界面的样式进行测试的 so最帅的头像就是我的微博啦
下面进入正题代码的实现~

布局cell上的子控件

        // Masonry布局
        // 头像
        [_headerImageView mas_makeConstraints:^(MASConstraintMaker *make) {
            // 进行约束设置
            make.top.left.equalTo(self.contentView).with.offset(SPACE);
            make.width.height.mas_equalTo(33);
        }];
        // 昵称
        // 文本内容要显示多长
        _labelName.preferredMaxLayoutWidth = SCREEN_W - 63;
        _labelName.numberOfLines = 0;
        [_labelName mas_makeConstraints:^(MASConstraintMaker *make) {
            make.top.equalTo(self.contentView).with.offset(SPACE);
            make.left.equalTo(self.headerImageView.mas_right).with.offset(SPACE);
            make.right.equalTo(self.contentView).with.offset(-SPACE);
        }];
        // 时间
        _labelTime.preferredMaxLayoutWidth = SCREEN_W - 63;
        _labelTime.numberOfLines = 0;
        [_labelTime mas_makeConstraints:^(MASConstraintMaker *make) {
            make.top.equalTo(self.labelName.mas_bottom).with.offset(SPACE); // 空隙 为 10(SPACE)
            make.left.right.equalTo(self.labelName);
        }];
        // 发布的内容
        // 视图是多宽的 进行相应的设置
        self.labelText.preferredMaxLayoutWidth = SCREEN_W - 63;
        _labelText.numberOfLines = 0;
        [_labelText mas_makeConstraints:^(MASConstraintMaker *make) {
            make.top.equalTo(self.labelTime.mas_bottom).with.offset(SPACE);
            make.left.right.mas_equalTo(self.labelTime);
        }];
        // 自动检测链接
        _labelText.enabledTextCheckingTypes = NSTextCheckingTypeLink;
        // 图片浏览器
        [_photosGroup mas_makeConstraints:^(MASConstraintMaker *make) {
            //
            make.top.equalTo(self.labelText.mas_bottom).with.offset(SPACE);
            make.left.equalTo(self.labelText);
            make.width.mas_equalTo(SCREEN_W - 63);
        }];

方法赋值 赋值 - 并且更新需要更新的布局

#pragma mark - 赋值
- (void)configCellWithModel:(CommonModel *)model user:(User *)userModel
{
    // 头像
    [_headerImageView sd_setImageWithURL:[NSURL URLWithString:userModel.profile_image_url] placeholderImage:nil];
    _labelName.text = [NSString stringWithFormat:@"%@ %@", userModel.name, @"我测试cell的高度是否准确, 我测试cell的高度是否准确"];
    _labelTime.text = [NSString stringWithFormat:@"%@ %@", model.created_at, @"我测试cell的高度是否准确, 我测试cell的高度是否准确"];;
    // 发布的内容
    _labelText.text = model.text;
    // 计算Photo的height
    // 这里用到了类似朋友圈的九宫格布局所以我进行了相应的计算
    CGFloat pg_Height = 0.0;
    if (model.pic_urls.count > 1 && model.pic_urls.count <= 3) {
        pg_Height = (SCREEN_W - 73) / 3 + 5;
    }else if(model.pic_urls.count > 3 && model.pic_urls.count <= 6)
    {
        pg_Height = (SCREEN_W - 73) / 3 * 2 + 10;
    }else if (model.pic_urls.count > 6 && model.pic_urls.count <= 9)
    {
        pg_Height = (SCREEN_W - 73) + 15;
    }else if (model.pic_urls.count == 1)
    {
        // 单张图片 为 4/7
        pg_Height = (SCREEN_W - 63) * 4 / 7 + 5;
    }
    else
    {
        pg_Height = 0.0;
    }
    // 同时九宫格进行更新约束
    [_photosGroup mas_updateConstraints:^(MASConstraintMaker *make) {
        make.height.mas_equalTo(pg_Height);
    }];
}

在返回cell高度的方法中

#pragma mark - 返回 Cell的高度
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    CommonModel *model = self.dataArray[indexPath.row];
    User *user = self.userArray[indexPath.row];
    CGFloat cellHeight = [CommonTableViewCell hyb_heightForTableView:tableView config:^(UITableViewCell *sourceCell) {
        //
        CommonTableViewCell *cell = (CommonTableViewCell *)sourceCell;
        // 进行模型方法赋值-传进cell
        [cell configCellWithModel:model user:user];
    } cache:^NSDictionary *{
        return @{kHYBCacheUniqueKey: [NSString stringWithFormat:@"%@", model.id],
                 kHYBCacheStateKey : @"",
                 kHYBRecalculateForStateKey : @(NO) // 标识不用重新更新
                 };
    }];
    return cellHeight;
}

除去以上这些你可能还好奇 话题+表情+链接如何实现识别可点击的
写一个工具类

// .h
/// 话题正则 例如 #夏天帅不帅#
+ (NSRegularExpression *)regexTopic;

/// 表情正则 例如 [偷笑]
+ (NSRegularExpression *)regexEmoticon;
// .m
+ (NSRegularExpression *)regexTopic {
    static NSRegularExpression *regex;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        regex = [NSRegularExpression regularExpressionWithPattern:@"#[^@#]+?#" options:kNilOptions error:NULL];
    });
    return regex;
}

+ (NSRegularExpression *)regexEmoticon {
    static NSRegularExpression *regex;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        regex = [NSRegularExpression regularExpressionWithPattern:@"\\[[^ \\[\\]]+?\\]" options:kNilOptions error:NULL];
    });
    return regex;
}

然后在cell中进行使用检测文字

    // 话题检测
    NSArray *results = [[XTWBStatusHelper regexTopic] matchesInString:model.text options:0 range:NSMakeRange(0, model.text.length)];
    for (NSTextCheckingResult *result in results) {
        // 话题范围
        NSLog(@"range === %@", NSStringFromRange(result.range));
        [_labelText addLinkWithTextCheckingResult:result];
    }
    // 表情检测
    NSArray *results1 = [[XTWBStatusHelper regexEmoticon] matchesInString:model.text options:0 range:NSMakeRange(0, model.text.length)];
    for (NSTextCheckingResult *result in results1) {
        // 表情范围
        NSLog(@"range === %@", NSStringFromRange(result.range));
        [_labelText addLinkWithTextCheckingResult:result];
    }

TTTAttributedLabel的简单使用 — 点击了话题和链接 – 签订协议 指定代理人 实现协议方法

/// 点击链接的方法
- (void)attributedLabel:(TTTAttributedLabel *)label
   didSelectLinkWithURL:(NSURL *)url
{
    NSLog(@"被点击的url === %@", url);
}

/// 点击长按数据
- (void)attributedLabel:(TTTAttributedLabel *)label
  didSelectLinkWithDate:(NSDate *)date
{

}

/// 点击文本链接
- (void)attributedLabel:(TTTAttributedLabel *)label
didSelectLinkWithTextCheckingResult:(NSTextCheckingResult *)result
{
    NSLog(@"被点击的话题 === %@", NSStringFromRange(result.range))

}
/// 长按链接的方法
- (void)attributedLabel:(TTTAttributedLabel *)label
didLongPressLinkWithURL:(NSURL *)url
                atPoint:(CGPoint)point
{
    NSLog(@"被长按的url === %@", url);
}
/// 可以长按的文本
- (void)attributedLabel:(TTTAttributedLabel *)label
didLongPressLinkWithTextCheckingResult:(NSTextCheckingResult *)result
                atPoint:(CGPoint)point
{
    NSLog(@"被长按的话题 === %@", NSStringFromRange(result.range))
}

检测结果打印

总结: Masonry对cell的处理 我的逻辑是这样的, 对你有帮助点个喜欢/关注, 如果您有更好的方案, 请与我交流, 谢谢~.

疑问: 为什么简书Markdown不支持html标签

文/ 夏天然后

End

我的微博-点我@夏天是个大人了 || QQ群: 498143780 进群与我一起玩耍啊
最近还写了一些比较实用的文章 比如:
1. 如何把自己写的库添加Cocoapods支持
2. Hexo + GitHub 建站最详细教程 - 程序员还是写作爱好者都可以拥有这样一个个人站
3. 关于推送的文

作者:sinat_30162391 发表于2016/9/25 22:54:39 原文链接
阅读:171 评论:0 查看评论

Chromium扩展(Extension)的Content Script加载过程分析

$
0
0

       Chromium的Extension由Page和Content Script组成。Page有UI和JS,它们加载在自己的Extension Process中渲染和执行。Content Script只有JS,这些JS是注入在宿主网页中执行的。Content Script可以访问宿主网页的DOM Tree,从而可以增强宿主网页的功能。本文接下来分析Content Script注入到宿主网页执行的过程。

老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!

       我们可以在Extension的清单文件中指定Content Script对哪些网页有兴趣,如前面Chromium扩展(Extension)机制简要介绍和学习计划一文的Page action example所示:

{
  ......

  "content_scripts": [
    {
      "matches": ["https://fast.com/"],
      "js": ["content.js"],
      "run_at": "document_start",
      "all_frames": true
    }
  ]
}

       这个清单文件仅对URL为“https://fast.com/”的网页感兴趣。当这个网页在Chromium中加载的时候,Chromium就会在其中注入到一个content.js。注入过程如图1所示:


图1 Content Script注入到宿主网页执行的过程

       首先,在前面Chromium扩展(Extension)加载过程分析一文提到,Browser进程在加载Extension之前,会创建一个UserScriptMaster对象。此后每当加载一个Extension,这个UserScriptMaster对象的成员函数OnExtensionLoaded都会被调用,用来收集当前正在加载的Extension的Content Script。

       此后,每当Browser进程启动一个Render进程时,代表该Render进程的一个RenderProcessHostImpl对象的成员函数OnProcessLaunched都会被调用,用来通知Browser进程新的Render进程已经启动起来的。这时候这个RenderProcessHostImpl对象会到上述UserScriptMaster对象中获取当前收集到的所有Content Script。这些Content Script接下来会通过一个类型为ExtensionMsg_UpdateUserScript的IPC消息传递给新启动的Render进程。新启动的Render进程通过一个Dispatcher对象接收这个IPC消息,并且会将它传递过来的Content Script保存在一个UserScriptSlave对象中。

       接下来,每当Render进程加载一个网页时,都会在三个时机检查是否需要在该网页中注入Content Script。从前面Chromium扩展(Extension)机制简要介绍和学习计划一文可以知道,这三个时机分别为document_start、document_end和document_idle,分别表示网页的Document对象开始创建、结束创建以及空闲时。接下来我们以document_start这个时机为例,说明Content Script注入到宿主网页的过程。

       网页的Document对象是在WebKit中创建的。WebKit为网页创建了Document对象之后,会调用Content层的一个RenderFrameImpl对象的成员函数didCreateDocumentElement,用来通知后者,它描述的网页的Document对象已经创建好了。这时候这个RenderFrameImpl对象将会调用前面提到的UserScriptSlave对象的成员函数InjectScripts,用来通知后者,现在可以将Content Script注入当前正在加载的网页中去执行。前面提到的UserScriptSlave对象会调用另外一个WebLocalFrameImpl对象的成员函数executeScriptInIsolatedWorld,用来注入符合条件的Content Script到当前正在加载的网页中去,并且在JS引擎的一个Isolated World中执行。Content Script在Isolated World中执行,意味着它不可以访问在宿主网页中定义的JavaScript,包括不能调用在宿主网页中定义的JavaScript函数,以及访问宿主网页中定义的变量。

       以上就是Content Script注入到宿主网页中执行的大概流程。接下来我们结合源代码进行详细的分析,以便对这个注入流程有更深刻的认识。

       我们首先分析UserScriptMaster类收集Content Script的过程。这要从UserScriptMaster类的构造函数说起,如下所示:

UserScriptMaster::UserScriptMaster(Profile* profile)
    : ......,
      extension_registry_observer_(this) {
  extension_registry_observer_.Add(ExtensionRegistry::Get(profile_));
  registrar_.Add(this, chrome::NOTIFICATION_EXTENSIONS_READY,
                 content::Source(profile_));
  registrar_.Add(this, content::NOTIFICATION_RENDERER_PROCESS_CREATED,
                 content::NotificationService::AllBrowserContextsAndSources());
}
       这个函数定义在文件external/chromium_org/chrome/browser/extensions/user_script_master.cc中。

       UserScriptMaster类的成员变量extension_registry_observer_描述的是一个ExtensionRegistryObserver对象。这个ExtensionRegistryObserver对象接下来会注册到与参数profile描述的一个Profile对象关联的一个Extension Registry对象中去,也就是注入到与当前使用的Profile关联的一个Extension Registry对象中去。这个Extension Registry对象的创建过程可以参考前面Chromium扩展(Extension)加载过程分析一文。

       与此同时,UserScriptMaster类的构造函数还会通过成员变量registrar_描述的一个NotificationRegistrar对象监控chrome::NOTIFICATION_EXTENSIONS_READY和content::NOTIFICATION_RENDERER_PROCESS_CREATED事件。其中,事件chrome::NOTIFICATION_EXTENSIONS_READY用来通知Chromium的Extension Service已经启动了,而事件content::NOTIFICATION_RENDERER_PROCESS_CREATED用来通知有一个新的Render进程启动起来。如前面所述,当新的Render进程启动起来的时候,UserScriptMaster类会将当前加载的Extension定义的Content Script传递给它处理。这个过程我们在后面会进行详细分析。

       从前面Chromium扩展(Extension)加载过程分析一文还可以知道,接下来加载的每一个Extension,都会保存在上述Extension Registry对象内部的一个Enabled List中,并且都会调用注册在上述Extension Registry对象中的每一个ExtensionRegistryObserver对象的成员函数OnExtensionLoaded,通知它们有一个新的Extension被加载。

       当UserScriptMaster类的成员变量extension_registry_observer_描述的ExtensionRegistryObserver对象的成员函数OnExtensionLoaded被调用时,它又会调用UserScriptMaster类的成员函数OnExtensionLoaded,以便UserScriptMaster类可以收集新加载的Extension定义的Content Script,如下所示:

void UserScriptMaster::OnExtensionLoaded(
    content::BrowserContext* browser_context,
    const Extension* extension) {
  // Add any content scripts inside the extension.
  extensions_info_[extension->id()] =
      ExtensionSet::ExtensionPathAndDefaultLocale(
          extension->path(), LocaleInfo::GetDefaultLocale(extension));
  ......
  const UserScriptList& scripts =
      ContentScriptsInfo::GetContentScripts(extension);
  for (UserScriptList::const_iterator iter = scripts.begin();
       iter != scripts.end();
       ++iter) {
    user_scripts_.push_back(*iter);
    .....
  }
  if (extensions_service_ready_) {
    changed_extensions_.insert(extension->id());
    if (script_reloader_.get()) {
      pending_load_ = true;
    } else {
      StartLoad();
    }
  }
}
       这个函数定义在文件external/chromium_org/chrome/browser/extensions/user_script_master.cc中。

       参数extension描述的就是当前正在加载的Extension。UserScriptMaster类的成员函数OnExtensionLoaded首先会调用ExtensionSet类的静态成员函数ExtensionPathAndDefaultLocale将该Extension的Path和Locale信息封装在一个ExtensionPathAndDefaultLocale对象中,并且以该Extension的ID为键值,将上述ExtensionPathAndDefaultLocale对象保存在成员变量extensions_info_描述的一个std::map中。

       UserScriptMaster类的成员函数OnExtensionLoaded接下来将当前正在加载的Extension定义的所有Content Script保存在成员变量user_scripts_描述的一个std::vector中。

       UserScriptMaster类有一个类型为bool的成员变量extensions_service_ready_。当它的值等于true的时候,表示Chromium的Extension Service已经启动起来了。这时候extensions_service_ready_就会将当前正在加载的Extension的ID插入到UserScriptMaster类的成员变量changed_extensions_描述的一个std::set中去,表示有一个新的Extension需要处理。这里说的处理,就是将新加载的Extension定义的Content Script的内容读取出来,并且保存在一个共享内存中。

       将Extension定义的Content Script的内容读取出来,并且保存在一个共享内存中,是通过UserScriptMaster类的成员变量script_reloader_指向的一个ScriptReloader对象实现的。如果这个ScriptReloader已经创建出来,那么就表示它现在正在读取Content Script的过程中。这时候UserScriptMaster类的成员变量pending_load_的值会被设置为true,表示当前需要读取的Content Script发生了变化,因此需要重新进行读取。

       如果UserScriptMaster类的成员变量script_reloader_指向的ScriptReloader对象还没有创建出来,那么UserScriptMaster类的成员函数OnExtensionLoaded就会调用另外一个成员函数StartLoad创建该ScriptReloader对象,并且通过该ScriptReloader对象读取当前已经加载的Extension定义的Content Script,如下所示:

void UserScriptMaster::StartLoad() {
  if (!script_reloader_.get())
    script_reloader_ = new ScriptReloader(this);

  script_reloader_->StartLoad(user_scripts_, extensions_info_);
}
       这个函数定义在文件external/chromium_org/chrome/browser/extensions/user_script_master.cc中。

       从这里可以看到,如果UserScriptMaster类的成员变量script_reloader_指向的ScriptReloader对象还没有创建出来,那么UserScriptMaster类的成员函数OnExtensionLoaded就会创建,并且在创建之后,调用它的成员函数StartLoad读取当前已经加载的Extension定义的Content Script,如下所示:

void UserScriptMaster::ScriptReloader::StartLoad(
    const UserScriptList& user_scripts,
    const ExtensionsInfo& extensions_info) {
  // Add a reference to ourselves to keep ourselves alive while we're running.
  // Balanced by NotifyMaster().
  AddRef();

  ......
  this->extensions_info_ = extensions_info;
  BrowserThread::PostTask(
      BrowserThread::FILE, FROM_HERE,
      base::Bind(
          &UserScriptMaster::ScriptReloader::RunLoad, this, user_scripts));
}
       这个函数定义在文件external/chromium_org/chrome/browser/extensions/user_script_master.cc中。

       ScriptReloader类的成员函数StartLoad首先调用成员函数AddRef增加当前正在处理的ScriptReloader对象的引用计数,避免它在读取Content Script的过程中被销毁。

       从前面的调用过程可以知道,参数user_scripts描述的是一个std::vector。这个std::vector保存在当前已经加载的Extension定义的Content Script。另外一个参数extension_info指向的是一个std::map。这个std::map描述了当前加载的所有Extension。

       ScriptReloader类的成员函数StartLoad将参数extension_info指向的std::map保存在自己的成员变量extension_info_之后,就向Browser进程中专用用来执行文件读写操作的BrowserThread::FILE线程的消息队列发送一个Task。这个Task绑定了ScriptReloader类的成员函数RunLoad。

       这意味着ScriptReloader类的成员函数RunLoad接下来会在BrowserThread::FILE线程被调用,用来读取保存在参数user_scripts描述的std::vector中的Content Script,如下所示:

// This method will be called on the file thread.
void UserScriptMaster::ScriptReloader::RunLoad(
    const UserScriptList& user_scripts) {
  LoadUserScripts(const_cast<UserScriptList*>(&user_scripts));

  // Scripts now contains list of up-to-date scripts. Load the content in the
  // shared memory and let the master know it's ready. We need to post the task
  // back even if no scripts ware found to balance the AddRef/Release calls.
  BrowserThread::PostTask(master_thread_id_,
                          FROM_HERE,
                          base::Bind(&ScriptReloader::NotifyMaster,
                                     this,
                                     base::Passed(Serialize(user_scripts))));
}

      这个函数定义在文件external/chromium_org/chrome/browser/extensions/user_script_master.cc中。

      ScriptReloader类的成员函数RunLoad首先调用成员函数LoadUserScripts读取当前已经加载的Extension定义的Content Script,如下所示:

void UserScriptMaster::ScriptReloader::LoadUserScripts(
    UserScriptList* user_scripts) {
  for (size_t i = 0; i < user_scripts->size(); ++i) {
    UserScript& script = user_scripts->at(i);
    scoped_ptr<SubstitutionMap> localization_messages(
        GetLocalizationMessages(script.extension_id()));
    for (size_t k = 0; k < script.js_scripts().size(); ++k) {
      UserScript::File& script_file = script.js_scripts()[k];
      if (script_file.GetContent().empty())
        LoadScriptContent(
            script.extension_id(), &script_file, NULL, verifier_.get());
    }
    for (size_t k = 0; k < script.css_scripts().size(); ++k) {
      UserScript::File& script_file = script.css_scripts()[k];
      if (script_file.GetContent().empty())
        LoadScriptContent(script.extension_id(),
                          &script_file,
                          localization_messages.get(),
                          verifier_.get());
    }
  }
}
      这个函数定义在文件external/chromium_org/chrome/browser/extensions/user_script_master.cc中。

      参数user_srcipts描述的std::vector里面保存的是一系列的UserScript对象。每一个UserScript对象里面包含若干个Content Script文件。每一个Content Script文件都是通过一个UserScript::File对象描述。注意,这些Content Script有可能是Java Script,也有可能是CSS Script。这意味着Extension不仅可以注入Java Script到宿主网页中,还可以注入CSS Script。

      ScriptReloader类的成员函数LoadUserScripts依次调用函数LoadScriptContent读取这些Content Script文件的内容,如下所示:

static bool LoadScriptContent(const std::string& extension_id,
                              UserScript::File* script_file,
                              const SubstitutionMap* localization_messages,
                              ContentVerifier* verifier) {
  std::string content;
  const base::FilePath& path = ExtensionResource::GetFilePath(
      script_file->extension_root(), script_file->relative_path(),
      ExtensionResource::SYMLINKS_MUST_RESOLVE_WITHIN_ROOT);
  if (path.empty()) {
    ......
  } else {
    if (!base::ReadFileToString(path, &content)) {
      LOG(WARNING) << "Failed to load user script file: " << path.value();
      return false;
    }
    ......
  }

  ......

  // Remove BOM from the content.
  std::string::size_type index = content.find(base::kUtf8ByteOrderMark);
  if (index == 0) {
    script_file->set_content(content.substr(strlen(base::kUtf8ByteOrderMark)));
  } else {
    script_file->set_content(content);
  }

  return true;
}
       这个函数定义在文件external/chromium_org/chrome/browser/extensions/user_script_master.cc中。

       函数LoadScriptContent首先调用ExtensionResource类的静态成员函数GetFilePath获得要读取的Content Script的文件路径,然后再调用函数base::ReadFileToString读取该文件的内容。这样就可以获得要要读取的Content Script的内容,这些内容最终又会保存在参数script_file描述的一个UserScript::File对象的内部。

       这一步执行完成后,Chromium就获得了当前已经加载的Extension所定义的Content Script的内容。这些内容保存在每一个Content Script对应的UserScript::File对象中。回到前面分析的ScriptReloader类的成员函数RunLoad中,接下来它调用函数Serialize将前面读取的Content Script的内容保存在一块共享内存中,如下所示:

// Pickle user scripts and return pointer to the shared memory.
static scoped_ptr<base::SharedMemory> Serialize(const UserScriptList& scripts) {
  Pickle pickle;
  pickle.WriteUInt64(scripts.size());
  for (size_t i = 0; i < scripts.size(); i++) {
    const UserScript& script = scripts[i];
    // TODO(aa): This can be replaced by sending content script metadata to
    // renderers along with other extension data in ExtensionMsg_Loaded.
    // See crbug.com/70516.
    script.Pickle(&pickle);
    // Write scripts as 'data' so that we can read it out in the slave without
    // allocating a new string.
    for (size_t j = 0; j < script.js_scripts().size(); j++) {
      base::StringPiece contents = script.js_scripts()[j].GetContent();
      pickle.WriteData(contents.data(), contents.length());
    }
    for (size_t j = 0; j < script.css_scripts().size(); j++) {
      base::StringPiece contents = script.css_scripts()[j].GetContent();
      pickle.WriteData(contents.data(), contents.length());
    }
  }

  // Create the shared memory object.
  base::SharedMemory shared_memory;

  base::SharedMemoryCreateOptions options;
  options.size = pickle.size();
  options.share_read_only = true;
  if (!shared_memory.Create(options))
    return scoped_ptr<base::SharedMemory>();

  if (!shared_memory.Map(pickle.size()))
    return scoped_ptr<base::SharedMemory>();

  // Copy the pickle to shared memory.
  memcpy(shared_memory.memory(), pickle.data(), pickle.size());

  base::SharedMemoryHandle readonly_handle;
  if (!shared_memory.ShareReadOnlyToProcess(base::GetCurrentProcessHandle(),
                                            &readonly_handle))
    return scoped_ptr<base::SharedMemory>();

  return make_scoped_ptr(new base::SharedMemory(readonly_handle,
                                                /*read_only=*/true));
}
       这个函数定义在文件external/chromium_org/chrome/browser/extensions/user_script_master.cc中。

       函数Serialize依次遍历保存在参数scripts描述的一个std::vector中的每一个UserScript对象,并且将这些UserScript对象包含的Content Script写入到本地变量pickle描述一个Pickle对象中。从前面Chromium的IPC消息发送、接收和分发机制分析一文可以知道,Pickle类是Chromium定义的一种IPC消息格式,它将数据按照一定的格式打包在一块内存中。

       函数Serialize将Content Script写入到本地变量pickle描述的Pickle对象中去之后,接下来又会创建一块共享内存。这块共享内存通过本地变量shared_memory描述的一个SharedMemory对象描述。有了这块共享内存之后,函数Serialize就会将Content Script的内容从本地变量pickle描述的Pickle对象中拷贝到它里面去。

       函数Serialize最后获得已经写入了Content Script的共享内存的只读版本,并且将这个只读版本封装在另外一个SharedMemory对象中返回给调用者,以便调用者以后可以将它传递给宿主网页所在的Render进程进行只读访问。

       这一步执行完成后,Chromium就获得了一块只读的共享内存,这块共享内存保存了当前已经加载的Extension所定义的Content Script的内容。回到前面分析的ScriptReloader类的成员函数RunLoad中,接下来它又会向成员变量master_thread_id_描述的线程的消息队列发送一个Task。这个Task绑定了ScriptReloader类的成员函数NotifyMaster,用来通知其内部引用的一个UserScriptMaster对象,当前已经加载的Extension所定义的Content Script的内容已经读取完毕。

       ScriptReloader类的成员函数NotifyMaster的实现如下所示:

void UserScriptMaster::ScriptReloader::NotifyMaster(
    scoped_ptr<base::SharedMemory> memory) {
  // The master could go away
  if (master_)
    master_->NewScriptsAvailable(memory.Pass());

  // Drop our self-reference.
  // Balances StartLoad().
  Release();
}
      这个函数定义在文件external/chromium_org/chrome/browser/extensions/user_script_master.cc中。

      ScriptReloader类内部引用的UserScriptMaster对象保存在成员变量master_中。ScriptReloader类的成员函数NotifyMaster首先调用这个UserScriptMaster对象的成员函数NewScriptsAvailable,通知它已经加载的Extension所定义的Content Script的内容已经读取完毕。

      通知完成后,ScriptReloader类的成员函数NotifyMaster还会调用成员函数Release减少当前正在处理的ScriptReloader对象的引用计数,用来平衡在读取Content Script之前,对该ScriptReloader对象的引用计数的增加。

      接下来我们继续分析UserScriptMaster类的成员函数NewScriptsAvailable的实现,以便了解它是如何处理当前已经加载的Extension的Content Script的内容的,如下所示:

void UserScriptMaster::NewScriptsAvailable(
    scoped_ptr<base::SharedMemory> handle) {
  if (pending_load_) {
    // While we were loading, there were further changes.  Don't bother
    // notifying about these scripts and instead just immediately reload.
    pending_load_ = false;
    StartLoad();
  } else {
    ......

    // We've got scripts ready to go.
    shared_memory_ = handle.Pass();

    for (content::RenderProcessHost::iterator i(
            content::RenderProcessHost::AllHostsIterator());
         !i.IsAtEnd(); i.Advance()) {
      SendUpdate(i.GetCurrentValue(),
                 shared_memory_.get(),
                 changed_extensions_);
    }
    ......
  }
}
       这个函数定义在文件external/chromium_org/chrome/browser/extensions/user_script_master.cc中。

       UserScriptMaster类的成员函数NewScriptsAvailable首先判断成员变量pending_load_的值是否等于true。如果等于true,那么就说明前面在读取Content Script的过程中,又有新的Extension被加载。这时候UserScriptMaster类的成员函数会调用前面分析过的成员函数StartLoad对Content Script进行重新读取。

       我们假设这时候UserScriptMaster类的成员变量pending_load_的值等于false。在这种情况下,UserScriptMaster类的成员函数NewScriptsAvailable首先将参数handle描述的共享内存保丰在成员变量shared_memory_中,接下来又会将这块共享内存传递给当前已经启动的Render进程,这是通过调用成员函数SendUpdate实现的。

       我们假设当前还没有Render进程启动起来。前面分析UserScriptMaster类的构造函数的实现时提到,UserScriptMaster类会监控Render进程启动事件,也就是content::NOTIFICATION_RENDERER_PROCESS_CREATED事件。以后每当有一个Render进程启动完成,UserScriptMaster类的成员函数Observe就会被调用。UserScriptMaster类的成员函数Observe在调用的过程中,就会将前面已经加载的Extension的Content Script发送给新启动的Render进程,以便后者可以将Content Script注入到它后续加载的网页中去。

      接下来我们就从Render进程启动完成后开始,分析UserScriptMaster类将Content Script传递给Render进程的过程。从前面Chromium的Render进程启动过程分析一文可以知道,Render进程是由Browser进程启动的。Browser进程又是通过ChildProcessLauncher::Context类启动Render进程的。当Render进程启动完成后,ChildProcessLauncher::Context类的静态成员函数OnChildProcessStarted就会被调用,它的实现如下所示:

class ChildProcessLauncher::Context
    : public base::RefCountedThreadSafe<ChildProcessLauncher::Context> {
 public:
  ......

  static void OnChildProcessStarted(
      // |this_object| is NOT thread safe. Only use it to post a task back.
      scoped_refptr<Context> this_object,
      BrowserThread::ID client_thread_id,
      const base::TimeTicks begin_launch_time,
      base::ProcessHandle handle) {
    RecordHistograms(begin_launch_time);
    if (BrowserThread::CurrentlyOn(client_thread_id)) {
      // This is always invoked on the UI thread which is commonly the
      // |client_thread_id| so we can shortcut one PostTask.
      this_object->Notify(handle);
    } else {
      BrowserThread::PostTask(
          client_thread_id, FROM_HERE,
          base::Bind(
              &ChildProcessLauncher::Context::Notify,
              this_object,
              handle));
    }
  }

  ......
};
      这个函数定义在文件external/chromium_org/content/browser/child_process_launcher.cc中。

      参数client_thread_id描述的是当初请求启动Render进程的线程的ID。另外一个参数this_object描述的是用来启动Render进程的一个ChildProcessLauncher::Context对象。这个ChildProcessLauncher::Context对象就是在请求启动Render进程的线程中创建的。

      ChildProcessLauncher::Context类的静态成员函数OnChildProcessStarted首先检查当前线程是否就是当初请求启动Render进程的线程。如果是的话,那么就直接调用参数this_object描述的ChildProcessLauncher::Context对象的成员函数Notify,用来通知它请求的Render进程已经启动完成了。否则的话,会向当初请求启动Render进程的线程的消息队列发送一个Task,然后在这个Task执行的时候,再调用参数this_object描述的ChildProcessLauncher::Context对象的成员函数Notify。通过这种方式,保证参数this_object描述的ChildProcessLauncher::Context对象总是在创建它的线程中使用。这样可以避免在多线程环境下,访问对象(调用对象的成员函数)需要加锁的问题(加锁会引入竞争,竞争会带来不确定的延时)。这是Chromium多线程编程哲学所要遵循的原则之一。关于Chromium多线程编程哲学,可以参考前面Chromium多线程模型设计和实现分析一文。

       接下来,我们就继续分析ChildProcessLauncher::Context类的成员函数Notify的实现,如下所示:

class ChildProcessLauncher::Context
    : public base::RefCountedThreadSafe<ChildProcessLauncher::Context> {
  ......

 private:
  ......

  void Notify(
#if defined(OS_POSIX) && !defined(OS_MACOSX) && !defined(OS_ANDROID)
      bool zygote,
#endif
      base::ProcessHandle handle) {
    ......

    if (client_) {
      if (handle) {
        client_->OnProcessLaunched();
      } else {
        client_->OnProcessLaunchFailed();
      }
    } 

    ......
  }

  ......
};
       这个函数定义在文件external/chromium_org/content/browser/child_process_launcher.cc中。

       ChildProcessLauncher::Context类的成员变量client_指向的是一个RenderProcessHostImpl对象。这个RenderProcessHostImpl对象是在Browser进程中描述它启动的一个Render进程的。通过这个RenderProcessHostImpl对象,可以与Render进程进行IPC。

       参数handle描述的就是前面请求启动的Render进程的名柄。当这个句柄值不等于0时,就表示请求的Render进程已经成功地启动起来了。这时候ChildProcessLauncher::Context类的成员函数就会调用成员变量client_指向的RenderProcessHostImpl对象的成员函数OnProcessLaunched,以便它可以发出一个content::NOTIFICATION_RENDERER_PROCESS_CREATED事件通知,如下所示:

void RenderProcessHostImpl::OnProcessLaunched() {
  ......

  NotificationService::current()->Notify(
      NOTIFICATION_RENDERER_PROCESS_CREATED,
      Source<RenderProcessHost>(this),
      NotificationService::NoDetails());

  ......
}
      这个函数定义在文件external/chromium_org/content/browser/renderer_host/render_process_host_impl.cc中。

      从前面的分析可以知道,一旦RenderProcessHostImpl对象的成员函数OnProcessLaunched发出content::NOTIFICATION_RENDERER_PROCESS_CREATED事件通知,UserScriptMaster类的成员函数Observe就会被调用,如下所示:

void UserScriptMaster::Observe(int type,
                               const content::NotificationSource& source,
                               const content::NotificationDetails& details) {
  ......

  switch (type) {
    ......
    case content::NOTIFICATION_RENDERER_PROCESS_CREATED: {
      content::RenderProcessHost* process =
          content::Source<content::RenderProcessHost>(source).ptr();
      ......
      if (ScriptsReady()) {
        SendUpdate(process,
                   GetSharedMemory(),
                   std::set<std::string>());  // Include all extensions.
      }
      break;
    }
    ......
  }

  ......
}
       这个函数定义在文件external/chromium_org/chrome/browser/extensions/user_script_master.cc中。

       UserScriptMaster类的成员函数Observe在处理content::NOTIFICATION_RENDERER_PROCESS_CREATED事件通知的时候,首先会调用成员函数ScriptsReady检查当前加载的Extension的Content Script是否已经读取出来,并且保存在内部维护的一块共享内存中去了。如果是的话,那么就会继续调用另外一个成员函数SendUpdate将这块共享内存传递给当前启动完成的Render进程。

       UserScriptMaster类的成员函数SendUpdate的实现如下所示:

void UserScriptMaster::SendUpdate(
    content::RenderProcessHost* process,
    base::SharedMemory* shared_memory,
    const std::set<std::string>& changed_extensions) {
  ......

  base::SharedMemoryHandle handle_for_process;
  if (!shared_memory->ShareToProcess(handle, &handle_for_process))
    return;  // This can legitimately fail if the renderer asserts at startup.

  if (base::SharedMemory::IsHandleValid(handle_for_process)) {
    process->Send(new ExtensionMsg_UpdateUserScripts(handle_for_process,
                                                     changed_extensions));
  }
}
       这个函数定义在文件external/chromium_org/chrome/browser/extensions/user_script_master.cc中。

       参数shared_memory描述的共享内存就是要发送给参数process描述的Render进程的,它里面包含了当前加载的Extension的Content Script。UserScriptMaster类的成员函数SendUpdate将这块共享封装在一个类型为ExtensionMsg_UpdateUserScripts的IPC消息中,并且发送给参数process描述的Render进程。

       Render进程在启动的时候会创建一个Dispatcher对象。这个Dispatcher对象会通过成员函数OnControlMessageReceived接收类型为ExtensionMsg_UpdateUserScripts的IPC消息,如下所示:

bool Dispatcher::OnControlMessageReceived(const IPC::Message& message) {
  bool handled = true;
  IPC_BEGIN_MESSAGE_MAP(Dispatcher, message)
  ......
  IPC_MESSAGE_HANDLER(ExtensionMsg_UpdateUserScripts, OnUpdateUserScripts)
  ......
  IPC_MESSAGE_UNHANDLED(handled = false)
  IPC_END_MESSAGE_MAP()

  return handled;
}
      这个函数定义在文件external/chromium_org/extensions/renderer/dispatcher.cc中。

      Dispatcher类的成员函数OnControlMessageReceived将类型为ExtensionMsg_UpdateUserScripts的IPC消息分发给另外一个成员函数OnUpdateUserScripts处理,如下所示:

void Dispatcher::OnUpdateUserScripts(
    base::SharedMemoryHandle scripts,
    const std::set<std::string>& extension_ids) {
  ......

  user_script_slave_->UpdateScripts(scripts, extension_ids);
  ......
}
      这个函数定义在文件external/chromium_org/extensions/renderer/dispatcher.cc中。

      Dispatcher类的成员变量user_script_slave_指向的是一个UserScriptSlave对象。Dispatcher类的成员函数OnUpdateUserScripts调用这个UserScriptSlave对象的成员函数UpdateScripts将参数scripts描述的共享内存包含的Content Script交给它处理。处理过程如下所示:

bool UserScriptSlave::UpdateScripts(
    base::SharedMemoryHandle shared_memory,
    const std::set<std::string>& changed_extensions) {
  ......

  // Unpickle scripts.
  uint64 num_scripts = 0;
  Pickle pickle(reinterpret_cast<char*>(shared_memory_->memory()), pickle_size);
  PickleIterator iter(pickle);
  CHECK(pickle.ReadUInt64(&iter, &num_scripts));
  ......

  // If we pass no explicit extension ids, we should refresh all extensions.
  bool include_all_extensions = changed_extensions.empty();
  ......
  
  if (include_all_extensions) {
    script_injections_.clear();
  } 

  ......

  for (uint64 i = 0; i < num_scripts; ++i) {
    scoped_ptr<UserScript> script(new UserScript());
    script->Unpickle(pickle, &iter);
    ......
    
    for (size_t j = 0; j < script->js_scripts().size(); ++j) {
      const char* body = NULL;
      int body_length = 0;
      CHECK(pickle.ReadData(&iter, &body, &body_length));
      script->js_scripts()[j].set_external_content(
          base::StringPiece(body, body_length));
    }
    for (size_t j = 0; j < script->css_scripts().size(); ++j) {
      const char* body = NULL;
      int body_length = 0;
      CHECK(pickle.ReadData(&iter, &body, &body_length));
      script->css_scripts()[j].set_external_content(
          base::StringPiece(body, body_length));
    }

    // If we include all extensions or the given extension changed, we add a
    // new script injection.
    if (include_all_extensions ||
        changed_extensions.count(script->extension_id()) > 0) {
      script_injections_.push_back(new ScriptInjection(script.Pass(), this));
    } 

    ......
  }
  return true;
}
       这个函数定义在文件external/chromium_org/extensions/renderer/user_script_slave.cc中。

       UserScriptSlave类的成员函数UpdateScripts会通过本地变量pickle描述的Pickle对象解析和读取包含在参数shared_memory中的Content Script。这些Content Script将会保存在UserScriptSlave类的成员变量script_injections_描述的一个Vector中。

       当参数changed_extensions描述的字符串不等于空时,它的值就表示Content Script内容发生过变化的Extension。这时候UserScriptSlave类可以对内部维护的Content Script进行增量更新。从前面的调用过程可以知道,在我们这种情景中,参数changed_extensions描述的字符串等于空。在这种情况下,UserScriptSlave类将会对内部维护的Content Script进行全量更新。也就是先清空成员变量script_injections_描述的Vector,然后再将包含在参数shared_memory中的Content Script增加到这个Vector中去。

       从前面分析的函数Serialize可以知道,包含在参数shared_memory中的Content Script是按照Extension进行组织的。一个Extension对应一个UserScript对象。一个UserScript对象又可以包含若干个Content Script。这些Content Script又可能同时包含有Java Script和CSS Script。UserScriptSlave类的成员函数UpdateScripts通过本地变量pickle描述的Pickle对象依次获得每一个Extension的Content Script,并且封装封装在一个ScriptInjection对象中。这些ScriptInjection对象会保存在UserScriptSlave类的成员变量script_injections_描述的一个Vector中。

       这一步执行完成后,每一个Render进程就会获得当前加载的所有Extension定义的Content Script。这些Content Script由一个UserScriptSlave对象维护。以后每当Render进程加载一个网页,就会询问这个UserScriptSlave对象,是否需要往里面注入相应的Content Script。

       接下来,我们以前面提到的Page action example的Content Script注入到URL为“https://fast.com/”的网页为例,分析Content Script注入宿主网页执行的过程。从Page action example的Content Script的定义可以知道,它是在URL为“https://fast.com/”的网页的Document对象创建时注入的。

       前面提到,网页的Document对象是在WebKit中创建的。WebKit为网页创建了Document对象之后,会调用Content层的一个RenderFrameImpl对象的成员函数didCreateDocumentElement,用来通知后者,它描述的网页的Document对象已经创建好了,如下所示:

void RenderFrameImpl::didCreateDocumentElement(blink::WebLocalFrame* frame) {
  ......

  FOR_EACH_OBSERVER(RenderViewObserver, render_view_->observers(),
                    DidCreateDocumentElement(frame));
}
       这个函数定义在文件external/chromium_org/content/renderer/render_frame_impl.cc中。

       RenderFrameImpl类的成员变量render_view_指向的是一个RenderViewImpl对象。这个RenderViewImpl对象的内部维护有一系列的Render View Observer。这些Render View Observer用来监听WebKit事件。这样,如果一个模块要监听某一个网页的WebKit事件,那么就往与该网页对应的RenderViewImpl对象内部注册一个Render View Observer即可。

       Chromium的Extension模块会监听所有网页的WebKit事件,这是通过向与这些网页对应的RenderViewImpl对象内部注册一个类型为ExtensionHelper的Render View Observer实现的。这意味着当WebKit发出事件通知时,ExtensionHelper类的相应成员函数会被调用。在我们这个情景中,就是当网页的Document对象已经创建出来时,ExtensionHelper类的成员函数DidCreateDocumentElement会被调用。在调用的过程中,它就会往当前加载的网页注入那些运行时机为“document_start”的Content Script,如下所示:

void ExtensionHelper::DidCreateDocumentElement(WebLocalFrame* frame) {
  dispatcher_->user_script_slave()->InjectScripts(
      frame, UserScript::DOCUMENT_START);
  ......
}
      这个函数定义在文件external/chromium_org/extensions/renderer/extension_helper.cc中。

      ExtensionHelper类的成员变量dispatcher_指向的是一个Dispatcher对象。这个Dispatcher对象就是前面分析的用来处理从Browser进程发送过来的类型为ExtensionMsg_UpdateUserScripts的IPC消息的Dispatcher对象。ExtensionHelper类的成员函数DidCreateDocumentElement首先调用这个Dispatcher对象的成员函数user_script_slave获得它内部维护的一个UserScriptSlave对象。有了这个UserScriptSlave对象之后,就可以调用它的成员函数InjectScripts向当前加载的网页注入那些运行时机为“document_start”的Content Script,如下所示:

void UserScriptSlave::InjectScripts(WebFrame* frame,
                                    UserScript::RunLocation location) {
  GURL document_url = ScriptInjection::GetDocumentUrlForFrame(frame);
  ......

  ScriptInjection::ScriptsRunInfo scripts_run_info;
  for (ScopedVector<ScriptInjection>::const_iterator iter =
           script_injections_.begin();
       iter != script_injections_.end();
       ++iter) {
    (*iter)->InjectIfAllowed(frame, location, document_url, &scripts_run_info);
  }

  ......
}
       这个函数定义在文件external/chromium_org/extensions/renderer/user_script_slave.cc中。

       参数frame描述的就是当前加载的网页。UserScriptSlave类的成员函数InjectScripts首先通过调用ScriptInjection类的静态成员函数GetDocumentUrlForFrame获得这个网页的URL。有了这个URL之后,UserScriptSlave类的成员函数InjectScripts接下来就会遍历保存在成员变量script_injections_描述的Vector中的每一个ScriptInjection对象,并且调用这些ScriptInjection对象的成员函数InjectIfAllowed。

       从前面的分析可以知道,保存在UserScriptSlave类的成员变量script_injections_中的ScriptInjection对象描述的就是当前加载的Extension的Content Script。这些ScriptInjection对象的成员函数InjectIfAllowed在执行期间,就会判断是否需要将自己描述的Content Script注入到当前加载的网页中去执行,也就是前面获得的URL对应的网页。

       接下来我们就继续分析ScriptInjection类的成员函数InjectIfAllowed的实现,以便了解Content Script注入到宿主网页执行的过程,如下所示:

void ScriptInjection::InjectIfAllowed(blink::WebFrame* frame,
                                      UserScript::RunLocation run_location,
                                      const GURL& document_url,
                                      ScriptsRunInfo* scripts_run_info) {
  if (!WantsToRun(frame, run_location, document_url))
    return;

  const Extension* extension = user_script_slave_->GetExtension(extension_id_);
  DCHECK(extension);  // WantsToRun() should be false if there's no extension.

  // We use the top render view here (instead of the render view for the
  // frame), because script injection on any frame requires permission for
  // the top frame. Additionally, if we have to show any UI for permissions,
  // it should only be done on the top frame.
  content::RenderView* top_render_view =
      content::RenderView::FromWebView(frame->top()->view());

  int tab_id = ExtensionHelper::Get(top_render_view)->tab_id();

  // By default, we allow injection.
  bool should_inject = true;

  // Check if the extension requires user consent for injection *and* we have a
  // valid tab id (if we don't have a tab id, we have no UI surface to ask for
  // user consent).
  if (tab_id != -1 &&
      extension->permissions_data()->RequiresActionForScriptExecution(
          extension, tab_id, frame->top()->document().url())) {
    int64 request_id = kInvalidRequestId;
    int page_id = top_render_view->GetPageId();

    // We only delay the injection if the feature is enabled.
    // Otherwise, we simply treat this as a notification by passing an invalid
    // id.
    if (FeatureSwitch::scripts_require_action()->IsEnabled()) {
      should_inject = false;
      ScopedVector<PendingInjection>::iterator pending_injection =
          pending_injections_.insert(
              pending_injections_.end(),
              new PendingInjection(frame, run_location, page_id));
      request_id = (*pending_injection)->id;
    }

    top_render_view->Send(
        new ExtensionHostMsg_RequestContentScriptPermission(
            top_render_view->GetRoutingID(),
            extension->id(),
            page_id,
            request_id));
  }

  if (should_inject)
    Inject(frame, run_location, scripts_run_info);
}
      这个函数定义在文件external/chromium_org/extensions/renderer/script_injection.cc中。

      ScriptInjection类的成员函数InjectIfAllowed首先调用成员函数WantsToRun判断当前加载的网页是否是当前正在处理的Content Script感兴趣的网页,也就是判断当前加载的网页的URL是否匹配Content Script在其Extension的清单文件设置的URL规则。如果不匹配,那么就说明不需要将当前正在处理的Content Script注入到当前加载的网页中执行。

      如果匹配,ScriptInjection类的成员函数InjectIfAllowed还会进一步检查当前正在处理的Content Script所在的Extension是否对当前正在加载的网页申请了Permission。如果没有申请,并且Chromium启用了Scripts Require Action Feature,那么就需要用户同意后,才能将当前正在处理的Content Script注入到当前加载的网页中执行。这个需要用户同意的操作是通过向Browser进程发出一个类型ExtensionHostMsg_RequestContentScriptPermission的IPC消息触发的。

      我们假设当前加载的网页是当前正在处理的Content Script感兴趣的网页,并且Chromium没有开启Scripts Require Action Feature。这时候ScriptInjection类的成员函数InjectIfAllowed就会马上调用成员函数Inject将当前正在处理的Content Script注入到当前加载的网页中执行,如下所示:

void ScriptInjection::Inject(blink::WebFrame* frame,
                             UserScript::RunLocation run_location,
                             ScriptsRunInfo* scripts_run_info) const {
  ......

  if (ShouldInjectCSS(run_location))
    InjectCSS(frame, scripts_run_info);
  if (ShouldInjectJS(run_location))
    InjectJS(frame, scripts_run_info);
}
       这个函数定义在文件external/chromium_org/extensions/renderer/script_injection.cc中。

       ScriptInjection类的成员函数Inject首先调用成员函数ShouldInjectsCSS判断当前正在处理的Content Script是否包含有CSS Script,并且参数run_location描述的Content Script运行时机是否为“document_start”。如果都是的话,那么当前正在处理的Content Script包含的CSS Script就会通过调用另外一个成员函数InjectCSS注入到当前加载的网页中去。这意味着CSS Script只可以在宿主网页的Document对象创建时注入。

       ScriptInjection类的成员函数Inject接下来又调用成员函数ShouldInjectJS判断当前正在处理的Content Script是否包含有Java Script,并且参数run_location描述的Content Script运行时机是否为包含的Java Script在清单文件中指定的运行时机。如果都是的话,那么当前正在处理的Content Script包含的Java Script就会通过调用另外一个成员函数InjectJS注入到当前加载的网页中去执行。

       接下来我们只关注Java Script注入到宿主网页执行的过程,因此我们继续分析ScriptInjection类的成员函数InjectJS的实现,如下所示:

void ScriptInjection::InjectJS(blink::WebFrame* frame,
                               ScriptsRunInfo* scripts_run_info) const {
  const UserScript::FileList& js_scripts = script_->js_scripts();
  std::vector<blink::WebScriptSource> sources;
  scripts_run_info->num_js += js_scripts.size();
  for (UserScript::FileList::const_iterator iter = js_scripts.begin();
       iter != js_scripts.end();
       ++iter) {
    std::string content = iter->GetContent().as_string();

    .......
    sources.push_back(blink::WebScriptSource(
        blink::WebString::fromUTF8(content), iter->url()));
  }

  ......

  int isolated_world_id =
      user_script_slave_->GetIsolatedWorldIdForExtension(
          user_script_slave_->GetExtension(extension_id_), frame);
  ......

  DOMActivityLogger::AttachToWorld(isolated_world_id, extension_id_);
  frame->executeScriptInIsolatedWorld(isolated_world_id,
                                      &sources.front(),
                                      sources.size(),
                                      EXTENSION_GROUP_CONTENT_SCRIPTS);
  
 
  ......
}
       这个函数定义在文件external/chromium_org/extensions/renderer/script_injection.cc中。

       ScriptInjection类的成员函数InjectJS首先遍历将要执行的每一个Java Script文件。在遍历的过程中,每一个Java Script文件的内容都会被读取出来,并且封装在一个blink::WebScriptSource对象中。这些blink::WebScriptSource对象最后又会保存在本地变量sources描述的一个blink::WebScriptSource向量中。这个blink::WebScriptSource向量接下来会传递给WebKit中的JS引擎。JS引擎获得了这个向量之后,就会执行保存在里面的Java Script。

       Extension的Content Script是在一个Isolated World中执行的,这意味着它们不能访问在宿主网页中定义的JS变量和函数,也不能访问在宿主网页中注入的其它Extension的Content Script定义的JS变量和函数。为此,Chromium会为每一个Extension分配一个不同的Isolated World ID,使得它们的Content Script注入到宿主网页时,既不能互相访问各自定义的JS变量和函数,也不能访问宿主网页定义的JS变量和函数。

       按照上述规则,ScriptInjection类的成员函数InjectJS在注入指定的Java Script到宿主网页中执行之前,首先会调用成员变量user_script_slave_指向的一个UserScriptSlave对象的成员函数GetIsolatedWorldIdForExtension获得一个Isolated World ID。有了这个Isolated World ID之后,就可以调用参数frame指向的一个WebLocalFrameImpl对象的成员函数executeScriptInIsolatedWorld了。

       WebLocalFrameImpl类是WebKit向Chromium提供的一个API接口。这个API接口的成员函数executeScriptInIsolatedWorld会将指定的Java Script交给WebKit内部使用的JS引擎在指定的Isolated World中执行。在Chromium中,这个JS引擎就是V8引擎。V8引擎执行Java Script的过程,以后我们有机会再分析。

       至此,我们就分析完成Extension的Content Script注入到宿主网页中执行的过程了。这些Content Script虽然是在宿主网页中执行,但是它们是不能访问宿主网页定义的JS变量和函数的,也不能访问注入在宿主网页中的其它Extension的Content Script定义的JS变量和函数。不过,它们却可以操作宿主网页的DOM Tree,这样就可以修改宿主网页的UI和行为了。

       如果我们将Extension看作是一个App,那么它的Page和Content Script就可以看作是它的Module。既然是Module,它们之间就避免不了互相通信,以完成一个App的功能。Chromium为Extension的Page之间,以及Page和Content Script之间,均提供了通信接口。不过,Page之间的通信方式,与Page和Content Script之间的通信方式,是不一样的。这源于Page运行在所属Extension所加载在的Extension Process中,而Content Script运行在宿主网页所加载在的Render Process中。在接下来一篇文章中,我们就继续分析Extension的Page之间,以及Page与Content Script之间的通信机制,敬请关注!更多的信息也可以关注老罗的新浪微博:http://weibo.com/shengyangluo

作者:Luoshengyang 发表于2016/9/26 1:00:57 原文链接
阅读:224 评论:0 查看评论

Android 源码系列之从源码的角度深入理解Activity的launchModel特性

$
0
0

        转载请注明出处:http://blog.csdn.net/llew2011/article/details/52509515

        随着公司新业务的起步由于原有APP_A的包已经很大了,所以上边要求另外开发一款APP_B,要求是APP_A和APP_B账号通用且两个APP可以相互打开。账号通用也就是说在APP_A上登录了那么打开APP_B也就默认是登录状态,这个实现也不复杂就不介绍了;APP相互打开本来也不是难事,但是在测试的过程中发现了一个之前没有遇到的问题,现象如下图的demo所示:


        运行现象是在APP_A中打开了APP_B后,这时候在APP_B中进行任何操作都是没问题的,在APP_B不退出的情况下若摁了HOME键切换到桌面后此时再点击APP_A的icon图标打开APP_A时,发现界面竟然是APP_B的界面,当时感觉很诡异,是什么原因导致出现这种现象呢?当时就琢磨着可能是APP_B运行在了APP_A的任务栈中了,于是开始排查代码,在APP_B中响应APP_A的代码如下所示:

<activity
    android:name="com.llew.wb.A"
    android:label="@string/app_name" >
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />

        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />

        <data android:scheme="llew" />
    </intent-filter>
</activity>
        由于我们APP_A和APP_B约定了相互打开采用scheme的形式,所以响应代码看起来是没有问题的,接着查看在APP_A中打开APP_B的代码,如下所示:
public void openAPP_B1() {
	Uri uri = Uri.parse("llew://");
	Intent intent = new Intent(Intent.ACTION_VIEW, uri);
	startActivity(intent);
}
        这段就是打开我们APP_B的代码,看上去也没有什么问题,但是为什么会出现上述现象呢?然后我就尝试在openAPP_B中采用另外的方式,代码如下:
public void openAPP_B2() {
	Intent intent = getPackageManager().getLaunchIntentForPackage("packageName");
	if(null != intent) {
		startActivity(intent);
	}
}

        方式二以前使用过并看过这块相关源码,所以首先就想到了通过PackageManager来获取Intent来启动我们的APP_B,运行程序后发现第二种方式是没问题的,那也就是说在第二种中采用PackageManager获取到的Intent肯定是和采用第一种方式获取到的Intent是有区别的,那他们的区别在哪呢?先不说结论我们接着往下看,运行程序通过debug模式分别查看这两种方式获取到的Intent的不同之处:

        方式一的intent截图如下所示:

        方式二的intent截图如下所示:

        通过对比这两种方式的Intent对象可以发现方式二中的intent对象包含了flg属性,而该flg属性的值恰好是Intent.FLAG_ACTIVITY_NEW_TASK的值,这时候豁然开朗了,原来方式二中的Intent添加了FLAG_ACTIVITY_NEW_TASK标记,也就是说采用方式一开打APP_B时的页面是运行在APP_A的任务栈中,而通过方式二打开APP_B的页面运行在了新的任务栈中。为了证明通过方式一打开的APP_B的页面是运行在APP_A的任务栈中,我们可以使用adb shell dumpsys activity activities 命令来查看Activity任务栈的情况,截图如下:

        然后我们在方式一中的Intent也添加FLAG_ACTIVITY_NEW_TASK标记在运行一下,使用adb shell dumpsys activity activities 命令查看一下,截图如下:

        出现以上问题的原因就是APP_B运行在了APP_A的任务栈中,解决方法也就是在启动APP_B的时候让APP_B运行在新的任务栈中,接下来顺带进入源码看一看通过PackageManager获取到的Intent对象在哪赋值的flag标记吧,在Activity中调用getPackageManager()辗转调用的是其间接父类ContextWrapper的getPackageManager()的方法,源码如下所示:

@Override
public PackageManager getPackageManager() {
	// mBase为Context类型,其实现类为ContextImpl
    return mBase.getPackageManager();
}
        ContextWrapper的getPackageManager()方法中调用的是Context的getPackageManager()同名方法,而mBase的实现类为ContextImpl,所以我们直接查看ContextImpl的getPackageManager()方法,源码如下:
@Override
public PackageManager getPackageManager() {
	// 如果mPackageManager非空就直接返回
    if (mPackageManager != null) {
        return mPackageManager;
    }

    // 通过ActivityThread获取IPackageManager对象pm
    IPackageManager pm = ActivityThread.getPackageManager();
    if (pm != null) {
    	// 新建ApplicationPackageManager对象并返回
        // Doesn't matter if we make more than one instance.
        return (mPackageManager = new ApplicationPackageManager(this, pm));
    }

    return null;
}
        通过源码我们知道getPackageManger()方法获取的是ApplicationPackageManager对象,获取Intent对象就是调用该对象的getLaunchIntentForPackage()方法,源码如下:
@Override
public Intent getLaunchIntentForPackage(String packageName) {
    // First see if the package has an INFO activity; the existence of
    // such an activity is implied to be the desired front-door for the
    // overall package (such as if it has multiple launcher entries).
    Intent intentToResolve = new Intent(Intent.ACTION_MAIN);
    intentToResolve.addCategory(Intent.CATEGORY_INFO);
    intentToResolve.setPackage(packageName);
    List<ResolveInfo> ris = queryIntentActivities(intentToResolve, 0);

    // Otherwise, try to find a main launcher activity.
    if (ris == null || ris.size() <= 0) {
        // reuse the intent instance
        intentToResolve.removeCategory(Intent.CATEGORY_INFO);
        intentToResolve.addCategory(Intent.CATEGORY_LAUNCHER);
        intentToResolve.setPackage(packageName);
        ris = queryIntentActivities(intentToResolve, 0);
    }
    if (ris == null || ris.size() <= 0) {
        return null;
    }
    // 运行到这里是查找到了符合条件的Intent了,新建Intent
    Intent intent = new Intent(intentToResolve);
    // 在这里给Intent添加了我们期待的FLAG_ACTIVITY_NEW_TASK标签
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    intent.setClassName(ris.get(0).activityInfo.packageName, ris.get(0).activityInfo.name);
    // 返回新建的Intent对象
    return intent;
}

        通过源码我们看到在ApplicationPackageManager的getLaunchIntentForPackage()方法中给符合条件的Intent添加了FLAG_ACTIVITY_NEW_TASK标签,而该标签的作用就是为目标Activity开启新的任务栈并把目标Activity放到栈底。

        开始讲解Activity的launchMode之前我们先提一下任务和返回栈的概念,以下部分内容参考自官方文档

  • 任务和返回栈
            应用通常包含多个Activity。每个 Activity 均应围绕用户可以执行的特定操作设计,并且能够启动其他 Activity。 例如,电子邮件应用可能有一个 Activity 显示新邮件的列表。用户选择某邮件时,会打开一个新 Activity 以查看该邮件。
            一个 Activity 甚至可以启动设备上其他应用中存在的 Activity。例如,如果应用想要发送电子邮件,则可将 Intent 定义为执行“发送”操作并加入一些数据,如电子邮件地址和电子邮件。 然后,系统将打开其他应用中声明自己处理此类 Intent 的 Activity。在这种情况下, Intent 是要发送电子邮件,因此将启动电子邮件应用的“撰写”Activity(如果多个 Activity 支持相同 Intent,则系统会让用户选择要使用的 Activity)。发送电子邮件时,Activity 将恢复,看起来好像电子邮件 Activity 是您的应用的一部分。 即使这两个 Activity 可能来自不同的应用,但是 Android 仍会将 Activity 保留在相同的任务中,以维护这种无缝的用户体验。
            任务是指在执行特定作业时与用户交互的一系列 Activity。 这些 Activity 按照各自的打开顺序排列在堆栈(即“返回栈”)中。
            设备主屏幕是大多数任务的起点。当用户触摸应用启动器中的图标(或主屏幕上的快捷键)时,该应用的任务将出现在前台。 如果应用不存在任务(应用最近未曾使用),则会创建一个新任务,并且该应用的“主”Activity 将作为堆栈中的根 Activity 打开。
            当前 Activity 启动另一个 Activity 时,该新 Activity 会被推送到堆栈顶部,成为焦点所在。 前一个 Activity 仍保留在堆栈中,但是处于停止状态。Activity 停止时,系统会保持其用户界面的当前状态。 用户按“返回”按钮时,当前 Activity 会从堆栈顶部弹出(Activity 被销毁),而前一个 Activity 恢复执行(恢复其 UI 的前一状态)。 堆栈中的 Activity 永远不会重新排列,仅推入和弹出堆栈:由当前 Activity 启动时推入堆栈;用户使用“返回”按钮退出时弹出堆栈。 因此,返回栈以“后进先出”对象结构运行。 图 1 通过时间线显示 Activity 之间的进度以及每个时间点的当前返回栈,直观呈现了这种行为。

            如果用户继续按“返回”,堆栈中的相应 Activity 就会弹出,以显示前一个 Activity,直到用户返回主屏幕为止(或者,返回任务开始时正在运行的任意 Activity)。 当所有 Activity 均从堆栈中删除后,任务即不复存在。

            
    由于返回栈中的 Activity 永远不会重新排列,因此如果应用允许用户从多个 Activity 中启动特定 Activity,则会创建该 Activity 的新实例并推入堆栈中(而不是将 Activity 的任一先前实例置于顶部)。 因此,应用中的一个 Activity 可能会多次实例化(即使 Activity 来自不同的任务)。
            【注意:】后台可以同时运行多个任务。但是,如果用户同时运行多个后台任务,则系统可能会开始销毁后台 Activity,以回收内存资源,从而导致 Activity 状态丢失。

        好了,用了不小篇幅介绍了任务和返回栈的概念,若要改变返回栈的默认行为,可通过Activity的launchMode以及Intent的Flag标签,我们今天主要讲解的是通过launchMode来改变任务栈的默认行为,Android系统为launchMode提供了四种机制,分别是standard,singleTop,singleTask,singleInstance,为了方便查看任务栈的相关信息,这里给大家说一个命令:adb shell dumpsys activity,如果有对该命令不熟悉的,请自行查阅并掌握。下面我们来逐一讲解launchMode的各个属性值。

  • standard
            该属性是Activity默认情况下的启动模式,也就是说我们如果没有在manifest.xml中声明Activity的launchMode属性,系统会默认为Activity配置成standard,每次启动该Activity时系统都会在当前的任务栈中新建一个该Activity的实例并加入任务栈中。
    【例如:A和B都是standard】打开顺序为:A→B→B→B,则任务栈中的顺序如下所示:

  • singleTop
            1、如果Activity的launchMode属性定义成了singleTop,若在当前任务栈中已经存在该Activity的实例并且在栈顶位置,那再次打开该Activity都不会新建该Activity的实例,此时会回调该Activity的onNewIntent()方法。【注意:】如果Activity的launchMode属性为singleTop,则taskAffinity属性无效。
            【例如:B为singleTop,其它为默认】打开顺序为:A→B→B→B,则任务栈中的顺序如下所示:

            2、如果Activity的launchMode属性定义成了singleTop,若在当前任务栈中已经存在该Activity的实例且不在栈顶,那此时会继续新建该Activity的实例
            【例如:B为singleTop,其它为默认】打开顺序为:A→B→C→B→C→B→C→B

  • singleTask
            1、如果Activity的launchMode属性定义成了singleTask,如果此时没有声明taskAffinity属性(当不声明taskAffinity属性,那么Activity就会以包名作为其默认值)
            【例如:B为singleTask,其它为默认】打开顺序为:A→B→C→D→B

            2、如果Activity的launchMode属性定义成了singleTask,如果此时声明了taskAffinity属性且该属性不同于包名,则
            【例如:B为singleTask,其它为默认】打开顺序为:A→B

            【例如:B为singleTask,其它为默认】打开顺序为:A→B→C

            【例如:B为singleTask,其它为默认】打开顺序为:A→B→C→D

            【例如:B为singleTask,其它为默认】打开顺序为:A→B→C→D→B

             根据运行结果我们发现,当Activity的launchMode设置成singleTask,singTask保证了当前任务栈中只有一个该Activity的实例,若该Activity不在栈顶,则会清除该Activity之上的所有的Activity并回调该Activity的onNewIntent()方法,singleTask的使用小结如下所示:
    if( 发现一个 Task 的 affinity == Activity 的 affinity ){
        if(此 Activity 的实例已经在这个 Task 中){
            这个 Activity 启动并且清除顶部的 Acitivity ,通过标识 CLEAR_TOP 
        } else {
            在这个 Task 中新建这个 Activity 实例
        }
    } else { // Task 的 affinity 属性值与 Activity 不一样
        新建一个 affinity 属性值与之相等的 Task
        新建一个 Activity 的实例并且将其放入这个 Task 之中
    }
  • singleInstance
          1、singleInstance稍微比singleTask好理解,singleInstance的Activity只能在一个新的Task中并且这个Task中有且只能有这一个Activity,举个栗子
             【例如:B为singleInstance,其它为默认】打开顺序为:A→B
            【例如:B为singleInstance,其它为默认】打开顺序为:A→B→C→D→C

        根据以上结果我们已经大致掌握了launchMode的各种特性了,为了深刻理解需要小伙伴们自己动手实验尝试各种情况下的Activity的打开方式。本篇文章到此就结束了,感谢观看(*^__^*) ……



        【参考文章:】

        1、https://developer.android.com/guide/components/tasks-and-back-stack.htm

        2、http://www.songzhw.com/2016/08/09/explain-activity-launch-mode-with-examples/



作者:llew2011 发表于2016/9/26 8:23:20 原文链接
阅读:63 评论:0 查看评论

Github项目解析(十三)-->使用Kotlin实现UC头条ViewPager左右滑动效果

$
0
0

转载请标明出处:一片枫叶的专栏

上一篇文章中我们讲解了一个使用的多行文本显示控件,在实际开发过程中我们时常会遇到这种需求:有两个TextView控件分行显示,当第一个TextView的内容过多一行显示不下时,我们需要将第二个TextView在第一个TextView的第二行末尾显示,当第二个TextView第二行也显示不下时,第一个TextView的第二行结尾以“…”结束,第二个TextView显示在第二行的最后段,而上一篇文章介绍的就是一个实现这种需求的自定义控件。

而本文我们将介绍一个使用kotlin实现的仿照UC头条ViewPager的左右滑动效果。这个项目是为了学习kotlin的使用以及基本语法,在实现的过程中主要需要注意的有两点:一个是UC头条在滑动过程中的遮盖动画效果,一个是跨多个Tab点击屏蔽多个页面滑动效果。

本项目的github地址:android-xmviewpager,欢迎star和follow。

在介绍具体的使用说明之前,我们先看一下简单的实现效果:

这里写图片描述

实现说明

本项目是通过TabLayout+ViewPager的方式实现的,这里我们首先看一下整个页面的布局文件的实现方式:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".java.TabLayoutActivity"
    android:orientation="vertical">
    <android.support.design.widget.AppBarLayout
        android:id="@+id/appbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            app:layout_scrollFlags="scroll|enterAlways"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>

        <android.support.design.widget.TabLayout
            android:id="@+id/tabs"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:tabIndicatorColor="#ADBE107E"
            app:tabIndicatorHeight="0dp"
            app:tabMode="scrollable"
            app:tabPadding="0dp"
            />

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

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

</LinearLayout>

可以发现其是通过TabLayout和ViewPager的方式实现的,在实现过程中由于要求点击跨多个Tab的时候屏蔽多次滑动效果,这里重写了TabLayout的onTabSelectedListener监听:

/**
     * 自定义函数, : Unit 表示函数没有返回值
     */
    fun initViewPager() : Unit {
        /**
         * 获取初始化数据
         */
        val titles = ViewData().getTitles()

        /**
         * as 类似于java中的类型强转
         */
        val toolbar = findViewById(R.id.toolbar) as Toolbar
        setSupportActionBar(toolbar)
        mViewPager = findViewById(R.id.viewpager) as ViewPager
        mTabLayout = findViewById(R.id.tabs) as TabLayout

        /**
         * 通过 in 关键字实现循环遍历
         * 在调用mTabLayou变量的方法时,由于mTabLayout可能为空,所以在调用方法时添加!!
         * titles[] 与 titles.get 方法的功能是一样的
         * titles.indices 获取的是数组的下标
         */
        for (i in titles.indices) {
            mTabLayout!!.addTab(mTabLayout!!.newTab().setText(titles[i]))
        }

        val fragments = ArrayList<Fragment>()

        /**
         * 循环遍历添加ViewPager的Fragment
         */
        for (i in titles.indices) {
            val listFragment = MListFragment()
            val bundle = Bundle()
            val sb = StringBuffer()
            for (j in 1..8) {
                sb.append(titles[i]).append(" ")
            }
            bundle.putString("content", sb.toString())
            listFragment.arguments = bundle
            fragments.add(listFragment)
        }

        val mFragmentAdapteradapter = MFragmentAdapter(supportFragmentManager, fragments, titles)
        mViewPager!!.adapter = mFragmentAdapteradapter
        mViewPager!!.adapter = mFragmentAdapteradapter
        mTabLayout!!.setupWithViewPager(mViewPager)
        mTabLayout!!.setTabsFromPagerAdapter(mFragmentAdapteradapter)

        /**
         * 自定义设置ViewPager切换动画
         */
        mViewPager!!.setPageTransformer(true, MTransformer())

        /**
         * 通过object : TabLayout.OnTabSelectedListener 的方式创建内部匿名类(这里主要是接口)
         */
        mTabLayout!!.setOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
            override fun onTabReselected(tab: TabLayout.Tab?) {
            }

            override fun onTabUnselected(tab: TabLayout.Tab?) {
            }

            override fun onTabSelected(tab: TabLayout.Tab?) {
                /**
                 * 控制变量
                 */
                if (isOk) {
                    isOk = false
                    val currentItemIndex = mViewPager!!.currentItem

                    if (Math.abs(currentItemIndex - tab!!.position) > 1) {
                        /**
                         * 向后点击
                         */
                        if (currentItemIndex <= tab!!.position) {
                            mViewPager!!.setCurrentItem(tab.position - 1, false)
                            mViewPager!!.setCurrentItem(tab.position, true)
                        }
                        /**
                         * 向前点击
                         */
                        else {
                            mViewPager!!.setCurrentItem(tab.position + 1, false)
                            mViewPager!!.setCurrentItem(tab.position, true)
                        }
                    } else {
                        mViewPager!!.setCurrentItem(tab.position, true)
                    }

                    isOk = true
                }
            }
        })

    }

然后我们可以继续看一下初始化数据的实现:

/**
 * Created by aaron on 16/9/14.
 * 主要用于保存界面ViewPager数据
 */
class ViewData {

    /**
     * 该方法用于获取ViewPager TAB 显示数据
     */
    fun getTitles() : ArrayList<String> {
        /**
         * 通过类名创建该类的对象,这里直接调用java中的集合框架
         */
        val titles = ArrayList<String>()

        titles.clear()
        titles.add("推荐")
        titles.add("视频")
        titles.add("热点")
        titles.add("娱乐")
        titles.add("体育")
        titles.add("北京")
        titles.add("财经")
        titles.add("科技")
        titles.add("汽车")
        titles.add("社会")
        titles.add("搞笑")
        titles.add("军事")
        titles.add("历史")
        titles.add("涨知识")
        titles.add("NBA")
        titles.add("两性")

        return titles
    }

}

好吧,这其中需要注意的是:viewPager的setCurrentItem方法,表示会将viewPager的当前显示Item设置为指定的item,而我们可以发现这里的setCurrentItem有两个参数,第一个参数,是显示当前Item的position,而第二个参数为boolean类型,表示是否有滑动效果,比如当前我们在ViewPager的第一项,而我们点击了TabLayout的第八项,这时候如果我们调用了:setCurrentItem(8, true),它表示我们将滑动到ViewPager的第八项,且有滚动效果。这样我们做一下变通,当我们点击的TabLayout与当前Item的距离大于一个Item的时候就先滑动到当前Item的前一个并且没有滑动效果,然后在执行一次setCurrentItem方法,这样在跨多个Tab点击的时候就屏蔽了多个Item滚动的效果了。

在实现过程中还需要实现滑动覆盖的效果,一开始想了很久包括使用ViewPager的setPageTransformer方法,但是还是没法实现这个思路,后来经过同事指点,终于搞定了。就是对ViewPager每一项item中的子View执行动画效果,这样就会实现需求的动画效果了。

以下是自己重写的setPageTransformer类:

/**
 * Created by aaron on 16/9/13.
 * 自定义实现ViewPager的切换动画效果
 */
class MTransformer : ViewPager.PageTransformer {

    /**
     * 回调方法,重写viewpager的切换动画
     */
    override fun transformPage(view: View, position: Float) {
        val pageWidth = view.width
        val wallpaper = view.findViewById(R.id.recycler_view)
        if (position < -1) { // [-Infinity,-1)
            wallpaper.translationX = 0.toFloat()
            view.translationX = 0.toFloat()
        } else if (position <= 1) { // [-1,1]
            wallpaper.translationX = pageWidth * getFactor(position)
            view.translationX = 8 * position
        } else { // (1,+Infinity]
            wallpaper.translationX = 0.toFloat()
            view.translationX = 0.toFloat()
        }
    }

    private fun getFactor(position: Float): Float {
        return -position / 2
    }

}

可以看到在我们自定义的PageTransformer中,我们通过findViewById方法获取了滑动Item的子View,并对子View执行translationX操作,进而实现了滑动Item的遮盖效果。

另外由于本文主要介绍Kotlin的使用,更多关于Kotlin的相关知识点,可参考:

Basic Syntax - Kotlin Programing

Kotlin:Android事件的Swift

Kotlin在Android工程中的应用

当然更具体的关于本控件的实现可以下载源码参考。

总结:

以上就是通过Kotlin实现的仿照UC头条ViewPager左右滑动效果的小项目。当然现在还很不完善,对于源码有兴趣的同学可以到github上看一下具体实现。项目地址:android-xmviewpager


另外对github项目,开源项目解析感兴趣的同学可以参考我的:
Github项目解析(一)–>上传Android项目至github
Github项目解析(二)–>将Android项目发布至JCenter代码库
Github项目解析(三)–>Android内存泄露监测之leakcanary
Github项目解析(四)–>动态更改TextView的字体大小
Github项目解析(五)–>Android日志框架
Github项目解析(六)–>自定义实现ButterKnife框架
Github项目解析(七)–>防止按钮重复点击
Github项目解析(八)–>Activity启动过程中获取组件宽高的五种方式
Github项目解析(九)–>实现Activity跳转动画的五种方式
Github项目解析(十)–>几行代码快速集成二维码扫描库
Github项目解析(十一)–>一个简单,强大的自定义广告活动弹窗
Github项目解析(十二)–>一个简单的多行文本显示控件

作者:qq_23547831 发表于2016/9/26 9:18:45 原文链接
阅读:40 评论:0 查看评论
Viewing all 5930 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>