From 944c5dacc37ab1ef60a2b0a32e9007773e97ecfa Mon Sep 17 00:00:00 2001 From: Nikita Yatskivskiy Date: Sun, 11 Nov 2018 21:14:42 +0300 Subject: [PATCH 01/22] Implement news section. --- app/build.gradle | 21 +- app/src/main/AndroidManifest.xml | 12 +- .../androidacademynewsapp/DateFormatter.java | 23 +++ .../androidacademynewsapp/MainActivity.kt | 12 -- .../androidacademynewsapp/data/Category.java | 24 +++ .../androidacademynewsapp/data/News.java | 75 +++++++ .../data/NewsRepository.java | 8 + .../data/NewsRepositoryImpl.java | 194 ++++++++++++++++++ .../news/details/NewsDetailsActivity.java | 84 ++++++++ .../presentation/news/list/NewsAdapter.java | 43 ++++ .../news/list/NewsItemDecorationImpl.java | 33 +++ .../news/list/NewsListActivity.java | 72 +++++++ .../news/list/NewsViewHolder.java | 63 ++++++ .../news/list/OnNewsClickListener.java | 8 + app/src/main/res/layout/activity_main.xml | 21 -- .../main/res/layout/activity_news_details.xml | 71 +++++++ .../main/res/layout/activity_news_list.xml | 14 ++ app/src/main/res/layout/app_bar.xml | 16 ++ app/src/main/res/layout/item_news.xml | 85 ++++++++ app/src/main/res/values-ru-rRU/strings.xml | 5 + app/src/main/res/values/dimens.xml | 6 + app/src/main/res/values/strings.xml | 5 +- app/src/main/res/values/styles.xml | 22 ++ build.gradle | 4 +- delegateadapter/.gitignore | 6 + delegateadapter/build.gradle | 34 +++ delegateadapter/proguard-rules.pro | 21 ++ delegateadapter/src/main/AndroidManifest.xml | 2 + .../adapterdelegate/BaseDelegateAdapter.java | 47 +++++ .../adapterdelegate/BaseViewHolder.java | 15 ++ .../CompositeDelegateAdapter.java | 98 +++++++++ .../adapterdelegate/IDelegateAdapter.java | 24 +++ .../ru/nikijava/adapterdelegate/Item.java | 4 + .../src/main/res/values/strings.xml | 3 + gradle.properties | 2 + gradle/wrapper/gradle-wrapper.properties | 4 +- settings.gradle | 2 +- 37 files changed, 1136 insertions(+), 47 deletions(-) create mode 100644 app/src/main/java/ru/nikijava/androidacademynewsapp/DateFormatter.java delete mode 100644 app/src/main/java/ru/nikijava/androidacademynewsapp/MainActivity.kt create mode 100644 app/src/main/java/ru/nikijava/androidacademynewsapp/data/Category.java create mode 100644 app/src/main/java/ru/nikijava/androidacademynewsapp/data/News.java create mode 100644 app/src/main/java/ru/nikijava/androidacademynewsapp/data/NewsRepository.java create mode 100644 app/src/main/java/ru/nikijava/androidacademynewsapp/data/NewsRepositoryImpl.java create mode 100644 app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/details/NewsDetailsActivity.java create mode 100644 app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/NewsAdapter.java create mode 100644 app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/NewsItemDecorationImpl.java create mode 100644 app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/NewsListActivity.java create mode 100644 app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/NewsViewHolder.java create mode 100644 app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/OnNewsClickListener.java delete mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/layout/activity_news_details.xml create mode 100644 app/src/main/res/layout/activity_news_list.xml create mode 100644 app/src/main/res/layout/app_bar.xml create mode 100644 app/src/main/res/layout/item_news.xml create mode 100644 app/src/main/res/values-ru-rRU/strings.xml create mode 100644 app/src/main/res/values/dimens.xml create mode 100644 delegateadapter/.gitignore create mode 100644 delegateadapter/build.gradle create mode 100644 delegateadapter/proguard-rules.pro create mode 100644 delegateadapter/src/main/AndroidManifest.xml create mode 100644 delegateadapter/src/main/java/ru/nikijava/adapterdelegate/BaseDelegateAdapter.java create mode 100644 delegateadapter/src/main/java/ru/nikijava/adapterdelegate/BaseViewHolder.java create mode 100644 delegateadapter/src/main/java/ru/nikijava/adapterdelegate/CompositeDelegateAdapter.java create mode 100644 delegateadapter/src/main/java/ru/nikijava/adapterdelegate/IDelegateAdapter.java create mode 100644 delegateadapter/src/main/java/ru/nikijava/adapterdelegate/Item.java create mode 100644 delegateadapter/src/main/res/values/strings.xml diff --git a/app/build.gradle b/app/build.gradle index 9b3efa3..6719151 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -20,14 +20,25 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + packagingOptions { + exclude 'META-INF/proguard/androidx-annotations.pro' + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'com.android.support:appcompat-v7:28.0.0' - implementation 'com.android.support.constraint:constraint-layout:1.1.3' + implementation "androidx.cardview:cardview:1.0.0" + implementation project(":delegateadapter") + implementation 'androidx.appcompat:appcompat:1.0.1' + implementation 'com.google.android.material:material:1.1.0-alpha01' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' testImplementation 'junit:junit:4.12' - androidTestImplementation 'com.android.support.test:runner:1.0.2' - androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' + androidTestImplementation 'androidx.test:runner:1.1.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' + implementation 'androidx.recyclerview:recyclerview:1.0.0' + implementation 'com.github.bumptech.glide:glide:4.8.0' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 08b2e0a..349774f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,21 +1,27 @@ + + + - + android:theme="@style/AppTheme" + tools:ignore="GoogleAppIndexingWarning"> + + \ No newline at end of file diff --git a/app/src/main/java/ru/nikijava/androidacademynewsapp/DateFormatter.java b/app/src/main/java/ru/nikijava/androidacademynewsapp/DateFormatter.java new file mode 100644 index 0000000..ace7ea3 --- /dev/null +++ b/app/src/main/java/ru/nikijava/androidacademynewsapp/DateFormatter.java @@ -0,0 +1,23 @@ +package ru.nikijava.androidacademynewsapp; + +import static android.text.format.DateUtils.DAY_IN_MILLIS; +import static android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE; +import static android.text.format.DateUtils.HOUR_IN_MILLIS; + +import android.content.Context; +import android.text.format.DateUtils; + +import java.util.Date; + +public class DateFormatter { + + public CharSequence formatDateTime(Context context, Date dateTime) { + return DateUtils.getRelativeDateTimeString( + context, + dateTime.getTime(), + HOUR_IN_MILLIS, + 5 * DAY_IN_MILLIS, + FORMAT_ABBREV_RELATIVE + ); + } +} diff --git a/app/src/main/java/ru/nikijava/androidacademynewsapp/MainActivity.kt b/app/src/main/java/ru/nikijava/androidacademynewsapp/MainActivity.kt deleted file mode 100644 index 35b3949..0000000 --- a/app/src/main/java/ru/nikijava/androidacademynewsapp/MainActivity.kt +++ /dev/null @@ -1,12 +0,0 @@ -package ru.nikijava.androidacademynewsapp - -import android.support.v7.app.AppCompatActivity -import android.os.Bundle - -class MainActivity : AppCompatActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - } -} diff --git a/app/src/main/java/ru/nikijava/androidacademynewsapp/data/Category.java b/app/src/main/java/ru/nikijava/androidacademynewsapp/data/Category.java new file mode 100644 index 0000000..a7ba2df --- /dev/null +++ b/app/src/main/java/ru/nikijava/androidacademynewsapp/data/Category.java @@ -0,0 +1,24 @@ +package ru.nikijava.androidacademynewsapp.data; + +import java.io.Serializable; + +import androidx.annotation.NonNull; + +public class Category implements Serializable { + private final int id; + @NonNull private final String name; + + public Category(int id, @NonNull String name) { + this.id = id; + this.name = name; + } + + public int getId() { + return id; + } + + @NonNull + public String getName() { + return name; + } +} diff --git a/app/src/main/java/ru/nikijava/androidacademynewsapp/data/News.java b/app/src/main/java/ru/nikijava/androidacademynewsapp/data/News.java new file mode 100644 index 0000000..0cdcfc4 --- /dev/null +++ b/app/src/main/java/ru/nikijava/androidacademynewsapp/data/News.java @@ -0,0 +1,75 @@ +package ru.nikijava.androidacademynewsapp.data; + +import java.io.Serializable; +import java.util.Date; + +import androidx.annotation.NonNull; +import ru.nikijava.adapterdelegate.Item; + +public final class News implements Item, Serializable { + + @NonNull private final String title; + @NonNull private final String imageUrl; + @NonNull private final Category category; + @NonNull private final Date publishDate; + @NonNull private final String previewText; + @NonNull private final String fullText; + + public News( + @NonNull String title, + @NonNull String imageUrl, + @NonNull Category category, + @NonNull Date publishDate, + @NonNull String previewText, + @NonNull String fullText + ) { + this.title = title; + this.imageUrl = imageUrl; + this.category = category; + this.publishDate = publishDate; + this.previewText = previewText; + this.fullText = fullText; + } + + @NonNull + public String getTitle() { + return title; + } + + @NonNull + public String getImageUrl() { + return imageUrl; + } + + @NonNull + public Category getCategory() { + return category; + } + + @NonNull + public Date getPublishDate() { + return publishDate; + } + + @NonNull + public String getPreviewText() { + return previewText; + } + + @NonNull + public String getFullText() { + return fullText; + } + + @Override + public String toString() { + return "News{" + + "title='" + title + '\'' + + ", imageUrl='" + imageUrl + '\'' + + ", category=" + category + + ", publishDate=" + publishDate + + ", previewText='" + previewText + '\'' + + ", fullText='" + fullText + '\'' + + '}'; + } +} diff --git a/app/src/main/java/ru/nikijava/androidacademynewsapp/data/NewsRepository.java b/app/src/main/java/ru/nikijava/androidacademynewsapp/data/NewsRepository.java new file mode 100644 index 0000000..21a0b4c --- /dev/null +++ b/app/src/main/java/ru/nikijava/androidacademynewsapp/data/NewsRepository.java @@ -0,0 +1,8 @@ +package ru.nikijava.androidacademynewsapp.data; + +import java.util.List; + +public interface NewsRepository { + + List getNews(); +} diff --git a/app/src/main/java/ru/nikijava/androidacademynewsapp/data/NewsRepositoryImpl.java b/app/src/main/java/ru/nikijava/androidacademynewsapp/data/NewsRepositoryImpl.java new file mode 100644 index 0000000..fe0fbb0 --- /dev/null +++ b/app/src/main/java/ru/nikijava/androidacademynewsapp/data/NewsRepositoryImpl.java @@ -0,0 +1,194 @@ +package ru.nikijava.androidacademynewsapp.data; + +import java.util.ArrayList; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; + +public class NewsRepositoryImpl implements NewsRepository { + + public List getNews() { + final Category darwinAwards = new Category(1, "Darwin Awards"); + final Category criminal = new Category(2, "Criminal"); + final Category animals = new Category(3, "Animals"); + final Category music = new Category(4, "Music"); + + List news = new ArrayList<>(); + news.add(new News( + "Tourist filmed sitting on 5m-long crocodile", + "https://e3.365dm.com/18/09/736x414/skynews-crocodile-australia_4433218.jpg", + darwinAwards, + createDate(2018, 9, 26, 10, 34), + "\"It was dangerous, I know. It is a scary feeling sitting on something that " + + "could kill you in a fraction of a " + + "second,\" he says.", + "A Danish tourist has admitted he took his life in his hands by sitting on a " + + "large crocodile in Australia.\n\n" + + "Niels Jensen, 22, was on safari in a wildlife park east of Darwin in " + + "northern Australia when he " + + "encountered the predator, estimated to be 4.7m (15ft) long and " + + "weighing 653kg (1428 lbs).\n\n" + + "The wildlife management graduate is filmed enticing the large reptile," + + " which had been relocated to the " + + "park after it was caught preying on livestock, with a wallaby carcass." + + "After leaving the bait on the ground and waiting for the crocodile to " + + "start eating, he astonishingly " + + "straddled the reptile's back, sitting just behind its rear legs.\n\n" + + "He touched some of the scales on the animal's back, and, after a few " + + "moments, rose and walked away.\n\n" + + "But a man out of shot told him to get back and give a thumbs-up, so he" + + " approached the animal for a second" + + " time, sat down again, turned towards the camera, smiled and put this " + + "thumb in the air.\n\n" + + "Mr Jensen admitted he took life in his hands by sitting on a live " + + "crocodile for the first time." + )); + news.add(new News( + "Police warn daredevil cliff jumpers who are 'risking their lives for likes'", + "https://e3.365dm.com/18/09/2048x1152/skynews-cliff-jumping-greg-milam_4433647.jpg", + criminal, + createDate(2018, 9, 25, 12, 45), + "Police in Los Angeles say they are spending hundreds of thousands of dollars " + + "airlifting cliff jumpers out of " + + "dangerous spots.", + "Daredevils attempting dangerous cliff dives in a quest for likes has led to an " + + "increase in costly helicopter " + + "airlifts in California, police say.\n\n" + + "As young people pursue the perfect selfie or video for their social " + + "media pages, the Los Angeles County " + + "Sheriff's Department says it is spending hundreds of thousands of " + + "dollars plucking the injured and " + + "stranded from beauty spot locations. \"People have to understand: " + + "people die up in those mountains. " + + "For every rescue you see that we do, there are ones that we don't make" + + ". They're dead,\" said Deputy " + + "Stephen Doucette.\n\n" + + "A social media search for locations like Eaton Canyon, Hermit Falls " + + "and Malibu Creek Rock Pool reveal " + + "dozens of risky selfie videos. Two men were recently rescued after " + + "being injured while being filmed at " + + "Hermit Falls." + )); + news.add(new News( + "Bear saved after getting his head stuck in milk can", + "https://e3.365dm.com/18/09/2048x1152/skynews-bear-minnesota_4419111.jpg", + animals, + createDate(2018, 9, 20, 14, 4), + "Firefighters used the Jaws of Life to free the young black bear, a tool which is" + + " normally used to extricate car" + + " accident victims.", + "A bear has been freed after getting his head stuck in a milk can.\n\n" + + "Firefighters were called to help after a conservation officer " + + "encountered the grizzly sight in " + + "Minnesota.\n\n" + + "The young black male bear's head was stuck inside an old 10 gallon (38" + + " litre) milk can.\n\n" + + "At first, rescuers tried to use cooking oil to free the animal. When " + + "that didn't work, they drilled three" + + " holes in the milk can so the panting bear could breathe.\n\n" + + "Two hours later, firefighters used the \"Jaws of Life\" - a tool which" + + " is normally used to extricate car " + + "accident victims - and a spreader to pry the can off.\n\n" + + "After being released, the seemingly healthy bear ran off into the woods." + )); + news.add(new News( + "Nearly $18m of cocaine seized in donated boxes of bananas", + "https://e3.365dm.com/18/09/2048x1152/skynews-texas-bananas-drugs_4430760.jpg", + criminal, + createDate(2018, 9, 18, 4, 4), + "Massive quantities of the drug were found in boxes of fruit that had been " + + "donated to the Texas Department of " + + "Criminal Justice.", + "A huge haul of cocaine was discovered hidden in boxes of bananas donated to the " + + "Texas Department of Criminal " + + "Justice.\n\n" + + "Some 45 boxes of bananas from Ports of America in Freeport were given " + + "away to the agency because they " + + "were already ripe.\n\n" + + "According to a Facebook post on the TDCJ's page, when two sergeants of" + + " the Scott Unit arrived to pick " + + "them up they \"discovered something not quite right\".\n\n" + + "The post explains: \"One of the boxes felt different than the others" + + ".\n\n" + + "\"They snipped the straps, pulled free the box, and opened it up.\n\n" + + "\"Inside, under a bundle of bananas, he found another bundle! Inside " + + "that? What appeared to be a white " + + "powdery substance.\n\n" + + "\"They immediately notified port authorities and awaited their " + + "instruction.\"" + + "US Customs arrived and tested the substance, which confirmed the " + + "powder was cocaine." + )); + news.add(new News( + "US government hacker jailed after losing secrets", + "https://e3.365dm" + + ".com/17/09/736x414/d55722dc4eb37f6959d2e047c14710d586aab99f90aa1e4acfd9f992125294f5_4107038.jpg", + criminal, + createDate(2018, 9, 17, 12, 45), + "Nghia Hoang Pho, 68, who developed hacking tools for the National Security " + + "Agency, illegally stored material " + + "on his home computer.", + "A man who illegally took home hacking tools from his workplace at the National " + + "Security Agency, and then " + + "allegedly lost them to Russian intelligence, has been jailed for five " + + "years and six months.\n\n" + + "Nghia Hoang Pho, 68, developed hacking tools at the NSA's elite " + + "Tailored Access Operations (TAO) unit, " + + "which works on penetrating target computer networks for the US " + + "intelligence community.\n\n" + + "While employed by the NSA between 2010 and 2015, Pho took home what " + + "prosecutors described as \"massive " + + "troves of highly classified national defence information\" and stored " + + "those troves on his home computer " + + "network.\n\n" + + "Reports have alleged that while these tools were stored on his home " + + "computer, Pho installed Kaspersky " + + "Lab anti-virus software, which Russian intelligence then used to steal" + + " those tools for themselves.\n\n" + + "Although the company has vigorously denied claims its software was " + + "used by Russian intelligence to steal" + + " the data, the publicity damage has left Kaspersky Lab working to " + + "address customer fears in a global " + + "transparency initiative - including moving a significant portion of " + + "its operations from Russia to " + + "Switzerland.\n\n" + + "An internal investigation at the cyber security company into the " + + "incident prompted the company to suggest" + + " that an NSA employee had actually been hacked when he downloaded " + + "pirate software and disabled " + + "Kaspersky's anti-virus." + )); + news.add(new News( + "Wet Wet Wet announce Liberty X star Kevin Simm as new frontman", + "https://e3.365dm.com/18/09/2048x1152/skynews-wet-wet-wet-kevin-simm_4433314.jpg", + music, + createDate(2018, 9, 17, 12, 45), + "The Voice 2016 winner says he was \"really taken aback\" by the opportunity " + + "after singing the band's songs " + + "early in his career.", + "The Scottish band, who are best-known for their 1994 cover of The Troggs' 1960s " + + "hit Love Is All Around, " + + "revealed the change in line-up on Tuesday.\n\n" + + "Simm, 38, who won The Voice in 2016, will take over singing duties " + + "after founding member Marti Pellow " + + "left the band last year.\n\n" + + "Simm, from Lancashire, first shot to fame on ITV talent show Popstars " + + "in 2001 after forming the group " + + "Liberty X with four other runner-up contestants.\n\n" + + "He has recalled singing Wet Wet Wet's songs early in his career.He " + + "said: \"I was really taken aback, the" + + " opportunity to join a band with such amazing songs and great guys and" + + " a great fanbase really " + + "excites me.\n\n" + + "\"When I first started gigging around the pubs and clubs up North, two" + + " songs that were always in my set " + + "were Goodnight Girl and Love Is All Around.\"" + )); + + return news; + } + + private Date createDate(int year, int month, int date, int hrs, int min) { + return new GregorianCalendar(year, month - 1, date, hrs, min).getTime(); + } +} diff --git a/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/details/NewsDetailsActivity.java b/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/details/NewsDetailsActivity.java new file mode 100644 index 0000000..37a0c5e --- /dev/null +++ b/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/details/NewsDetailsActivity.java @@ -0,0 +1,84 @@ +package ru.nikijava.androidacademynewsapp.presentation.news.details; + +import static android.content.res.Configuration.ORIENTATION_PORTRAIT; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import android.widget.TextView; + +import com.bumptech.glide.Glide; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import ru.nikijava.androidacademynewsapp.DateFormatter; +import ru.nikijava.androidacademynewsapp.R; +import ru.nikijava.androidacademynewsapp.data.News; + +public class NewsDetailsActivity extends AppCompatActivity { + + private static String NEWS_KEY = "news_key"; + private final DateFormatter dateFormatter = new DateFormatter(); + + private News news; + + private ImageView ivImage; + private TextView tvTitle; + private TextView tvDate; + private TextView tvDetails; + private ScrollView svContent; + + public static void start(Context context, News news) { + Intent intent = new Intent(context, NewsDetailsActivity.class); + intent.putExtra(NEWS_KEY, news); + context.startActivity(intent); + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_news_details); + news = (News) getIntent().getSerializableExtra(NEWS_KEY); + getSupportActionBar().setTitle(news.getCategory().getName()); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + initViews(); + showNews(); + } + + @Override + public boolean onSupportNavigateUp() { + onBackPressed(); + return true; + } + + private void initViews() { + ivImage = findViewById(R.id.ivImage); + tvTitle = findViewById(R.id.tvTitle); + tvDate = findViewById(R.id.tvDate); + tvDetails = findViewById(R.id.tvDetails); + svContent = findViewById(R.id.svContent); + } + + private void showNews() { + tvDate.setText(dateFormatter.formatDateTime(this, news.getPublishDate())); + tvDetails.setText(news.getFullText()); + tvTitle.setText(news.getTitle()); + setImage(); + } + + private void setImage() { + int orientation = getResources().getConfiguration().orientation; + if (orientation == ORIENTATION_PORTRAIT) ivImage.post(this::recomputeAvatarSize); + Glide.with(this).load(news.getImageUrl()).into(ivImage); + } + + private void recomputeAvatarSize() { + float layoutPosition = svContent.getHeight(); + LinearLayout.LayoutParams params = + (LinearLayout.LayoutParams) ivImage.getLayoutParams(); + params.height = (int) (layoutPosition * 0.4); + } +} diff --git a/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/NewsAdapter.java b/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/NewsAdapter.java new file mode 100644 index 0000000..3f3c349 --- /dev/null +++ b/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/NewsAdapter.java @@ -0,0 +1,43 @@ +package ru.nikijava.androidacademynewsapp.presentation.news.list; + +import android.view.View; + +import com.bumptech.glide.RequestManager; + +import java.util.List; + +import androidx.annotation.NonNull; +import ru.nikijava.adapterdelegate.BaseDelegateAdapter; +import ru.nikijava.androidacademynewsapp.R; +import ru.nikijava.androidacademynewsapp.data.News; + +public class NewsAdapter extends BaseDelegateAdapter { + + private static final String TAG = NewsAdapter.class.getSimpleName(); + @NonNull private final OnNewsClickListener onNewsClickListener; + @NonNull private final RequestManager requestManager; + + public NewsAdapter( + @NonNull OnNewsClickListener onNewsClickListener, + @NonNull RequestManager requestManager + ) { + this.onNewsClickListener = onNewsClickListener; + this.requestManager = requestManager; + } + + @Override + public int getLayoutId() { + return R.layout.item_news; + } + + @NonNull + @Override + public NewsViewHolder createViewHolder(@NonNull View itemView) { + return new NewsViewHolder(itemView, onNewsClickListener, requestManager); + } + + @Override + public boolean isForViewType(@NonNull List items, int position) { + return items.get(position) instanceof News; + } +} diff --git a/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/NewsItemDecorationImpl.java b/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/NewsItemDecorationImpl.java new file mode 100644 index 0000000..93177bf --- /dev/null +++ b/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/NewsItemDecorationImpl.java @@ -0,0 +1,33 @@ +package ru.nikijava.androidacademynewsapp.presentation.news.list; + +import android.graphics.Rect; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +public class NewsItemDecorationImpl extends RecyclerView.ItemDecoration { + + private final int offset; + + public NewsItemDecorationImpl(int offset) { + this.offset = offset; + } + + @Override + public void getItemOffsets( + @NonNull Rect outRect, + @NonNull View view, + @NonNull RecyclerView parent, + @NonNull RecyclerView.State state + ) { + + final int position = parent.getChildLayoutPosition(view); + + if (position != RecyclerView.NO_POSITION) { + outRect.set(offset, offset, offset, offset); + } else { + outRect.set(0, 0, 0, 0); + } + } +} diff --git a/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/NewsListActivity.java b/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/NewsListActivity.java new file mode 100644 index 0000000..1dfa146 --- /dev/null +++ b/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/NewsListActivity.java @@ -0,0 +1,72 @@ +package ru.nikijava.androidacademynewsapp.presentation.news.list; + +import android.content.res.Configuration; +import android.os.Bundle; + +import com.bumptech.glide.Glide; + +import java.util.ArrayList; +import java.util.List; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import ru.nikijava.adapterdelegate.CompositeDelegateAdapter; +import ru.nikijava.adapterdelegate.Item; +import ru.nikijava.androidacademynewsapp.R; +import ru.nikijava.androidacademynewsapp.data.NewsRepository; +import ru.nikijava.androidacademynewsapp.data.NewsRepositoryImpl; +import ru.nikijava.androidacademynewsapp.presentation.news.details.NewsDetailsActivity; + +public class NewsListActivity extends AppCompatActivity { + + private static final String TAG = NewsListActivity.class.getSimpleName(); + private final NewsRepository newsRepository = new NewsRepositoryImpl(); + private RecyclerView rvNewsList; + + protected void onCreate(@Nullable final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_news_list); + getSupportActionBar().setTitle(getString(R.string.top_news)); + rvNewsList = findViewById(R.id.rvNewsList); + initList(); + populateList(); + } + + private void initList() { + CompositeDelegateAdapter adapter = new CompositeDelegateAdapter.Builder() + .add(new NewsAdapter(item -> NewsDetailsActivity.start(this, item), + Glide.with(this))) + .build(); + + rvNewsList.setAdapter(adapter); + + RecyclerView.LayoutManager layoutManager; + switch (getResources().getConfiguration().orientation) { + case Configuration.ORIENTATION_LANDSCAPE: { + layoutManager = new GridLayoutManager(this, 2); + break; + } + default: { + layoutManager = new LinearLayoutManager(this, RecyclerView.VERTICAL, false); + } + } + + rvNewsList.setLayoutManager(layoutManager); + + rvNewsList.addItemDecoration(new NewsItemDecorationImpl( + getResources().getDimensionPixelSize(R.dimen.spacing_micro))); + } + + private void populateList() { + List data = new ArrayList<>(newsRepository.getNews()); + getAdapter().swapData(data); + } + + @SuppressWarnings("unchecked") + private CompositeDelegateAdapter getAdapter() { + return (CompositeDelegateAdapter) rvNewsList.getAdapter(); + } +} diff --git a/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/NewsViewHolder.java b/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/NewsViewHolder.java new file mode 100644 index 0000000..ac31b4a --- /dev/null +++ b/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/NewsViewHolder.java @@ -0,0 +1,63 @@ +package ru.nikijava.androidacademynewsapp.presentation.news.list; + +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import com.bumptech.glide.RequestManager; + +import java.text.SimpleDateFormat; +import java.util.Locale; + +import androidx.annotation.NonNull; +import ru.nikijava.adapterdelegate.BaseViewHolder; +import ru.nikijava.androidacademynewsapp.DateFormatter; +import ru.nikijava.androidacademynewsapp.R; +import ru.nikijava.androidacademynewsapp.data.News; + +public class NewsViewHolder extends BaseViewHolder implements View.OnClickListener { + + private static final String TAG = NewsViewHolder.class.getSimpleName(); + @NonNull private final OnNewsClickListener onNewsClickListener; + @NonNull private final RequestManager requestManager; + private final DateFormatter dateFormatter = new DateFormatter(); + + private final TextView tvCategory; + private final TextView tvTitle; + private final TextView tvBody; + private final ImageView ivImage; + private final TextView tvDate; + + private News item; + + public NewsViewHolder( + @NonNull View itemView, + @NonNull OnNewsClickListener onNewsClickListener, + @NonNull RequestManager requestManager + ) { + super(itemView); + itemView.setOnClickListener(this); + this.onNewsClickListener = onNewsClickListener; + this.requestManager = requestManager; + tvCategory = itemView.findViewById(R.id.tvCategory); + tvTitle = itemView.findViewById(R.id.tvTitle); + tvBody = itemView.findViewById(R.id.tvBody); + ivImage = itemView.findViewById(R.id.ivImage); + tvDate = itemView.findViewById(R.id.tvDate); + } + @Override + protected void bind(@NonNull News item) { + this.item = item; + tvCategory.setText(item.getCategory().getName()); + tvTitle.setText(item.getTitle()); + tvBody.setText(item.getFullText()); + SimpleDateFormat format = new SimpleDateFormat("MMM d, hh:mm", Locale.getDefault()); + tvDate.setText(dateFormatter.formatDateTime(itemView.getContext(), item.getPublishDate())); + requestManager.load(item.getImageUrl()).into(ivImage); + } + + @Override + public void onClick(View v) { + onNewsClickListener.onNewsClick(item); + } +} diff --git a/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/OnNewsClickListener.java b/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/OnNewsClickListener.java new file mode 100644 index 0000000..eabadfb --- /dev/null +++ b/app/src/main/java/ru/nikijava/androidacademynewsapp/presentation/news/list/OnNewsClickListener.java @@ -0,0 +1,8 @@ +package ru.nikijava.androidacademynewsapp.presentation.news.list; + +import ru.nikijava.androidacademynewsapp.data.News; + +public interface OnNewsClickListener { + + void onNewsClick(News news); +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index d46a68a..0000000 --- a/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_news_details.xml b/app/src/main/res/layout/activity_news_details.xml new file mode 100644 index 0000000..c92b525 --- /dev/null +++ b/app/src/main/res/layout/activity_news_details.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_news_list.xml b/app/src/main/res/layout/activity_news_list.xml new file mode 100644 index 0000000..00e964f --- /dev/null +++ b/app/src/main/res/layout/activity_news_list.xml @@ -0,0 +1,14 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/app_bar.xml b/app/src/main/res/layout/app_bar.xml new file mode 100644 index 0000000..a1f6595 --- /dev/null +++ b/app/src/main/res/layout/app_bar.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/layout/item_news.xml b/app/src/main/res/layout/item_news.xml new file mode 100644 index 0000000..ff87b90 --- /dev/null +++ b/app/src/main/res/layout/item_news.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-ru-rRU/strings.xml b/app/src/main/res/values-ru-rRU/strings.xml new file mode 100644 index 0000000..0befac9 --- /dev/null +++ b/app/src/main/res/values-ru-rRU/strings.xml @@ -0,0 +1,5 @@ + + + Изображение для новости + Главные новости + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000..75f6fd5 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,6 @@ + + + 16dp + 8dp + 4dp + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b8488bd..72aa9be 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,6 @@ + - AndroidAcademyNewsApp + AndroidAcademyNewsApp + Image for the selected news + Top news diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 5885930..a6806d9 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -8,4 +8,26 @@ @color/colorAccent + + + + + + +